diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 41e8640d2..742f53e49 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -68,6 +68,7 @@ const nextConfig = { '@affine/i18n', '@affine/debug', '@affine/env', + '@affine/templates', ], publicRuntimeConfig: { PROJECT_NAME: process.env.npm_package_name, diff --git a/apps/web/src/components/blocksuite/block-suite-editor/index.tsx b/apps/web/src/components/blocksuite/block-suite-editor/index.tsx index 230e994e3..c507b085a 100644 --- a/apps/web/src/components/blocksuite/block-suite-editor/index.tsx +++ b/apps/web/src/components/blocksuite/block-suite-editor/index.tsx @@ -1,97 +1,2 @@ -import { config } from '@affine/env'; -import { BlockHub } from '@blocksuite/blocks'; -import { EditorContainer } from '@blocksuite/editor'; -import type { Page } from '@blocksuite/store'; -import { assertExists } from '@blocksuite/store'; -import { CSSProperties, useEffect, useRef } from 'react'; - -import { BlockSuiteWorkspace } from '../../../shared'; - -export type EditorProps = { - blockSuiteWorkspace: BlockSuiteWorkspace; - page: Page; - mode: 'page' | 'edgeless'; - onInit: (page: Page, editor: Readonly) => void; - onLoad?: (page: Page, editor: EditorContainer) => void; - style?: CSSProperties; -}; - -declare global { - // eslint-disable-next-line no-var - var currentBlockSuiteWorkspace: BlockSuiteWorkspace | undefined; - // eslint-disable-next-line no-var - var currentPage: Page | undefined; -} - -export const BlockSuiteEditor = (props: EditorProps) => { - const page = props.page; - const editorRef = useRef(null); - const blockHubRef = useRef(null); - if (editorRef.current === null) { - editorRef.current = new EditorContainer(); - // fixme(himself65): remove `globalThis.editor` - // @ts-expect-error - globalThis.editor = editorRef.current; - } - const ref = useRef(null); - useEffect(() => { - if (editorRef.current) { - editorRef.current.mode = props.mode; - } - }, [props.mode]); - - useEffect(() => { - const editor = editorRef.current; - if (!editor || !ref.current || !page) { - return; - } - - editor.page = page; - if (page.root === null) { - props.onInit(page, editor); - } - if (config.exposeInternal) { - globalThis.currentBlockSuiteWorkspace = props.blockSuiteWorkspace; - globalThis.currentPage = page; - } - props.onLoad?.(page, editor); - return; - }, [page, props]); - - useEffect(() => { - const editor = editorRef.current; - const container = ref.current; - - if (!editor || !container || !page) { - return; - } - if ( - page.awarenessStore.getFlag('enable_block_hub') && - props.mode === 'page' - ) { - editor.createBlockHub().then(blockHub => { - if (blockHubRef.current) { - blockHubRef.current.remove(); - } - blockHubRef.current = blockHub; - const toolWrapper = document.querySelector('#toolWrapper'); - assertExists(toolWrapper); - toolWrapper.appendChild(blockHub); - }); - } - - container.appendChild(editor); - return () => { - blockHubRef.current?.remove(); - container.removeChild(editor); - }; - }, [page, props.mode]); - return ( -
- ); -}; +export type { EditorProps } from '@affine/component/block-suite-editor'; +export { BlockSuiteEditor } from '@affine/component/block-suite-editor'; diff --git a/apps/web/src/pages/preview/[previewId].tsx b/apps/web/src/pages/preview/[previewId].tsx index 5c8463191..47ec55d11 100644 --- a/apps/web/src/pages/preview/[previewId].tsx +++ b/apps/web/src/pages/preview/[previewId].tsx @@ -98,7 +98,15 @@ export const getStaticProps: GetStaticProps< const fs = await import('node:fs/promises'); const path = await import('node:path'); const markdown: string = await fs.readFile( - path.resolve(process.cwd(), 'src', 'templates', `${name}.md`), + path.resolve( + process.cwd(), + '..', + '..', + 'packages', + 'templates', + 'src', + `${name}.md` + ), 'utf8' ); const title = markdown diff --git a/apps/web/src/utils/blocksuite.ts b/apps/web/src/utils/blocksuite.ts index dec2ecbff..d7662c899 100644 --- a/apps/web/src/utils/blocksuite.ts +++ b/apps/web/src/utils/blocksuite.ts @@ -1,9 +1,8 @@ import { DebugLogger } from '@affine/debug'; +import markdown from '@affine/templates/Welcome-to-AFFiNE.md'; import { EditorContainer } from '@blocksuite/editor'; import { Page } from '@blocksuite/store'; -import markdown from '../templates/Welcome-to-AFFiNE.md'; - const demoTitle = markdown .split('\n') .splice(0, 1) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 943457f44..f70175678 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -16,8 +16,7 @@ "jsx": "preserve", "jsxImportSource": "@emotion/react", "incremental": true, - "experimentalDecorators": true, - "baseUrl": "." + "experimentalDecorators": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/types/types.d.ts"], "exclude": ["node_modules"] diff --git a/package.json b/package.json index 8319b9836..014435548 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,8 @@ "@storybook/components": "^7.0.0-rc.1", "@storybook/core-events": "^7.0.0-rc.1", "@storybook/global": "^5.0.0", - "@storybook/theming": "^7.0.0-rc.1" + "@storybook/theming": "^7.0.0-rc.1", + "jest-mock": "^29.5.0" } }, "packageManager": "pnpm@7.28.0" diff --git a/packages/component/.storybook/main.ts b/packages/component/.storybook/main.ts index 594cd29b6..165cf878b 100644 --- a/packages/component/.storybook/main.ts +++ b/packages/component/.storybook/main.ts @@ -3,7 +3,11 @@ import type { StorybookConfig } from '@storybook/react-vite'; export default { stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], staticDirs: ['../../../apps/web/public'], - addons: ['@storybook/addon-links', 'storybook-dark-mode-v7'], + addons: [ + '@storybook/addon-links', + 'storybook-dark-mode-v7', + '@storybook/addon-interactions', + ], framework: { name: '@storybook/react-vite', options: { diff --git a/packages/component/.storybook/preview-head.html b/packages/component/.storybook/preview-head.html new file mode 100644 index 000000000..e55104010 --- /dev/null +++ b/packages/component/.storybook/preview-head.html @@ -0,0 +1,3 @@ + diff --git a/packages/component/.storybook/preview.tsx b/packages/component/.storybook/preview.tsx index 565436a13..2e1eeda73 100644 --- a/packages/component/.storybook/preview.tsx +++ b/packages/component/.storybook/preview.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import '@blocksuite/editor/themes/affine.css'; + import { getDarkTheme, getLightTheme, ThemeProvider } from '../src'; import { useDarkMode } from 'storybook-dark-mode-v7'; export const parameters = { diff --git a/packages/component/package.json b/packages/component/package.json index ebf2fdf00..d4f93c1e6 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -4,8 +4,13 @@ "version": "0.3.1", "main": "./src/index.ts", "scripts": { - "storybook": "storybook dev", - "build-storybook": "storybook build" + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "test-storybook": "test-storybook" + }, + "exports": { + ".": "./src/index.ts", + "./block-suite-editor": "./src/components/block-suite-editor/index.tsx" }, "dependencies": { "@affine/debug": "workspace:*", @@ -31,13 +36,18 @@ "devDependencies": { "@storybook/addon-actions": "7.0.0-rc.1", "@storybook/addon-essentials": "7.0.0-rc.1", + "@storybook/addon-interactions": "7.0.0-rc.1", "@storybook/addon-links": "7.0.0-rc.1", "@storybook/builder-vite": "7.0.0-rc.1", + "@storybook/jest": "0.0.11-next.0", "@storybook/react": "7.0.0-rc.1", "@storybook/react-vite": "7.0.0-rc.1", + "@storybook/test-runner": "^0.10.0-next.10", + "@storybook/testing-library": "0.0.14-next.1", "@types/react": "^18.0.28", "@types/react-dom": "18.0.11", "@vitejs/plugin-react": "^3.1.0", + "jest-mock": "^28.1.3", "storybook": "7.0.0-rc.1", "storybook-dark-mode-v7": "3.0.0-alpha.0", "typescript": "^4.9.5", diff --git a/packages/component/src/components/block-suite-editor/index.stories.tsx b/packages/component/src/components/block-suite-editor/index.stories.tsx new file mode 100644 index 000000000..0bd1554fa --- /dev/null +++ b/packages/component/src/components/block-suite-editor/index.stories.tsx @@ -0,0 +1,83 @@ +/* deepscan-disable USELESS_ARROW_FUNC_BIND */ +import { __unstableSchemas, builtInSchemas } from '@blocksuite/blocks/models'; +import { EditorContainer } from '@blocksuite/editor'; +import { Page, Workspace } from '@blocksuite/store'; +import { expect } from '@storybook/jest'; +import { Meta, StoryFn } from '@storybook/react'; +import { useEffect, useState } from 'react'; + +import { BlockSuiteEditor, EditorProps } from '.'; + +function initPage(page: Page, editor: Readonly): void { + // Add page block and surface block at root level + const pageBlockId = page.addBlockByFlavour('affine:page', { + title: new page.Text(''), + }); + page.addBlockByFlavour('affine:surface', {}, null); + const frameId = page.addBlockByFlavour('affine:frame', {}, pageBlockId); + page.addBlockByFlavour('affine:paragraph', {}, frameId); + page.resetHistory(); +} + +const blockSuiteWorkspace = new Workspace({ + id: 'test', + blobOptionsGetter: () => void 0, +}); +blockSuiteWorkspace.register(builtInSchemas).register(__unstableSchemas); +const promise = new Promise(resolve => { + blockSuiteWorkspace.slots.pageAdded.once(() => { + resolve(); + }); +}); +blockSuiteWorkspace.createPage('page0'); + +type BlockSuiteMeta = Meta; +export default { + title: 'BlockSuite/Editor', + component: BlockSuiteEditor, +} satisfies BlockSuiteMeta; + +const Template: StoryFn = (args: EditorProps) => { + const [loaded, setLoaded] = useState(false); + const page = blockSuiteWorkspace.getPage('page0'); + useEffect(() => { + promise + .then(() => setLoaded(true)) + .then(() => { + document.dispatchEvent(new Event('blocksuite:ready')); + }); + }, []); + if (!loaded || !page) return
Loading...
; + return ( + + ); +}; +export const Empty = Template.bind({}); +Empty.play = async ({ canvasElement }) => { + await new Promise(resolve => { + document.addEventListener('blocksuite:ready', () => resolve(), { + once: true, + }); + }); + + const editorContainer = canvasElement.querySelector( + '[data-testid="editor-test-page0"]' + ) as HTMLDivElement; + expect(editorContainer).not.toBeNull(); + await new Promise(resolve => { + setTimeout(() => resolve(), 50); + }); + const editor = editorContainer.querySelector( + 'editor-container' + ) as EditorContainer; + expect(editor).not.toBeNull(); +}; + +Empty.args = { + onInit: initPage, + mode: 'page', +}; diff --git a/packages/component/src/components/block-suite-editor/index.tsx b/packages/component/src/components/block-suite-editor/index.tsx new file mode 100644 index 000000000..af963bde9 --- /dev/null +++ b/packages/component/src/components/block-suite-editor/index.tsx @@ -0,0 +1,94 @@ +import { BlockHub } from '@blocksuite/blocks'; +import { EditorContainer } from '@blocksuite/editor'; +import type { Page, Workspace } from '@blocksuite/store'; +import { CSSProperties, useEffect, useRef } from 'react'; + +export type EditorProps = { + blockSuiteWorkspace: Workspace; + page: Page; + mode: 'page' | 'edgeless'; + onInit: (page: Page, editor: Readonly) => void; + onLoad?: (page: Page, editor: EditorContainer) => void; + style?: CSSProperties; +}; + +declare global { + // eslint-disable-next-line no-var + var currentBlockSuiteWorkspace: Workspace | undefined; + // eslint-disable-next-line no-var + var currentPage: Page | undefined; + // eslint-disable-next-line no-var + var currentEditor: EditorContainer | undefined; +} + +export const BlockSuiteEditor = (props: EditorProps) => { + const page = props.page; + const editorRef = useRef(null); + const blockHubRef = useRef(null); + if (editorRef.current === null) { + editorRef.current = new EditorContainer(); + globalThis.currentEditor = editorRef.current; + } + const ref = useRef(null); + useEffect(() => { + if (editorRef.current) { + editorRef.current.mode = props.mode; + } + }, [props.mode]); + + useEffect(() => { + const editor = editorRef.current; + if (!editor || !ref.current || !page) { + return; + } + + editor.page = page; + if (page.root === null) { + props.onInit(page, editor); + } + props.onLoad?.(page, editor); + return; + }, [page, props]); + + useEffect(() => { + const editor = editorRef.current; + const container = ref.current; + + if (!editor || !container || !page) { + return; + } + if ( + page.awarenessStore.getFlag('enable_block_hub') && + props.mode === 'page' + ) { + editor.createBlockHub().then(blockHub => { + if (blockHubRef.current) { + blockHubRef.current.remove(); + } + blockHubRef.current = blockHub; + const toolWrapper = document.querySelector('#toolWrapper'); + if (!toolWrapper) { + console.warn( + 'toolWrapper not found, block hub feature will not be available.' + ); + } else { + toolWrapper.appendChild(blockHub); + } + }); + } + + container.appendChild(editor); + return () => { + blockHubRef.current?.remove(); + container.removeChild(editor); + }; + }, [page, props.mode]); + return ( +
+ ); +}; diff --git a/packages/component/src/index.ts b/packages/component/src/index.ts index b56b99226..959d7b410 100644 --- a/packages/component/src/index.ts +++ b/packages/component/src/index.ts @@ -1,4 +1,3 @@ -// export * from './components/BlockSuiteEditor'; export * from './components/ListSkeleton'; export * from './styles'; export * from './ui/breadcrumbs'; diff --git a/packages/component/src/stories/Breadcrumbs.stories.tsx b/packages/component/src/stories/Breadcrumbs.stories.tsx index 14fa83a1d..2eee4c2d3 100644 --- a/packages/component/src/stories/Breadcrumbs.stories.tsx +++ b/packages/component/src/stories/Breadcrumbs.stories.tsx @@ -1,20 +1,33 @@ /* deepscan-disable USELESS_ARROW_FUNC_BIND */ import { Link, Typography } from '@mui/material'; -import { Meta, Story } from '@storybook/react'; +import { expect } from '@storybook/jest'; +import { Meta, StoryFn } from '@storybook/react'; +import { within } from '@storybook/testing-library'; import { Breadcrumbs } from '..'; export default { title: 'AFFiNE/Breadcrumbs', component: Breadcrumbs, -} as Meta; +} as Meta; -const Template: Story = args => ; +const Template: StoryFn = args => ; export const Primary = Template.bind(undefined); +Primary.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + const text = canvas.getByText('AFFiNE'); + expect(text.getAttribute('data-testid')).toBe('affine'); +}; Primary.args = { children: [ - + AFFiNE , diff --git a/packages/component/src/stories/Button.stories.tsx b/packages/component/src/stories/Button.stories.tsx index f7c935ea1..e385477d5 100644 --- a/packages/component/src/stories/Button.stories.tsx +++ b/packages/component/src/stories/Button.stories.tsx @@ -1,5 +1,5 @@ /* deepscan-disable USELESS_ARROW_FUNC_BIND */ -import { Meta, Story } from '@storybook/react'; +import { Meta, StoryFn } from '@storybook/react'; import { Button } from '..'; import { ButtonProps } from '../ui/button/interface'; @@ -13,7 +13,7 @@ export default { }, } as Meta; -const Template: Story = args =>