mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: add @affine/bookmark-block plugin (#2618)
This commit is contained in:
@@ -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]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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<EditorContainer>) => 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(
|
||||
)}
|
||||
</Suspense>
|
||||
)}
|
||||
{plugins.map(plugin => {
|
||||
const Renderer = plugin.render;
|
||||
return Renderer ? (
|
||||
<Renderer page={props.page} key={plugin.flavour} />
|
||||
) : null;
|
||||
})}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
||||
10
packages/env/src/bootstrap.ts
vendored
10
packages/env/src/bootstrap.ts
vendored
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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": "*"
|
||||
|
||||
@@ -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<Record<string, AffinePlugin<string>>>({});
|
||||
|
||||
const pluginLogger = new DebugLogger('affine:plugin');
|
||||
import { config } from '@affine/env';
|
||||
|
||||
export function definePlugin<ID extends string>(
|
||||
definition: Definition<ID>,
|
||||
uiAdapterLoader?: Loader<Partial<PluginUIAdapter>>
|
||||
uiAdapterLoader?: Loader<Partial<PluginUIAdapter>>,
|
||||
blockSuiteAdapter?: Loader<Partial<PluginBlockSuiteAdapter>>
|
||||
) {
|
||||
if (!config.enablePlugin) {
|
||||
return;
|
||||
}
|
||||
const basePlugin = {
|
||||
definition,
|
||||
uiAdapter: {},
|
||||
blockSuiteAdapter: {},
|
||||
};
|
||||
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
...plugins,
|
||||
[definition.id]: basePlugin,
|
||||
}));
|
||||
|
||||
if (blockSuiteAdapter) {
|
||||
const updateAdapter = (adapter: Partial<PluginBlockSuiteAdapter>) => {
|
||||
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<PluginUIAdapter>) => {
|
||||
rootStore.set(affinePluginsAtom, plugins => ({
|
||||
@@ -39,6 +65,7 @@ export function definePlugin<ID extends string>(
|
||||
uiAdapterLoader
|
||||
.load()
|
||||
.then(({ default: adapter }) => updateAdapter(adapter));
|
||||
|
||||
if (import.meta.webpackHot) {
|
||||
uiAdapterLoader.hotModuleReload(async _ => {
|
||||
const adapter = (await _).default;
|
||||
|
||||
@@ -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<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
type Cleanup = () => void;
|
||||
|
||||
export type PluginBlockSuiteAdapter = {
|
||||
storeDecorator: (currentWorkspace: Workspace) => Promise<void>;
|
||||
pageDecorator: (currentPage: Page) => Cleanup;
|
||||
uiDecorator: (root: EditorContainer) => Cleanup;
|
||||
};
|
||||
|
||||
export type PluginAdapterCreator = (
|
||||
context: AffinePluginContext
|
||||
) => PluginUIAdapter;
|
||||
@@ -159,4 +170,5 @@ export type PluginAdapterCreator = (
|
||||
export type AffinePlugin<ID extends string> = {
|
||||
definition: Definition<ID>;
|
||||
uiAdapter: Partial<PluginUIAdapter>;
|
||||
blockSuiteAdapter: Partial<PluginBlockSuiteAdapter>;
|
||||
};
|
||||
|
||||
5
plugins/bookmark-block/README.md
Normal file
5
plugins/bookmark-block/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# `@affine/bookmark-block`
|
||||
|
||||
> A block for bookmarking a website
|
||||
|
||||

|
||||
BIN
plugins/bookmark-block/assets/preview.png
Normal file
BIN
plugins/bookmark-block/assets/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
20
plugins/bookmark-block/package.json
Normal file
20
plugins/bookmark-block/package.json
Normal file
@@ -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": "*"
|
||||
}
|
||||
}
|
||||
29
plugins/bookmark-block/src/blocksuite/index.tsx
Normal file
29
plugins/bookmark-block/src/blocksuite/index.tsx
Normal file
@@ -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(
|
||||
<StrictMode>
|
||||
<BookMarkUI page={editor.page} />
|
||||
</StrictMode>
|
||||
);
|
||||
return () => {
|
||||
root.unmount();
|
||||
div.remove();
|
||||
};
|
||||
} else {
|
||||
return () => {};
|
||||
}
|
||||
},
|
||||
} satisfies Partial<PluginBlockSuiteAdapter>;
|
||||
@@ -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<BookMarkProps> = ({ page }) => {
|
||||
const [anchor, setAnchor] = useState<Range | null>(null);
|
||||
const [selectedOption, setSelectedOption] = useState<string>(
|
||||
menuOptions[0].id
|
||||
@@ -213,13 +218,3 @@ const BookMarkMenu: EditorPlugin['render'] = ({ page }) => {
|
||||
</MuiClickAwayListener>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const Defender: EditorPlugin['render'] = props => {
|
||||
const flag = props.page.awarenessStore.getFlag('enable_bookmark_operation');
|
||||
return flag ? <BookMarkMenu {...props} /> : null;
|
||||
};
|
||||
|
||||
export const bookmarkPlugin: EditorPlugin = {
|
||||
flavour: 'bookmark',
|
||||
render: Defender,
|
||||
};
|
||||
32
plugins/bookmark-block/src/index.ts
Normal file
32
plugins/bookmark-block/src/index.ts
Normal file
@@ -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'))
|
||||
),
|
||||
}
|
||||
);
|
||||
4
plugins/bookmark-block/tsconfig.json
Normal file
4
plugins/bookmark-block/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src"]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const contentExpandAtom = atom(false);
|
||||
23
yarn.lock
23
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: "*"
|
||||
|
||||
Reference in New Issue
Block a user