diff --git a/apps/web/src/components/page-detail-editor.tsx b/apps/web/src/components/page-detail-editor.tsx index dbe7fd495e..e43736fda7 100644 --- a/apps/web/src/components/page-detail-editor.tsx +++ b/apps/web/src/components/page-detail-editor.tsx @@ -11,6 +11,7 @@ import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-block-s import { affinePluginsAtom } from '@toeverything/plugin-infra/manager'; import type { PluginUIAdapter } from '@toeverything/plugin-infra/type'; import type { ExpectedLayout } from '@toeverything/plugin-infra/type'; +import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import Head from 'next/head'; import type { FC } from 'react'; @@ -50,6 +51,11 @@ const EditorWrapper = memo(function EditorWrapper({ onLoad, isPublic, }: PageDetailEditorProps) { + const affinePluginsMap = useAtomValue(affinePluginsAtom); + const plugins = useMemo( + () => Object.values(affinePluginsMap), + [affinePluginsMap] + ); const blockSuiteWorkspace = workspace.blockSuiteWorkspace; const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId); if (!page) { @@ -88,12 +94,22 @@ const EditorWrapper = memo(function EditorWrapper({ updatedDate: Date.now(), }); localStorage.setItem('last_page_id', page.id); + let dispose = () => {}; if (onLoad) { - return onLoad(page, editor); + dispose = onLoad(page, editor); } - return () => {}; + const uiDecorators = plugins + .map(plugin => plugin.blockSuiteAdapter.uiDecorator) + .filter((ui): ui is PluginBlockSuiteAdapter['uiDecorator'] => + Boolean(ui) + ); + const disposes = uiDecorators.map(ui => ui(editor)); + return () => { + disposes.map(fn => fn()); + dispose(); + }; }, - [onLoad, setEditor] + [plugins, onLoad, setEditor] )} /> ); diff --git a/packages/component/src/components/block-suite-editor/index.tsx b/packages/component/src/components/block-suite-editor/index.tsx index 7e7cbc1db3..f9b74a507e 100644 --- a/packages/component/src/components/block-suite-editor/index.tsx +++ b/packages/component/src/components/block-suite-editor/index.tsx @@ -16,14 +16,6 @@ import { blockSuiteEditorHeaderStyle, blockSuiteEditorStyle, } from './index.css'; -import { bookmarkPlugin } from './plugins/bookmark'; - -export type EditorPlugin = { - flavour: string; - onInit?: (page: Page, editor: Readonly) => void; - onLoad?: (page: Page, editor: EditorContainer) => () => void; - render?: (props: { page: Page }) => ReactElement | null; -}; export type EditorProps = { page: Page; @@ -50,9 +42,6 @@ const ImagePreviewModal = lazy(() => })) ); -// todo(himself65): plugin-infra should support this -const plugins = [bookmarkPlugin]; - const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => { const { onLoad, page, mode, style, onInit } = props; const JotaiEditorContainer = useAtomValue( @@ -77,9 +66,6 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => { editor.page = page; if (page.root === null) { onInit(page, editor); - plugins.forEach(plugin => { - plugin.onInit?.(page, editor); - }); } } }, [editor, page, onInit]); @@ -88,7 +74,6 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => { if (editor.page && onLoad) { const disposes = [] as ((() => void) | undefined)[]; disposes.push(onLoad?.(page, editor)); - disposes.push(...plugins.map(plugin => plugin.onLoad?.(page, editor))); return () => { disposes .filter((dispose): dispose is () => void => !!dispose) @@ -201,12 +186,6 @@ export const BlockSuiteEditor = memo(function BlockSuiteEditor( )} )} - {plugins.map(plugin => { - const Renderer = plugin.render; - return Renderer ? ( - - ) : null; - })} ); }); diff --git a/packages/env/src/bootstrap.ts b/packages/env/src/bootstrap.ts index b1db8ce28a..352d0a0902 100644 --- a/packages/env/src/bootstrap.ts +++ b/packages/env/src/bootstrap.ts @@ -1,7 +1,11 @@ -import { config, getEnvironment, setupGlobal } from './config'; +import { config, setupGlobal } from './config'; -if (config.enablePlugin && !getEnvironment().isServer) { +setupGlobal(); + +if (config.enablePlugin && !environment.isServer) { import('@affine/copilot'); } -setupGlobal(); +if (!environment.isServer) { + import('@affine/bookmark-block'); +} diff --git a/packages/plugin-infra/package.json b/packages/plugin-infra/package.json index e9150c9597..804d994312 100644 --- a/packages/plugin-infra/package.json +++ b/packages/plugin-infra/package.json @@ -12,12 +12,22 @@ "dependencies": { "@affine/component": "workspace:*", "@affine/env": "workspace:*", - "@affine/workspace": "workspace:*" + "@affine/workspace": "workspace:*", + "@blocksuite/blocks": "0.0.0-20230531080915-ca9c55a2-nightly", + "@blocksuite/editor": "0.0.0-20230531080915-ca9c55a2-nightly", + "@blocksuite/global": "0.0.0-20230531080915-ca9c55a2-nightly", + "@blocksuite/lit": "0.0.0-20230531080915-ca9c55a2-nightly", + "@blocksuite/store": "0.0.0-20230531080915-ca9c55a2-nightly" }, "devDependencies": { "jotai": "^2.1.0" }, "peerDependencies": { + "@blocksuite/blocks": "*", + "@blocksuite/editor": "*", + "@blocksuite/global": "*", + "@blocksuite/lit": "*", + "@blocksuite/store": "*", "jotai": "*", "react": "*", "react-dom": "*" diff --git a/packages/plugin-infra/src/manager.ts b/packages/plugin-infra/src/manager.ts index a839986c95..0fbdae287f 100644 --- a/packages/plugin-infra/src/manager.ts +++ b/packages/plugin-infra/src/manager.ts @@ -4,27 +4,53 @@ import { atom } from 'jotai'; import type { AffinePlugin, Definition } from './type'; import type { Loader, PluginUIAdapter } from './type'; +import type { PluginBlockSuiteAdapter } from './type'; // todo: for now every plugin is enabled by default export const affinePluginsAtom = atom>>({}); const pluginLogger = new DebugLogger('affine:plugin'); -import { config } from '@affine/env'; + export function definePlugin( definition: Definition, - uiAdapterLoader?: Loader> + uiAdapterLoader?: Loader>, + blockSuiteAdapter?: Loader> ) { - if (!config.enablePlugin) { - return; - } const basePlugin = { definition, uiAdapter: {}, + blockSuiteAdapter: {}, }; + rootStore.set(affinePluginsAtom, plugins => ({ ...plugins, [definition.id]: basePlugin, })); + + if (blockSuiteAdapter) { + const updateAdapter = (adapter: Partial) => { + rootStore.set(affinePluginsAtom, plugins => ({ + ...plugins, + [definition.id]: { + ...basePlugin, + blockSuiteAdapter: adapter, + }, + })); + }; + + blockSuiteAdapter + .load() + .then(({ default: adapter }) => updateAdapter(adapter)); + + if (import.meta.webpackHot) { + blockSuiteAdapter.hotModuleReload(async _ => { + const adapter = (await _).default; + updateAdapter(adapter); + pluginLogger.info('[HMR] Plugin', definition.id, 'hot reloaded.'); + }); + } + } + if (uiAdapterLoader) { const updateAdapter = (adapter: Partial) => { rootStore.set(affinePluginsAtom, plugins => ({ @@ -39,6 +65,7 @@ export function definePlugin( uiAdapterLoader .load() .then(({ default: adapter }) => updateAdapter(adapter)); + if (import.meta.webpackHot) { uiAdapterLoader.hotModuleReload(async _ => { const adapter = (await _).default; diff --git a/packages/plugin-infra/src/type.ts b/packages/plugin-infra/src/type.ts index 5ffd8f43b8..58ac07c520 100644 --- a/packages/plugin-infra/src/type.ts +++ b/packages/plugin-infra/src/type.ts @@ -5,6 +5,9 @@ * AFFiNE Plugin System Types */ +import type { EditorContainer } from '@blocksuite/editor'; +import type { Workspace } from '@blocksuite/store'; +import type { Page } from '@playwright/test'; import type { WritableAtom } from 'jotai'; import type { ReactElement } from 'react'; import type { MosaicDirection, MosaicNode } from 'react-mosaic-component'; @@ -152,6 +155,14 @@ export type PluginUIAdapter = { debugContent: Adapter>; }; +type Cleanup = () => void; + +export type PluginBlockSuiteAdapter = { + storeDecorator: (currentWorkspace: Workspace) => Promise; + pageDecorator: (currentPage: Page) => Cleanup; + uiDecorator: (root: EditorContainer) => Cleanup; +}; + export type PluginAdapterCreator = ( context: AffinePluginContext ) => PluginUIAdapter; @@ -159,4 +170,5 @@ export type PluginAdapterCreator = ( export type AffinePlugin = { definition: Definition; uiAdapter: Partial; + blockSuiteAdapter: Partial; }; diff --git a/plugins/bookmark-block/README.md b/plugins/bookmark-block/README.md new file mode 100644 index 0000000000..e29a477b42 --- /dev/null +++ b/plugins/bookmark-block/README.md @@ -0,0 +1,5 @@ +# `@affine/bookmark-block` + +> A block for bookmarking a website + +![preview](assets/preview.png) diff --git a/plugins/bookmark-block/assets/preview.png b/plugins/bookmark-block/assets/preview.png new file mode 100644 index 0000000000..7b5ea5c0a9 Binary files /dev/null and b/plugins/bookmark-block/assets/preview.png differ diff --git a/plugins/bookmark-block/package.json b/plugins/bookmark-block/package.json new file mode 100644 index 0000000000..2372ee7ab6 --- /dev/null +++ b/plugins/bookmark-block/package.json @@ -0,0 +1,20 @@ +{ + "name": "@affine/bookmark-block", + "private": true, + "main": "./src/index.ts", + "module": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@toeverything/plugin-infra": "workspace:*" + }, + "devDependencies": { + "react": "18.3.0-canary-16d053d59-20230506", + "react-dom": "18.3.0-canary-16d053d59-20230506" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } +} diff --git a/plugins/bookmark-block/src/blocksuite/index.tsx b/plugins/bookmark-block/src/blocksuite/index.tsx new file mode 100644 index 0000000000..a7e04a9d47 --- /dev/null +++ b/plugins/bookmark-block/src/blocksuite/index.tsx @@ -0,0 +1,29 @@ +import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { BookMarkUI } from './ui'; + +export default { + uiDecorator: editor => { + if ( + editor.parentElement && + editor.page.awarenessStore.getFlag('enable_bookmark_operation') + ) { + const div = document.createElement('div'); + editor.parentElement.appendChild(div); + const root = createRoot(div); + root.render( + + + + ); + return () => { + root.unmount(); + div.remove(); + }; + } else { + return () => {}; + } + }, +} satisfies Partial; diff --git a/packages/component/src/components/block-suite-editor/plugins/bookmark.tsx b/plugins/bookmark-block/src/blocksuite/ui.tsx similarity index 92% rename from packages/component/src/components/block-suite-editor/plugins/bookmark.tsx rename to plugins/bookmark-block/src/blocksuite/ui.tsx index 0f2e525dc2..28f4066294 100644 --- a/packages/component/src/components/block-suite-editor/plugins/bookmark.tsx +++ b/plugins/bookmark-block/src/blocksuite/ui.tsx @@ -1,3 +1,5 @@ +import { MenuItem, PureMenu } from '@affine/component'; +import { MuiClickAwayListener } from '@affine/component'; import type { SerializedBlock } from '@blocksuite/blocks'; import { getCurrentBlockRange, @@ -6,14 +8,17 @@ import { } from '@blocksuite/blocks/std'; import type { Page } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store'; +import type { FC } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { MenuItem, MuiClickAwayListener, PureMenu } from '../../..'; -import type { EditorPlugin } from '..'; +export type BookMarkProps = { + page: Page; +}; type ShortcutMap = { [key: string]: (e: KeyboardEvent, page: Page) => void; }; + const menuOptions = [ { id: 'dismiss', @@ -90,7 +95,7 @@ const shouldShowBookmarkMenu = (pastedBlocks: SerializedBlock[]) => { } return !!firstBlock.text[0].attributes?.link; }; -const BookMarkMenu: EditorPlugin['render'] = ({ page }) => { +export const BookMarkUI: FC = ({ page }) => { const [anchor, setAnchor] = useState(null); const [selectedOption, setSelectedOption] = useState( menuOptions[0].id @@ -213,13 +218,3 @@ const BookMarkMenu: EditorPlugin['render'] = ({ page }) => { ) : null; }; - -const Defender: EditorPlugin['render'] = props => { - const flag = props.page.awarenessStore.getFlag('enable_bookmark_operation'); - return flag ? : null; -}; - -export const bookmarkPlugin: EditorPlugin = { - flavour: 'bookmark', - render: Defender, -}; diff --git a/plugins/bookmark-block/src/index.ts b/plugins/bookmark-block/src/index.ts new file mode 100644 index 0000000000..8d26bdb83c --- /dev/null +++ b/plugins/bookmark-block/src/index.ts @@ -0,0 +1,32 @@ +import { definePlugin } from '@toeverything/plugin-infra/manager'; +import { ReleaseStage } from '@toeverything/plugin-infra/type'; + +definePlugin( + { + id: 'com.blocksuite.bookmark-block', + name: { + fallback: 'BlockSuite Bookmark Block', + i18nKey: 'com.blocksuite.bookmark.name', + }, + description: { + fallback: 'Bookmark block', + }, + publisher: { + name: { + fallback: 'AFFiNE', + }, + link: 'https://affine.pro', + }, + stage: ReleaseStage.NIGHTLY, + version: '0.0.1', + }, + undefined, + { + load: () => import('./blocksuite/index'), + hotModuleReload: onHot => + import.meta.webpackHot && + import.meta.webpackHot.accept('./blocksuite', () => + onHot(import('./blocksuite/index')) + ), + } +); diff --git a/plugins/bookmark-block/tsconfig.json b/plugins/bookmark-block/tsconfig.json new file mode 100644 index 0000000000..7513033f03 --- /dev/null +++ b/plugins/bookmark-block/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src"] +} diff --git a/plugins/copilot/src/UI/jotai.ts b/plugins/copilot/src/UI/jotai.ts deleted file mode 100644 index 82427c408e..0000000000 --- a/plugins/copilot/src/UI/jotai.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { atom } from 'jotai'; - -export const contentExpandAtom = atom(false); diff --git a/yarn.lock b/yarn.lock index f390990e11..22f9285204 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,6 +30,19 @@ __metadata: languageName: unknown linkType: soft +"@affine/bookmark-block@workspace:plugins/bookmark-block": + version: 0.0.0-use.local + resolution: "@affine/bookmark-block@workspace:plugins/bookmark-block" + dependencies: + "@toeverything/plugin-infra": "workspace:*" + react: 18.3.0-canary-16d053d59-20230506 + react-dom: 18.3.0-canary-16d053d59-20230506 + peerDependencies: + react: "*" + react-dom: "*" + languageName: unknown + linkType: soft + "@affine/cli@workspace:*, @affine/cli@workspace:packages/cli": version: 0.0.0-use.local resolution: "@affine/cli@workspace:packages/cli" @@ -9087,8 +9100,18 @@ __metadata: "@affine/component": "workspace:*" "@affine/env": "workspace:*" "@affine/workspace": "workspace:*" + "@blocksuite/blocks": 0.0.0-20230531080915-ca9c55a2-nightly + "@blocksuite/editor": 0.0.0-20230531080915-ca9c55a2-nightly + "@blocksuite/global": 0.0.0-20230531080915-ca9c55a2-nightly + "@blocksuite/lit": 0.0.0-20230531080915-ca9c55a2-nightly + "@blocksuite/store": 0.0.0-20230531080915-ca9c55a2-nightly jotai: ^2.1.0 peerDependencies: + "@blocksuite/blocks": "*" + "@blocksuite/editor": "*" + "@blocksuite/global": "*" + "@blocksuite/lit": "*" + "@blocksuite/store": "*" jotai: "*" react: "*" react-dom: "*"