feat(core): auto select block when jump to block (#4858)

Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
JimmFly
2023-11-10 03:02:56 +00:00
committed by GitHub
co-authored by Peng Xiao
parent f1e32aab66
commit 1fe5a0fffa
10 changed files with 199 additions and 185 deletions
@@ -1,16 +1,10 @@
import type { BlockHub } from '@blocksuite/blocks'; import { rootBlockHubAtom } from '@affine/workspace/atom';
import type { Atom } from 'jotai';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import type { HTMLAttributes, ReactElement } from 'react';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
export interface BlockHubProps extends HTMLAttributes<HTMLDivElement> { export const RootBlockHub = () => {
blockHubAtom: Atom<Readonly<BlockHub> | null>;
}
export const BlockHubWrapper = (props: BlockHubProps): ReactElement => {
const blockHub = useAtomValue(props.blockHubAtom);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const blockHub = useAtomValue(rootBlockHubAtom);
useEffect(() => { useEffect(() => {
if (ref.current) { if (ref.current) {
const div = ref.current; const div = ref.current;
@@ -1,11 +1,19 @@
import type { BlockHub } from '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor'; import { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import { Skeleton } from '@mui/material'; import { Skeleton } from '@mui/material';
import clsx from 'clsx';
import { use } from 'foxact/use'; import { use } from 'foxact/use';
import type { CSSProperties, ReactElement } from 'react'; import type { CSSProperties, ReactElement } from 'react';
import { memo, Suspense, useCallback, useEffect, useRef } from 'react'; import {
memo,
Suspense,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import type { FallbackProps } from 'react-error-boundary'; import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
@@ -15,13 +23,17 @@ import {
} from './index.css'; } from './index.css';
import { getPresets } from './preset'; import { getPresets } from './preset';
interface BlockElement extends Element {
path: string[];
}
export type EditorProps = { export type EditorProps = {
page: Page; page: Page;
mode: 'page' | 'edgeless'; mode: 'page' | 'edgeless';
onInit: (page: Page, editor: Readonly<EditorContainer>) => void; defaultSelectedBlockId?: string;
onModeChange?: (mode: 'page' | 'edgeless') => void; onModeChange?: (mode: 'page' | 'edgeless') => void;
setBlockHub?: (blockHub: BlockHub | null) => void; // on Editor instance instantiated
onLoad?: (page: Page, editor: EditorContainer) => () => void; onLoadEditor?: (editor: EditorContainer) => () => void;
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
}; };
@@ -30,28 +42,62 @@ export type ErrorBoundaryProps = {
onReset?: () => void; onReset?: () => void;
}; };
declare global { // a workaround for returning the webcomponent for the given block id
// eslint-disable-next-line no-var // by iterating over the children of the rendered dom tree
var currentPage: Page | undefined; const useBlockElementById = (
// eslint-disable-next-line no-var container: HTMLElement | null,
var currentEditor: EditorContainer | undefined; blockId: string | undefined,
} timeout = 1000
) => {
const [blockElement, setBlockElement] = useState<BlockElement | null>(null);
useEffect(() => {
if (!blockId) {
return;
}
let canceled = false;
const start = Date.now();
function run() {
if (canceled || !container) {
return;
}
const element = container.querySelector(
`[data-block-id="${blockId}"]`
) as BlockElement | null;
if (element) {
setBlockElement(element);
} else if (Date.now() - start < timeout) {
setTimeout(run, 100);
}
}
run();
return () => {
canceled = true;
};
}, [container, blockId, timeout]);
return blockElement;
};
const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => { const BlockSuiteEditorImpl = ({
const { onLoad, onModeChange, page, mode, style } = props; mode,
page,
className,
defaultSelectedBlockId,
onLoadEditor,
onModeChange,
style,
}: EditorProps): ReactElement => {
if (!page.loaded) { if (!page.loaded) {
use(page.waitForLoaded()); use(page.waitForLoaded());
} }
assertExists(page, 'page should not be null'); 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);
if (editorRef.current === null) { if (editorRef.current === null) {
editorRef.current = new EditorContainer(); editorRef.current = new EditorContainer();
editorRef.current.autofocus = true; editorRef.current.autofocus = true;
globalThis.currentEditor = editorRef.current;
} }
const editor = editorRef.current; const editor = editorRef.current;
assertExists(editorRef, 'editorRef.current should not be null'); assertExists(editorRef, 'editorRef.current should not be null');
if (editor.mode !== mode) { if (editor.mode !== mode) {
editor.mode = mode; editor.mode = mode;
} }
@@ -64,36 +110,29 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
editor.pagePreset = presets.pageModePreset; editor.pagePreset = presets.pageModePreset;
editor.edgelessPreset = presets.edgelessModePreset; editor.edgelessPreset = presets.edgelessModePreset;
useEffect(() => { useLayoutEffect(() => {
const disposes = [] as ((() => void) | undefined)[];
if (editor) { if (editor) {
const dispose = editor.slots.pageModeSwitched.on(mode => { const disposes: (() => void)[] = [];
const disposeModeSwitch = editor.slots.pageModeSwitched.on(mode => {
onModeChange?.(mode); onModeChange?.(mode);
}); });
disposes.push(() => disposeModeSwitch?.dispose());
disposes.push(() => dispose?.dispose()); if (onLoadEditor) {
disposes.push(onLoadEditor(editor));
if (editor.page && onLoad) {
disposes.push(onLoad?.(page, editor));
} }
return () => {
disposes.forEach(dispose => dispose());
};
} }
return;
}, [editor, onModeChange, onLoadEditor]);
return () => { const containerRef = useRef<HTMLDivElement>(null);
disposes
.filter((dispose): dispose is () => void => !!dispose)
.forEach(dispose => dispose());
};
}, [editor, editor.page, page, onLoad, onModeChange]);
const ref = useRef<HTMLDivElement>(null);
const setBlockHub = props.setBlockHub;
useEffect(() => { useEffect(() => {
const editor = editorRef.current; const editor = editorRef.current;
assertExists(editor); assertExists(editor);
const container = ref.current; const container = containerRef.current;
if (!container) { if (!container) {
return; return;
} }
@@ -103,42 +142,38 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
}; };
}, [editor]); }, [editor]);
const blockElement = useBlockElementById(
containerRef.current,
defaultSelectedBlockId
);
useEffect(() => { useEffect(() => {
if (page.meta.trash) { if (blockElement) {
return; requestIdleCallback(() => {
} blockElement.scrollIntoView({
editor behavior: 'smooth',
.createBlockHub() block: 'center',
.then(blockHub => { inline: 'center',
if (blockHubRef.current) { });
blockHubRef.current.remove(); const selectManager = editor.root.value?.selection;
if (!blockElement.path.length || !selectManager) {
return;
} }
blockHubRef.current = blockHub; const newSelection = selectManager.getInstance('block', {
if (setBlockHub) { path: blockElement.path,
setBlockHub(blockHub); });
} selectManager.set([newSelection]);
})
.catch(err => {
console.error(err);
}); });
return () => { }
if (setBlockHub) { }, [editor, blockElement]);
setBlockHub(null);
}
blockHubRef.current?.remove();
};
}, [editor, page.awarenessStore, page.meta.trash, setBlockHub]);
// issue: https://github.com/toeverything/AFFiNE/issues/2004 // issue: https://github.com/toeverything/AFFiNE/issues/2004
const className = `editor-wrapper ${editor.mode}-mode ${
props.className || ''
}`;
return ( return (
<div <div
data-testid={`editor-${page.id}`} data-testid={`editor-${page.id}`}
className={className} className={clsx(`editor-wrapper ${editor.mode}-mode`, className)}
style={style} style={style}
ref={ref} ref={containerRef}
/> />
); );
}; };
@@ -3,7 +3,6 @@ import type {
WorkspaceFlavour, WorkspaceFlavour,
WorkspaceUISchema, WorkspaceUISchema,
} from '@affine/env/workspace'; } from '@affine/env/workspace';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { lazy, useCallback } from 'react'; import { lazy, useCallback } from 'react';
import type { OnLoadEditor } from '../../components/page-detail-editor'; import type { OnLoadEditor } from '../../components/page-detail-editor';
@@ -50,7 +49,6 @@ export const UI = {
return ( return (
<PageDetailEditor <PageDetailEditor
pageId={currentPageId} pageId={currentPageId}
onInit={useCallback(async page => initEmptyPage(page), [])}
onLoad={onLoad} onLoad={onLoad}
workspace={workspace.blockSuiteWorkspace} workspace={workspace.blockSuiteWorkspace}
/> />
@@ -25,7 +25,6 @@ import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite'; import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { useCallback } from 'react';
import { setPageModeAtom } from '../../atoms'; import { setPageModeAtom } from '../../atoms';
import { import {
@@ -94,7 +93,6 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
return ( return (
<PageDetailEditor <PageDetailEditor
pageId={currentPageId} pageId={currentPageId}
onInit={useCallback(async page => initEmptyPage(page), [])}
onLoad={onLoadEditor} onLoad={onLoadEditor}
workspace={workspace} workspace={workspace}
/> />
@@ -1,8 +1,6 @@
import { PageNotFoundError } from '@affine/env/constant'; import { PageNotFoundError } from '@affine/env/constant';
import type { WorkspaceFlavour } from '@affine/env/workspace'; import type { WorkspaceFlavour } from '@affine/env/workspace';
import { type WorkspaceUISchema } from '@affine/env/workspace'; import { type WorkspaceUISchema } from '@affine/env/workspace';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { useCallback } from 'react';
import { useWorkspace } from '../../hooks/use-workspace'; import { useWorkspace } from '../../hooks/use-workspace';
import { PageDetailEditor, Provider } from '../shared'; import { PageDetailEditor, Provider } from '../shared';
@@ -18,7 +16,6 @@ export const UI = {
return ( return (
<PageDetailEditor <PageDetailEditor
pageId={currentPageId} pageId={currentPageId}
onInit={useCallback(async page => initEmptyPage(page), [])}
onLoad={onLoadEditor} onLoad={onLoadEditor}
workspace={workspace.blockSuiteWorkspace} workspace={workspace.blockSuiteWorkspace}
/> />
@@ -1,8 +1,9 @@
import './page-detail-editor.css'; import './page-detail-editor.css';
import { PageNotFoundError } from '@affine/env/constant'; import { PageNotFoundError } from '@affine/env/constant';
import type { LayoutNode } from '@affine/sdk//entry'; import type { LayoutNode } from '@affine/sdk/entry';
import { rootBlockHubAtom } from '@affine/workspace/atom'; import { rootBlockHubAtom } from '@affine/workspace/atom';
import type { BlockHub } from '@blocksuite/blocks';
import type { EditorContainer } from '@blocksuite/editor'; import type { EditorContainer } from '@blocksuite/editor';
import { assertExists, DisposableGroup } from '@blocksuite/global/utils'; import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
import type { Page, Workspace } from '@blocksuite/store'; import type { Page, Workspace } from '@blocksuite/store';
@@ -40,8 +41,9 @@ import * as styles from './page-detail-editor.css';
import { editorContainer, pluginContainer } from './page-detail-editor.css'; import { editorContainer, pluginContainer } from './page-detail-editor.css';
import { TrashButtonGroup } from './pure/trash-button-group'; import { TrashButtonGroup } from './pure/trash-button-group';
function useRouterHash() { declare global {
return useLocation().hash.substring(1); // eslint-disable-next-line no-var
var currentEditor: EditorContainer | undefined;
} }
export type OnLoadEditor = (page: Page, editor: EditorContainer) => () => void; export type OnLoadEditor = (page: Page, editor: EditorContainer) => () => void;
@@ -50,17 +52,43 @@ export interface PageDetailEditorProps {
isPublic?: boolean; isPublic?: boolean;
workspace: Workspace; workspace: Workspace;
pageId: string; pageId: string;
onInit: (
page: Page,
editor: Readonly<EditorContainer>
) => Promise<void> | void;
onLoad?: OnLoadEditor; onLoad?: OnLoadEditor;
} }
function useRouterHash() {
return useLocation().hash.substring(1);
}
function useCreateAndSetRootBlockHub(
editor?: EditorContainer,
showBlockHub?: boolean
) {
const setBlockHub = useSetAtom(rootBlockHubAtom);
useEffect(() => {
let canceled = false;
let blockHub: BlockHub | undefined;
if (editor && showBlockHub) {
editor
.createBlockHub()
.then(bh => {
if (canceled) {
return;
}
blockHub = bh;
setBlockHub(blockHub);
})
.catch(console.error);
}
return () => {
canceled = true;
blockHub?.remove();
};
}, [editor, showBlockHub, setBlockHub]);
}
const EditorWrapper = memo(function EditorWrapper({ const EditorWrapper = memo(function EditorWrapper({
workspace, workspace,
pageId, pageId,
onInit,
onLoad, onLoad,
isPublic, isPublic,
}: PageDetailEditorProps) { }: PageDetailEditorProps) {
@@ -79,7 +107,6 @@ const EditorWrapper = memo(function EditorWrapper({
const pageSetting = useAtomValue(pageSettingAtom); const pageSetting = useAtomValue(pageSettingAtom);
const currentMode = pageSetting?.mode ?? 'page'; const currentMode = pageSetting?.mode ?? 'page';
const setBlockHub = useSetAtom(rootBlockHubAtom);
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();
assertExists(meta); assertExists(meta);
@@ -91,29 +118,6 @@ const EditorWrapper = memo(function EditorWrapper({
return fontStyle.value; return fontStyle.value;
}, [appSettings.fontStyle]); }, [appSettings.fontStyle]);
const [loading, setLoading] = useState(true);
const blockId = useRouterHash();
const blockElement = useMemo(() => {
if (!blockId || loading) {
return null;
}
return document.querySelector(`[data-block-id="${blockId}"]`);
}, [blockId, loading]);
useEffect(() => {
if (blockElement) {
setTimeout(
() =>
blockElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
}),
0
);
}
}, [blockElement]);
const setEditorMode = useCallback( const setEditorMode = useCallback(
(mode: 'page' | 'edgeless') => { (mode: 'page' | 'edgeless') => {
if (mode === 'edgeless') { if (mode === 'edgeless') {
@@ -125,6 +129,56 @@ const EditorWrapper = memo(function EditorWrapper({
[switchToEdgelessMode, switchToPageMode, pageId] [switchToEdgelessMode, switchToPageMode, pageId]
); );
const [editor, setEditor] = useState<EditorContainer>();
const blockId = useRouterHash();
useCreateAndSetRootBlockHub(editor, !meta.trash);
const onLoadEditor = useCallback(
(editor: EditorContainer) => {
// debug current detail editor
globalThis.currentEditor = editor;
setEditor(editor);
const disposableGroup = new DisposableGroup();
disposableGroup.add(
page.slots.blockUpdated.once(() => {
page.workspace.setPageMeta(page.id, {
updatedDate: Date.now(),
});
})
);
localStorage.setItem('last_page_id', page.id);
if (onLoad) {
disposableGroup.add(onLoad(page, editor));
}
const rootStore = getCurrentStore();
const editorItems = rootStore.get(pluginEditorAtom);
let disposes: (() => void)[] = [];
const renderTimeout = window.setTimeout(() => {
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
const div = document.createElement('div');
div.setAttribute('plugin-id', id);
const cleanup = editorItem(div, editor);
assertExists(parent);
document.body.appendChild(div);
return () => {
cleanup();
document.body.removeChild(div);
};
});
});
return () => {
disposableGroup.dispose();
clearTimeout(renderTimeout);
window.setTimeout(() => {
disposes.forEach(dispose => dispose());
});
};
},
[onLoad, page]
);
return ( return (
<> <>
<Editor <Editor
@@ -140,55 +194,8 @@ const EditorWrapper = memo(function EditorWrapper({
mode={isPublic ? 'page' : currentMode} mode={isPublic ? 'page' : currentMode}
page={page} page={page}
onModeChange={setEditorMode} onModeChange={setEditorMode}
onInit={useCallback( defaultSelectedBlockId={blockId}
(page: Page, editor: Readonly<EditorContainer>) => { onLoadEditor={onLoadEditor}
onInit(page, editor);
},
[onInit]
)}
setBlockHub={setBlockHub}
onLoad={useCallback(
(page: Page, editor: EditorContainer) => {
const disposableGroup = new DisposableGroup();
disposableGroup.add(
page.slots.blockUpdated.once(() => {
page.workspace.setPageMeta(page.id, {
updatedDate: Date.now(),
});
})
);
localStorage.setItem('last_page_id', page.id);
if (onLoad) {
disposableGroup.add(onLoad(page, editor));
}
const rootStore = getCurrentStore();
const editorItems = rootStore.get(pluginEditorAtom);
let disposes: (() => void)[] = [];
const renderTimeout = window.setTimeout(() => {
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
const div = document.createElement('div');
div.setAttribute('plugin-id', id);
const cleanup = editorItem(div, editor);
assertExists(parent);
document.body.appendChild(div);
return () => {
cleanup();
document.body.removeChild(div);
};
});
});
return () => {
disposableGroup.dispose();
clearTimeout(renderTimeout);
window.setTimeout(() => {
disposes.forEach(dispose => dispose());
});
setLoading(false);
};
},
[onLoad]
)}
/> />
{meta.trash && <TrashButtonGroup />} {meta.trash && <TrashButtonGroup />}
<Bookmark page={page} /> <Bookmark page={page} />
@@ -2,7 +2,7 @@ import {
AppSidebarFallback, AppSidebarFallback,
appSidebarResizingAtom, appSidebarResizingAtom,
} from '@affine/component/app-sidebar'; } from '@affine/component/app-sidebar';
import { BlockHubWrapper } from '@affine/component/block-hub'; import { RootBlockHub } from '@affine/component/block-hub';
import { import {
type DraggableTitleCellData, type DraggableTitleCellData,
PageListDragOverlay, PageListDragOverlay,
@@ -13,10 +13,7 @@ import {
WorkspaceFallback, WorkspaceFallback,
} from '@affine/component/workspace'; } from '@affine/component/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
rootBlockHubAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store'; import type { Page } from '@blocksuite/store';
import type { DragEndEvent } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core';
@@ -294,7 +291,7 @@ export const WorkspaceLayoutInner = ({
> >
{incompatible ? <WorkspaceUpgrade /> : children} {incompatible ? <WorkspaceUpgrade /> : children}
<ToolContainer inTrashPage={inTrashPage}> <ToolContainer inTrashPage={inTrashPage}>
<BlockHubWrapper blockHubAtom={rootBlockHubAtom} /> <RootBlockHub />
<HelpIsland showList={pageId ? undefined : showList} /> <HelpIsland showList={pageId ? undefined : showList} />
</ToolContainer> </ToolContainer>
</MainContainer> </MainContainer>
@@ -70,7 +70,6 @@ export const Component = (): ReactElement => {
isPublic isPublic
workspace={page.workspace} workspace={page.workspace}
pageId={page.id} pageId={page.id}
onInit={noop}
onLoad={useCallback(() => noop, [])} onLoad={useCallback(() => noop, [])}
/> />
</MainContainer> </MainContainer>
+5 -3
View File
@@ -372,7 +372,7 @@ test('show not found item', async ({ page }) => {
await expect(notFoundItem).toHaveText('Search for "test123456"'); await expect(notFoundItem).toHaveText('Search for "test123456"');
}); });
test('can use cmdk to search page content and scroll to it', async ({ test('can use cmdk to search page content and scroll to it, then the block will be selected', async ({
page, page,
}) => { }) => {
await openHomePage(page); await openHomePage(page);
@@ -382,8 +382,8 @@ test('can use cmdk to search page content and scroll to it', async ({
await getBlockSuiteEditorTitle(page).fill( await getBlockSuiteEditorTitle(page).fill(
'this is a new page to search for content' 'this is a new page to search for content'
); );
for (let i = 0; i < 50; i++) { for (let i = 0; i < 30; i++) {
await page.keyboard.press('Enter'); await page.keyboard.press('Enter', { delay: 10 });
} }
await page.keyboard.insertText('123456'); await page.keyboard.insertText('123456');
await clickSideBarAllPageButton(page); await clickSideBarAllPageButton(page);
@@ -398,4 +398,6 @@ test('can use cmdk to search page content and scroll to it', async ({
await waitForScrollToFinish(page); await waitForScrollToFinish(page);
const isVisitable = await checkElementIsInView(page, '123456'); const isVisitable = await checkElementIsInView(page, '123456');
expect(isVisitable).toBe(true); expect(isVisitable).toBe(true);
const selectionElement = page.locator('affine-block-selection');
await expect(selectionElement).toBeVisible();
}); });
@@ -1,13 +1,11 @@
import { BlockHubWrapper } from '@affine/component/block-hub'; import { RootBlockHub } from '@affine/component/block-hub';
import { BlockSuiteEditor } from '@affine/component/block-suite-editor'; import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { ImagePreviewModal } from '@affine/image-preview-plugin/src/component'; import { ImagePreviewModal } from '@affine/image-preview-plugin/src/component';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import { getOrCreateWorkspace } from '@affine/workspace/manager'; import { getOrCreateWorkspace } from '@affine/workspace/manager';
import type { Meta } from '@storybook/react'; import type { Meta } from '@storybook/react';
import { initEmptyPage } from '@toeverything/infra/blocksuite'; import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { useCallback } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
export default { export default {
@@ -54,24 +52,13 @@ export const Default = () => {
overflow: 'auto', overflow: 'auto',
}} }}
> >
<BlockSuiteEditor <BlockSuiteEditor mode="page" page={page} />
mode="page"
page={page}
onInit={useCallback(async page => initEmptyPage(page), [])}
/>
{createPortal( {createPortal(
<ImagePreviewModal pageId={page.id} workspace={page.workspace} />, <ImagePreviewModal pageId={page.id} workspace={page.workspace} />,
document.body document.body
)} )}
</div> </div>
<BlockHubWrapper <RootBlockHub />
style={{
position: 'absolute',
right: 12,
bottom: 12,
}}
blockHubAtom={rootBlockHubAtom}
/>
</> </>
); );
}; };