refactor(component): editor component (#1834)
This commit is contained in:
@@ -77,6 +77,7 @@ __metadata:
|
|||||||
react-dnd: ^16.0.1
|
react-dnd: ^16.0.1
|
||||||
react-dnd-html5-backend: ^16.0.1
|
react-dnd-html5-backend: ^16.0.1
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
|
react-error-boundary: ^4.0.3
|
||||||
react-is: ^18.2.0
|
react-is: ^18.2.0
|
||||||
serve: ^14.2.0
|
serve: ^14.2.0
|
||||||
storybook: ^7.0.2
|
storybook: ^7.0.2
|
||||||
@@ -14357,6 +14358,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-error-boundary@npm:^4.0.3":
|
||||||
|
version: 4.0.3
|
||||||
|
resolution: "react-error-boundary@npm:4.0.3"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.12.5
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.13.1"
|
||||||
|
checksum: 50813803d3f03eb2ca07c1250a4a001ffe054f5b3f49b15ea5f0a4e1108f549bc7d8def15db51d518516997d34cebc663f4276246301b4a40a72e83a784f5c38
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react-i18next@npm:^12.2.0":
|
"react-i18next@npm:^12.2.0":
|
||||||
version: 12.2.0
|
version: 12.2.0
|
||||||
resolution: "react-i18next@npm:12.2.0"
|
resolution: "react-i18next@npm:12.2.0"
|
||||||
|
|||||||
@@ -40,13 +40,7 @@ const Editor: React.FC<{
|
|||||||
return <>loading...</>;
|
return <>loading...</>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<BlockSuiteEditor
|
<BlockSuiteEditor page={page} mode="page" onInit={onInit} onLoad={onLoad} />
|
||||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
|
||||||
page={page}
|
|
||||||
mode="page"
|
|
||||||
onInit={onInit}
|
|
||||||
onLoad={onLoad}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
|
|||||||
height: 'calc(100% - 52px)',
|
height: 'calc(100% - 52px)',
|
||||||
}}
|
}}
|
||||||
key={pageId}
|
key={pageId}
|
||||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
|
||||||
mode={isPublic ? 'page' : currentMode}
|
mode={isPublic ? 'page' : currentMode}
|
||||||
page={page}
|
page={page}
|
||||||
onInit={useCallback(
|
onInit={useCallback(
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ const BroadcastPage: React.FC = () => {
|
|||||||
const [provider, setProvider] = useState<BroadCastChannelProvider | null>(
|
const [provider, setProvider] = useState<BroadCastChannelProvider | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
|
||||||
globalThis.currentBlockSuiteWorkspace = blockSuiteWorkspace;
|
|
||||||
}, [blockSuiteWorkspace]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const provider = createBroadCastChannelProvider(blockSuiteWorkspace);
|
const provider = createBroadCastChannelProvider(blockSuiteWorkspace);
|
||||||
setProvider(provider);
|
setProvider(provider);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-error-boundary": "^4.0.3",
|
||||||
"react-is": "^18.2.0"
|
"react-is": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { Page } from '@blocksuite/store';
|
|||||||
import { Workspace } from '@blocksuite/store';
|
import { Workspace } from '@blocksuite/store';
|
||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import type { Meta, StoryFn } from '@storybook/react';
|
import type { Meta, StoryFn } from '@storybook/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import type { EditorProps } from '.';
|
import type { EditorProps } from '.';
|
||||||
import { BlockSuiteEditor } from '.';
|
import { BlockSuiteEditor } from '.';
|
||||||
@@ -40,19 +41,26 @@ export default {
|
|||||||
component: BlockSuiteEditor,
|
component: BlockSuiteEditor,
|
||||||
} satisfies BlockSuiteMeta;
|
} satisfies BlockSuiteMeta;
|
||||||
|
|
||||||
const Template: StoryFn<EditorProps> = (args: EditorProps) => {
|
const Template: StoryFn<EditorProps> = (props: EditorProps) => {
|
||||||
return (
|
return (
|
||||||
<BlockSuiteEditor
|
<>
|
||||||
{...args}
|
<BlockSuiteEditor {...props} page={page} mode="page" />
|
||||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
<div
|
||||||
page={page}
|
style={{
|
||||||
/>
|
position: 'absolute',
|
||||||
|
right: 12,
|
||||||
|
bottom: 12,
|
||||||
|
}}
|
||||||
|
id="toolWrapper"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Empty = Template.bind({});
|
export const Empty = Template.bind({});
|
||||||
Empty.play = async ({ canvasElement }) => {
|
Empty.play = async ({ canvasElement }) => {
|
||||||
const editorContainer = canvasElement.querySelector(
|
const editorContainer = canvasElement.querySelector(
|
||||||
'[data-testid="editor-test-page0"]'
|
'[data-testid="editor-page0"]'
|
||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
expect(editorContainer).not.toBeNull();
|
expect(editorContainer).not.toBeNull();
|
||||||
await new Promise<void>(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
@@ -67,3 +75,45 @@ Empty.play = async ({ canvasElement }) => {
|
|||||||
Empty.args = {
|
Empty.args = {
|
||||||
mode: 'page',
|
mode: 'page',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Error: StoryFn = () => {
|
||||||
|
const [props, setProps] = useState<Pick<EditorProps, 'page' | 'onInit'>>({
|
||||||
|
page: null!,
|
||||||
|
onInit: null!,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<BlockSuiteEditor
|
||||||
|
{...props}
|
||||||
|
mode="page"
|
||||||
|
onReset={() => {
|
||||||
|
setProps({
|
||||||
|
page,
|
||||||
|
onInit: initPage,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Error.play = async ({ canvasElement }) => {
|
||||||
|
{
|
||||||
|
const editorContainer = canvasElement.querySelector(
|
||||||
|
'[data-testid="editor-page0"]'
|
||||||
|
);
|
||||||
|
expect(editorContainer).toBeNull();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const button = canvasElement.querySelector(
|
||||||
|
'[data-testid="error-fallback-reset-button"]'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
expect(button).not.toBeNull();
|
||||||
|
button.click();
|
||||||
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 50));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const editorContainer = canvasElement.querySelector(
|
||||||
|
'[data-testid="editor-page0"]'
|
||||||
|
);
|
||||||
|
expect(editorContainer).not.toBeNull();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import type { BlockHub } from '@blocksuite/blocks';
|
import type { BlockHub } from '@blocksuite/blocks';
|
||||||
import { EditorContainer } from '@blocksuite/editor';
|
import { EditorContainer } from '@blocksuite/editor';
|
||||||
import type { Page, Workspace } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { CSSProperties } from 'react';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { useEffect, useRef } from 'react';
|
import type { CSSProperties, ReactElement } from 'react';
|
||||||
|
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import type { FallbackProps } from 'react-error-boundary';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
|
||||||
export type EditorProps = {
|
export type EditorProps = {
|
||||||
blockSuiteWorkspace: Workspace;
|
|
||||||
page: Page;
|
page: Page;
|
||||||
mode: 'page' | 'edgeless';
|
mode: 'page' | 'edgeless';
|
||||||
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
|
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
|
||||||
@@ -13,51 +15,45 @@ export type EditorProps = {
|
|||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ErrorBoundaryProps = {
|
||||||
|
onReset?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var currentBlockSuiteWorkspace: Workspace | undefined;
|
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var currentPage: Page | undefined;
|
var currentPage: Page | undefined;
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var currentEditor: EditorContainer | undefined;
|
var currentEditor: EditorContainer | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BlockSuiteEditor = (props: EditorProps) => {
|
const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
|
||||||
const page = props.page;
|
const page = props.page;
|
||||||
|
assertExists(page, 'page should not be null');
|
||||||
const editorRef = useRef<EditorContainer | null>(null);
|
const editorRef = useRef<EditorContainer | null>(null);
|
||||||
const blockHubRef = useRef<BlockHub | null>(null);
|
const blockHubRef = useRef<BlockHub | null>(null);
|
||||||
if (editorRef.current === null) {
|
if (editorRef.current === null) {
|
||||||
editorRef.current = new EditorContainer();
|
editorRef.current = new EditorContainer();
|
||||||
editorRef.current.page = props.page;
|
|
||||||
editorRef.current.mode = props.mode;
|
|
||||||
globalThis.currentEditor = editorRef.current;
|
globalThis.currentEditor = editorRef.current;
|
||||||
}
|
}
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const editor = editorRef.current;
|
||||||
useEffect(() => {
|
assertExists(editorRef, 'editorRef.current should not be null');
|
||||||
if (editorRef.current) {
|
if (editor.mode !== props.mode) {
|
||||||
editorRef.current.mode = props.mode;
|
editor.mode = props.mode;
|
||||||
}
|
}
|
||||||
}, [props.mode]);
|
if (editor.page !== props.page) {
|
||||||
|
editor.page = props.page;
|
||||||
useEffect(() => {
|
|
||||||
const editor = editorRef.current;
|
|
||||||
if (!editor || !ref.current || !page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.page = page;
|
|
||||||
if (page.root === null) {
|
if (page.root === null) {
|
||||||
props.onInit(page, editor);
|
props.onInit(page, editor);
|
||||||
}
|
}
|
||||||
props.onLoad?.(page, editor);
|
props.onLoad?.(page, editor);
|
||||||
return;
|
}
|
||||||
}, [page, props]);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const editor = editorRef.current;
|
const editor = editorRef.current;
|
||||||
|
assertExists(editor);
|
||||||
const container = ref.current;
|
const container = ref.current;
|
||||||
|
if (!container) {
|
||||||
if (!editor || !container || !page) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (page.awarenessStore.getFlag('enable_block_hub')) {
|
if (page.awarenessStore.getFlag('enable_block_hub')) {
|
||||||
@@ -82,13 +78,52 @@ export const BlockSuiteEditor = (props: EditorProps) => {
|
|||||||
blockHubRef.current?.remove();
|
blockHubRef.current?.remove();
|
||||||
container.removeChild(editor);
|
container.removeChild(editor);
|
||||||
};
|
};
|
||||||
}, [page, props.mode]);
|
}, [page]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid={`editor-${props.blockSuiteWorkspace.id}-${props.page.id}`}
|
data-testid={`editor-${props.page.id}`}
|
||||||
className="editor-wrapper"
|
className="editor-wrapper"
|
||||||
style={props.style}
|
style={props.style}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BlockSuiteErrorFallback = (
|
||||||
|
props: FallbackProps & ErrorBoundaryProps
|
||||||
|
): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Sorry.. there was an error</h1>
|
||||||
|
<div>{props.error.message}</div>
|
||||||
|
<button
|
||||||
|
data-testid="error-fallback-reset-button"
|
||||||
|
onClick={() => {
|
||||||
|
props.onReset?.();
|
||||||
|
props.resetErrorBoundary();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlockSuiteEditor = memo(function BlockSuiteEditor(
|
||||||
|
props: EditorProps & ErrorBoundaryProps
|
||||||
|
): ReactElement {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary
|
||||||
|
fallbackRender={useCallback(
|
||||||
|
(fallbackProps: FallbackProps) => (
|
||||||
|
<BlockSuiteErrorFallback {...fallbackProps} onReset={props.onReset} />
|
||||||
|
),
|
||||||
|
[props.onReset]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<BlockSuiteEditorImpl {...props} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
BlockSuiteEditor.displayName = 'BlockSuiteEditor';
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@@ -77,6 +77,7 @@ __metadata:
|
|||||||
react-dnd: ^16.0.1
|
react-dnd: ^16.0.1
|
||||||
react-dnd-html5-backend: ^16.0.1
|
react-dnd-html5-backend: ^16.0.1
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
|
react-error-boundary: ^4.0.3
|
||||||
react-is: ^18.2.0
|
react-is: ^18.2.0
|
||||||
serve: ^14.2.0
|
serve: ^14.2.0
|
||||||
storybook: ^7.0.2
|
storybook: ^7.0.2
|
||||||
@@ -16017,6 +16018,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-error-boundary@npm:^4.0.3":
|
||||||
|
version: 4.0.3
|
||||||
|
resolution: "react-error-boundary@npm:4.0.3"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.12.5
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=16.13.1"
|
||||||
|
checksum: 50813803d3f03eb2ca07c1250a4a001ffe054f5b3f49b15ea5f0a4e1108f549bc7d8def15db51d518516997d34cebc663f4276246301b4a40a72e83a784f5c38
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react-i18next@npm:^12.2.0":
|
"react-i18next@npm:^12.2.0":
|
||||||
version: 12.2.0
|
version: 12.2.0
|
||||||
resolution: "react-i18next@npm:12.2.0"
|
resolution: "react-i18next@npm:12.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user