feat: add @affine/bookmark-block plugin (#2618)

This commit is contained in:
Himself65
2023-05-31 17:08:03 +08:00
committed by GitHub
parent 4e1e4e9435
commit 454f1887cf
15 changed files with 202 additions and 49 deletions

View File

@@ -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]
)}
/>
);

View File

@@ -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>
);
});

View File

@@ -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');
}

View File

@@ -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": "*"

View File

@@ -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;

View File

@@ -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>;
};

View File

@@ -0,0 +1,5 @@
# `@affine/bookmark-block`
> A block for bookmarking a website
![preview](assets/preview.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View 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": "*"
}
}

View 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>;

View File

@@ -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,
};

View 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'))
),
}
);

View File

@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["./src"]
}

View File

@@ -1,3 +0,0 @@
import { atom } from 'jotai';
export const contentExpandAtom = atom(false);

View File

@@ -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: "*"