refactor(core): separate editor & doc mode (#7873)

doc.mode -> primaryMode
(*new) editor.mode

New Service:
editor service

Change Mode:

```
const editor = useService(EditorService).editor;
editor.setMode('page')
```

Change primary mode

```
const editor = useService(EditorService).editor;
editor.doc.setPrimaryMode('page')
```
This commit is contained in:
EYHN
2024-08-14 11:43:03 +00:00
parent 50948318e0
commit 89537e6892
51 changed files with 453 additions and 379 deletions
@@ -0,0 +1,31 @@
import type { DocMode } from '@blocksuite/blocks';
import type { DocService, WorkspaceService } from '@toeverything/infra';
import { Entity, LiveData } from '@toeverything/infra';
import { EditorScope } from '../scopes/editor';
export class Editor extends Entity<{ defaultMode: DocMode }> {
readonly scope = this.framework.createScope(EditorScope, {
editor: this as Editor,
});
readonly mode$ = new LiveData(this.props.defaultMode);
readonly doc = this.docService.doc;
readonly isSharedMode =
this.workspaceService.workspace.openOptions.isSharedMode;
toggleMode() {
this.mode$.next(this.mode$.value === 'edgeless' ? 'page' : 'edgeless');
}
setMode(mode: DocMode) {
this.mode$.next(mode);
}
constructor(
private readonly docService: DocService,
private readonly workspaceService: WorkspaceService
) {
super();
}
}
@@ -0,0 +1,27 @@
import {
DocScope,
DocService,
type Framework,
WorkspaceScope,
WorkspaceService,
} from '@toeverything/infra';
import { Editor } from './entities/editor';
import { EditorScope } from './scopes/editor';
import { EditorService } from './services/editor';
import { EditorsService } from './services/editors';
export { Editor } from './entities/editor';
export { EditorScope } from './scopes/editor';
export { EditorService } from './services/editor';
export { EditorsService } from './services/editors';
export function configureEditorModule(framework: Framework) {
framework
.scope(WorkspaceScope)
.scope(DocScope)
.service(EditorsService)
.entity(Editor, [DocService, WorkspaceService])
.scope(EditorScope)
.service(EditorService, [EditorScope]);
}
@@ -0,0 +1,7 @@
import { Scope } from '@toeverything/infra';
import type { Editor } from '../entities/editor';
export class EditorScope extends Scope<{
editor: Editor;
}> {}
@@ -0,0 +1,11 @@
import { Service } from '@toeverything/infra';
import type { EditorScope } from '../scopes/editor';
export class EditorService extends Service {
readonly editor = this.scope.props.editor;
constructor(readonly scope: EditorScope) {
super();
}
}
@@ -0,0 +1,10 @@
import type { DocMode } from '@blocksuite/blocks';
import { Service } from '@toeverything/infra';
import { Editor } from '../entities/editor';
export class EditorsService extends Service {
createEditor(defaultMode: DocMode) {
return this.framework.createEntity(Editor, { defaultMode });
}
}
@@ -56,25 +56,25 @@ export const ExplorerDocNode = ({
const [collapsed, setCollapsed] = useState(true);
const docRecord = useLiveData(docsService.list.doc$(docId));
const docMode = useLiveData(docRecord?.mode$);
const docPrimaryMode = useLiveData(docRecord?.primaryMode$);
const docTitle = useLiveData(docRecord?.title$);
const isInTrash = useLiveData(docRecord?.trash$);
const Icon = useCallback(
({ className }: { className?: string }) => {
return isLinked ? (
docMode === 'edgeless' ? (
docPrimaryMode === 'edgeless' ? (
<LinkedEdgelessIcon className={className} />
) : (
<LinkedPageIcon className={className} />
)
) : docMode === 'edgeless' ? (
) : docPrimaryMode === 'edgeless' ? (
<EdgelessIcon className={className} />
) : (
<PageIcon className={className} />
);
},
[docMode, isLinked]
[docPrimaryMode, isLinked]
);
const children = useLiveData(
@@ -5,6 +5,7 @@ import { configureCloudModule } from './cloud';
import { configureCollectionModule } from './collection';
import { configureDocLinksModule } from './doc-link';
import { configureDocsSearchModule } from './docs-search';
import { configureEditorModule } from './editor';
import { configureExplorerModule } from './explorer';
import { configureFavoriteModule } from './favorite';
import { configureFindInPageModule } from './find-in-page';
@@ -39,4 +40,5 @@ export function configureCommonModules(framework: Framework) {
configureFavoriteModule(framework);
configureExplorerModule(framework);
configureThemeEditorModule(framework);
configureEditorModule(framework);
}
@@ -17,7 +17,7 @@ import { useCallback, useEffect, useState } from 'react';
import { WorkbenchService } from '../../../workbench';
import { PeekViewService } from '../../services/peek-view';
import { useDoc } from '../utils';
import { useEditor } from '../utils';
import * as styles from './doc-peek-view.css';
const logger = new DebugLogger('doc-peek-view');
@@ -69,33 +69,36 @@ export function DocPeekPreview({
mode?: DocMode;
xywh?: `[${number},${number},${number},${number}]`;
}) {
const { doc, workspace, loading } = useDoc(docId);
const { doc, workspace, loading } = useEditor(docId, mode);
const { jumpToTag } = useNavigateHelper();
const workbench = useService(WorkbenchService).workbench;
const peekView = useService(PeekViewService).peekView;
const [editor, setEditor] = useState<AffineEditorContainer | null>(null);
const [editorElement, setEditorElement] =
useState<AffineEditorContainer | null>(null);
const onRef = (editor: AffineEditorContainer) => {
setEditor(editor);
setEditorElement(editor);
};
const docs = useService(DocsService);
const [resolvedMode, setResolvedMode] = useState<DocMode | undefined>(mode);
useEffect(() => {
editor?.updateComplete
editorElement?.updateComplete
.then(() => {
if (resolvedMode === 'edgeless') {
fitViewport(editor, xywh);
fitViewport(editorElement, xywh);
}
})
.catch(console.error);
return;
}, [editor, resolvedMode, xywh]);
}, [editorElement, resolvedMode, xywh]);
useEffect(() => {
if (!mode || !resolvedMode) {
setResolvedMode(docs.list.doc$(docId).value?.mode$.value || 'page');
setResolvedMode(
docs.list.doc$(docId).value?.primaryMode$.value || 'page'
);
}
}, [docId, docs.list, resolvedMode, mode]);
@@ -115,14 +118,15 @@ export function DocPeekPreview({
useEffect(() => {
const disposableGroup = new DisposableGroup();
if (editor) {
editor.updateComplete
if (editorElement) {
editorElement.updateComplete
.then(() => {
if (!editor.host) {
if (!editorElement.host) {
return;
}
const rootService = editor.host.std.spec.getService('affine:page');
const rootService =
editorElement.host.std.spec.getService('affine:page');
// doc change event inside peek view should be handled by peek view
disposableGroup.add(
rootService.slots.docLinkClicked.on(({ docId, blockId }) => {
@@ -142,7 +146,7 @@ export function DocPeekPreview({
return () => {
disposableGroup.dispose();
};
}, [editor, jumpToTag, peekView, workspace.id]);
}, [editorElement, jumpToTag, peekView, workspace.id]);
const openOutlinePanel = useCallback(() => {
workbench.openDoc(docId);
@@ -176,7 +180,7 @@ export function DocPeekPreview({
/>
</FrameworkScope>
<EditorOutlineViewer
editor={editor}
editor={editorElement}
show={resolvedMode === 'page'}
openOutlinePanel={openOutlinePanel}
/>
@@ -33,7 +33,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import useSWR from 'swr';
import { PeekViewService } from '../../services/peek-view';
import { useDoc } from '../utils';
import { useEditor } from '../utils';
import { useZoomControls } from './hooks/use-zoom';
import * as styles from './index.css';
@@ -108,7 +108,7 @@ const ImagePreviewModalImpl = ({
onBlockIdChange: (blockId: string) => void;
onClose: () => void;
}): ReactElement | null => {
const { doc, workspace } = useDoc(docId);
const { doc, workspace } = useEditor(docId);
const blocksuiteDoc = doc?.blockSuiteDoc;
const docCollection = workspace.docCollection;
const blockModel = useMemo(() => {
@@ -20,7 +20,7 @@ import {
import { WorkbenchService } from '../../workbench';
import { PeekViewService } from '../services/peek-view';
import * as styles from './peek-view-controls.css';
import { useDoc } from './utils';
import { useEditor } from './utils';
type ControlButtonProps = {
nameKey: string;
@@ -100,7 +100,7 @@ export const DocPeekViewControls = ({
const workbench = useService(WorkbenchService).workbench;
const { jumpToPageBlock } = useNavigateHelper();
const t = useI18n();
const { doc, workspace } = useDoc(docId);
const { doc, workspace } = useEditor(docId);
const controls = useMemo(() => {
return [
{
@@ -115,12 +115,15 @@ export const DocPeekViewControls = ({
nameKey: 'open',
onClick: () => {
// TODO(@Peng): for frame blocks, we should mimic "view in edgeless" button behavior
if (mode) {
// TODO(@eyhn): change this to use mode link
doc?.setPrimaryMode(mode);
}
blockId
? jumpToPageBlock(workspace.id, docId, blockId)
: workbench.openDoc(docId);
if (mode) {
doc?.setMode(mode);
}
peekView.close('none');
},
},
@@ -1,20 +1,24 @@
import type { Doc } from '@toeverything/infra';
import type { Doc, DocMode } from '@toeverything/infra';
import {
DocsService,
useLiveData,
useService,
WorkspaceService,
} from '@toeverything/infra';
import { useEffect, useLayoutEffect, useState } from 'react';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
export const useDoc = (pageId: string) => {
import { type Editor, EditorsService } from '../../editor';
export const useEditor = (pageId: string, preferMode?: DocMode) => {
const currentWorkspace = useService(WorkspaceService).workspace;
const docsService = useService(DocsService);
const docRecordList = docsService.list;
const docListReady = useLiveData(docRecordList.isReady$);
const docRecord = docRecordList.doc$(pageId).value;
const preferModeRef = useRef(preferMode);
const [doc, setDoc] = useState<Doc | null>(null);
const [editor, setEditor] = useState<Editor | null>(null);
useLayoutEffect(() => {
if (!docRecord) {
@@ -27,6 +31,19 @@ export const useDoc = (pageId: string) => {
};
}, [docRecord, docsService, pageId]);
useLayoutEffect(() => {
if (!doc) {
return;
}
const editor = doc.scope
.get(EditorsService)
.createEditor(preferModeRef.current || doc.primaryMode$.value);
setEditor(editor);
return () => {
editor.dispose();
};
}, [doc]);
// set sync engine priority target
useEffect(() => {
currentWorkspace.engine.doc.setPriority(pageId, 10);
@@ -35,5 +52,5 @@ export const useDoc = (pageId: string) => {
};
}, [currentWorkspace, pageId]);
return { doc, workspace: currentWorkspace, loading: !docListReady };
return { doc, editor, workspace: currentWorkspace, loading: !docListReady };
};
@@ -63,13 +63,13 @@ export class CMDKQuickSearchService extends Service {
} else if (result.source === 'creation') {
if (result.id === 'creation:create-page') {
const newDoc = this.docsService.createDoc({
mode: 'page',
primaryMode: 'page',
title: result.payload.title,
});
this.workbenchService.workbench.openDoc(newDoc.id);
} else if (result.id === 'creation:create-edgeless') {
const newDoc = this.docsService.createDoc({
mode: 'edgeless',
primaryMode: 'edgeless',
title: result.payload.title,
});
this.workbenchService.workbench.openDoc(newDoc.id);
@@ -16,7 +16,7 @@ export class DocDisplayMetaService extends Service {
);
const icon = journalDateString
? TodayIcon
: docRecord.mode$.value === 'edgeless'
: docRecord.primaryMode$.value === 'edgeless'
? EdgelessIcon
: PageIcon;