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

View File

@@ -1,3 +1,5 @@
import type { RootBlockModel } from '@blocksuite/blocks';
import { Entity } from '../../../framework'; import { Entity } from '../../../framework';
import type { DocScope } from '../scopes/doc'; import type { DocScope } from '../scopes/doc';
import type { DocsStore } from '../stores/docs'; import type { DocsStore } from '../stores/docs';
@@ -19,24 +21,22 @@ export class Doc extends Entity {
public readonly record = this.scope.props.record; public readonly record = this.scope.props.record;
readonly meta$ = this.record.meta$; readonly meta$ = this.record.meta$;
readonly mode$ = this.record.mode$; readonly primaryMode$ = this.record.primaryMode$;
readonly title$ = this.record.title$; readonly title$ = this.record.title$;
readonly trash$ = this.record.trash$; readonly trash$ = this.record.trash$;
setMode(mode: DocMode) { setPrimaryMode(mode: DocMode) {
return this.record.setMode(mode); return this.record.setPrimaryMode(mode);
} }
getMode() { getPrimaryMode() {
return this.record.getMode(); return this.record.getPrimaryMode();
} }
toggleMode() { togglePrimaryMode() {
return this.record.toggleMode(); this.setPrimaryMode(
} this.getPrimaryMode() === 'edgeless' ? 'page' : 'edgeless'
);
observeMode() {
return this.record.observeMode();
} }
moveToTrash() { moveToTrash() {
@@ -54,4 +54,16 @@ export class Doc extends Entity {
setPriorityLoad(priority: number) { setPriorityLoad(priority: number) {
return this.store.setPriorityLoad(this.id, priority); return this.store.setPriorityLoad(this.id, priority);
} }
changeDocTitle(newTitle: string) {
const pageBlock = this.blockSuiteDoc.getBlocksByFlavour('affine:page').at(0)
?.model as RootBlockModel | undefined;
if (pageBlock) {
this.blockSuiteDoc.transact(() => {
pageBlock.title.delete(0, pageBlock.title.length);
pageBlock.title.insert(newTitle, 0);
});
this.record.setMeta({ title: newTitle });
}
}
} }

View File

@@ -55,21 +55,24 @@ export class DocRecordList extends Entity {
return this.docs$.map(record => record.find(record => record.id === id)); return this.docs$.map(record => record.find(record => record.id === id));
} }
public setMode(id: string, mode: DocMode) { public setPrimaryMode(id: string, mode: DocMode) {
return this.store.setDocModeSetting(id, mode); return this.store.setDocPrimaryModeSetting(id, mode);
} }
public getMode(id: string) { public getPrimaryMode(id: string) {
return this.store.getDocModeSetting(id); return this.store.getDocPrimaryModeSetting(id);
} }
public toggleMode(id: string) { public togglePrimaryMode(id: string) {
const mode = this.getMode(id) === 'edgeless' ? 'page' : 'edgeless'; const mode = this.getPrimaryMode(id) === 'edgeless' ? 'page' : 'edgeless';
this.setMode(id, mode); this.setPrimaryMode(id, mode);
return this.getMode(id); return this.getPrimaryMode(id);
} }
public observeMode(id: string) { public primaryMode$(id: string) {
return this.store.watchDocModeSetting(id); return LiveData.from(
this.store.watchDocPrimaryModeSetting(id),
this.getPrimaryMode(id)
);
} }
} }

View File

@@ -26,27 +26,17 @@ export class DocRecord extends Entity<{ id: string }> {
this.docsStore.setDocMeta(this.id, meta); this.docsStore.setDocMeta(this.id, meta);
} }
mode$: LiveData<DocMode> = LiveData.from( primaryMode$: LiveData<DocMode> = LiveData.from(
this.docsStore.watchDocModeSetting(this.id), this.docsStore.watchDocPrimaryModeSetting(this.id),
'page' 'page'
).map(mode => (mode === 'edgeless' ? 'edgeless' : 'page')); ).map(mode => (mode === 'edgeless' ? 'edgeless' : 'page'));
setMode(mode: DocMode) { setPrimaryMode(mode: DocMode) {
return this.docsStore.setDocModeSetting(this.id, mode); return this.docsStore.setDocPrimaryModeSetting(this.id, mode);
} }
getMode() { getPrimaryMode() {
return this.docsStore.getDocModeSetting(this.id); return this.docsStore.getDocPrimaryModeSetting(this.id);
}
toggleMode() {
const mode = this.getMode() === 'edgeless' ? 'page' : 'edgeless';
this.setMode(mode);
return this.getMode();
}
observeMode() {
return this.docsStore.watchDocModeSetting(this.id);
} }
moveToTrash() { moveToTrash() {

View File

@@ -1,5 +1,4 @@
import { Unreachable } from '@affine/env/constant'; import { Unreachable } from '@affine/env/constant';
import type { RootBlockModel } from '@blocksuite/blocks';
import { Service } from '../../../framework'; import { Service } from '../../../framework';
import { initEmptyPage } from '../../../initialization'; import { initEmptyPage } from '../../../initialization';
@@ -54,7 +53,7 @@ export class DocsService extends Service {
createDoc( createDoc(
options: { options: {
mode?: DocMode; primaryMode?: DocMode;
title?: string; title?: string;
} = {} } = {}
) { ) {
@@ -65,8 +64,8 @@ export class DocsService extends Service {
if (!docRecord) { if (!docRecord) {
throw new Unreachable(); throw new Unreachable();
} }
if (options.mode) { if (options.primaryMode) {
docRecord.setMode(options.mode); docRecord.setPrimaryMode(options.primaryMode);
} }
return docRecord; return docRecord;
} }
@@ -100,15 +99,7 @@ export class DocsService extends Service {
const { doc, release } = this.open(docId); const { doc, release } = this.open(docId);
doc.setPriorityLoad(10); doc.setPriorityLoad(10);
await doc.waitForSyncReady(); await doc.waitForSyncReady();
const pageBlock = doc.blockSuiteDoc.getBlocksByFlavour('affine:page').at(0) doc.changeDocTitle(newTitle);
?.model as RootBlockModel | undefined;
if (pageBlock) {
doc.blockSuiteDoc.transact(() => {
pageBlock.title.delete(0, pageBlock.title.length);
pageBlock.title.insert(newTitle, 0);
});
doc.record.setMeta({ title: newTitle });
}
release(); release();
} }
} }

View File

@@ -101,15 +101,15 @@ export class DocsStore extends Store {
this.workspaceService.workspace.docCollection.setDocMeta(id, meta); this.workspaceService.workspace.docCollection.setDocMeta(id, meta);
} }
setDocModeSetting(id: string, mode: DocMode) { setDocPrimaryModeSetting(id: string, mode: DocMode) {
return this.localState.set(`page:${id}:mode`, mode); return this.localState.set(`page:${id}:mode`, mode);
} }
getDocModeSetting(id: string) { getDocPrimaryModeSetting(id: string) {
return this.localState.get<DocMode>(`page:${id}:mode`); return this.localState.get<DocMode>(`page:${id}:mode`);
} }
watchDocModeSetting(id: string) { watchDocPrimaryModeSetting(id: string) {
return this.localState.watch<DocMode>(`page:${id}:mode`); return this.localState.watch<DocMode>(`page:${id}:mode`);
} }

View File

@@ -6,7 +6,7 @@ type SimpleRadioItem = string;
export interface RadioProps extends RadioGroupItemProps { export interface RadioProps extends RadioGroupItemProps {
items: RadioItem[] | SimpleRadioItem[]; items: RadioItem[] | SimpleRadioItem[];
value: any; value: any;
onChange: (value: any) => void; onChange?: (value: any) => void;
/** /**
* Total width of the radio group, items will be evenly distributed * Total width of the radio group, items will be evenly distributed

View File

@@ -31,7 +31,7 @@ export async function buildShowcaseWorkspace(
); );
if (defaultDoc) { if (defaultDoc) {
defaultDoc.setMode('edgeless'); defaultDoc.setPrimaryMode('edgeless');
} }
dispose(); dispose();

View File

@@ -2,14 +2,10 @@ import { Button, FlexWrapper, notify } from '@affine/component';
import { openSettingModalAtom } from '@affine/core/atoms'; import { openSettingModalAtom } from '@affine/core/atoms';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { SubscriptionService } from '@affine/core/modules/cloud'; import { SubscriptionService } from '@affine/core/modules/cloud';
import { EditorService } from '@affine/core/modules/editor';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { AiIcon } from '@blocksuite/icons/rc'; import { AiIcon } from '@blocksuite/icons/rc';
import { import { useLiveData, useServices } from '@toeverything/infra';
DocService,
useLiveData,
useServices,
WorkspaceService,
} from '@toeverything/infra';
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { useAtomValue, useSetAtom } from 'jotai'; import { useAtomValue, useSetAtom } from 'jotai';
import Lottie from 'lottie-react'; import Lottie from 'lottie-react';
@@ -46,10 +42,9 @@ const EdgelessOnboardingAnimation = () => {
}; };
export const AIOnboardingEdgeless = () => { export const AIOnboardingEdgeless = () => {
const { docService, subscriptionService } = useServices({ const { subscriptionService, editorService } = useServices({
WorkspaceService,
DocService,
SubscriptionService, SubscriptionService,
EditorService,
}); });
const t = useI18n(); const t = useI18n();
@@ -61,8 +56,7 @@ export const AIOnboardingEdgeless = () => {
const setSettingModal = useSetAtom(openSettingModalAtom); const setSettingModal = useSetAtom(openSettingModalAtom);
const doc = docService.doc; const mode = useLiveData(editorService.editor.mode$);
const mode = useLiveData(doc.mode$);
const goToPricingPlans = useCallback(() => { const goToPricingPlans = useCallback(() => {
track.$.aiOnboarding.dialog.viewPlans(); track.$.aiOnboarding.dialog.viewPlans();

View File

@@ -5,6 +5,7 @@ import { Modal, useConfirmModal } from '@affine/component/ui/modal';
import { openSettingModalAtom } from '@affine/core/atoms'; import { openSettingModalAtom } from '@affine/core/atoms';
import { useDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title'; import { useDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { EditorService } from '@affine/core/modules/editor';
import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { WorkspaceQuotaService } from '@affine/core/modules/quota'; import { WorkspaceQuotaService } from '@affine/core/modules/quota';
import { i18nTime, Trans, useI18n } from '@affine/i18n'; import { i18nTime, Trans, useI18n } from '@affine/i18n';
@@ -14,7 +15,6 @@ import * as Collapsible from '@radix-ui/react-collapsible';
import type { DialogContentProps } from '@radix-ui/react-dialog'; import type { DialogContentProps } from '@radix-ui/react-dialog';
import { import {
type DocMode, type DocMode,
DocService,
useLiveData, useLiveData,
useService, useService,
WorkspaceService, WorkspaceService,
@@ -433,8 +433,8 @@ const PageHistoryManager = ({
[activeVersion, onClose, onRestore, snapshotPage] [activeVersion, onClose, onRestore, snapshotPage]
); );
const doc = useService(DocService).doc; const editor = useService(EditorService).editor;
const [mode, setMode] = useState<DocMode>(doc.mode$.value); const [mode, setMode] = useState<DocMode>(editor.mode$.value);
const title = useDocCollectionPageTitle(docCollection, pageId); const title = useDocCollectionPageTitle(docCollection, pageId);

View File

@@ -5,12 +5,7 @@ import {
Scrollable, Scrollable,
} from '@affine/component'; } from '@affine/component';
import { DocsSearchService } from '@affine/core/modules/docs-search'; import { DocsSearchService } from '@affine/core/modules/docs-search';
import { import { LiveData, useLiveData, useServices } from '@toeverything/infra';
LiveData,
useLiveData,
useServices,
WorkspaceService,
} from '@toeverything/infra';
import { Suspense, useCallback, useContext, useMemo, useRef } from 'react'; import { Suspense, useCallback, useContext, useMemo, useRef } from 'react';
import { BlocksuiteHeaderTitle } from '../../../blocksuite/block-suite-header/title'; import { BlocksuiteHeaderTitle } from '../../../blocksuite/block-suite-header/title';
@@ -35,9 +30,8 @@ export const InfoModal = ({
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
docId: string; docId: string;
}) => { }) => {
const { docsSearchService, workspaceService } = useServices({ const { docsSearchService } = useServices({
DocsSearchService, DocsSearchService,
WorkspaceService,
}); });
const titleInputHandleRef = useRef<InlineEditHandle>(null); const titleInputHandleRef = useRef<InlineEditHandle>(null);
const manager = usePagePropertiesManager(docId); const manager = usePagePropertiesManager(docId);
@@ -72,10 +66,9 @@ export const InfoModal = ({
> >
<div className={styles.titleContainer} data-testid="info-modal-title"> <div className={styles.titleContainer} data-testid="info-modal-title">
<BlocksuiteHeaderTitle <BlocksuiteHeaderTitle
docId={docId}
className={styles.titleStyle} className={styles.titleStyle}
inputHandleRef={titleInputHandleRef} inputHandleRef={titleInputHandleRef}
pageId={docId}
docCollection={workspaceService.workspace.docCollection}
/> />
</div> </div>
<managerContext.Provider value={manager}> <managerContext.Provider value={manager}>

View File

@@ -84,10 +84,17 @@ export function AffinePageReference({
const t = useI18n(); const t = useI18n();
const docsService = useService(DocsService); const docsService = useService(DocsService);
const mode$ = LiveData.from(docsService.list.observeMode(pageId), undefined); const docPrimaryMode = useLiveData(
const docMode = useLiveData(mode$) ?? null; LiveData.computed(get => {
const primaryMode$ = get(docsService.list.doc$(pageId))?.primaryMode$;
if (!primaryMode$) {
return null;
}
return get(primaryMode$);
})
);
const el = pageReferenceRenderer({ const el = pageReferenceRenderer({
docMode, docMode: docPrimaryMode,
pageId, pageId,
pageMetaHelper, pageMetaHelper,
journalHelper, journalHelper,

View File

@@ -3,10 +3,11 @@ import { Divider } from '@affine/component/ui/divider';
import { ExportMenuItems } from '@affine/core/components/page-list'; import { ExportMenuItems } from '@affine/core/components/page-list';
import { useExportPage } from '@affine/core/hooks/affine/use-export-page'; import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url'; import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url';
import { EditorService } from '@affine/core/modules/editor';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { CopyIcon } from '@blocksuite/icons/rc'; import { CopyIcon } from '@blocksuite/icons/rc';
import { DocService, useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import * as styles from './index.css'; import * as styles from './index.css';
import type { ShareMenuProps } from './share-menu'; import type { ShareMenuProps } from './share-menu';
@@ -16,7 +17,7 @@ export const ShareExport = ({
currentPage, currentPage,
}: ShareMenuProps) => { }: ShareMenuProps) => {
const t = useI18n(); const t = useI18n();
const doc = useService(DocService).doc; const editor = useService(EditorService).editor;
const workspaceId = workspace.id; const workspaceId = workspace.id;
const pageId = currentPage.id; const pageId = currentPage.id;
const { sharingUrl, onClickCopyLink } = useSharingUrl({ const { sharingUrl, onClickCopyLink } = useSharingUrl({
@@ -25,7 +26,7 @@ export const ShareExport = ({
urlType: 'workspace', urlType: 'workspace',
}); });
const exportHandler = useExportPage(currentPage); const exportHandler = useExportPage(currentPage);
const currentMode = useLiveData(doc.mode$); const currentMode = useLiveData(editor.mode$);
const isMac = environment.isBrowser && environment.isMacOs; const isMac = environment.isBrowser && environment.isMacOs;
return ( return (

View File

@@ -71,7 +71,7 @@ export const AffineSharePage = (props: ShareMenuProps) => {
isSharedPage === null || sharedMode === null || baseUrl === null; isSharedPage === null || sharedMode === null || baseUrl === null;
const [showDisable, setShowDisable] = useState(false); const [showDisable, setShowDisable] = useState(false);
const currentDocMode = useLiveData(doc.mode$); const currentDocMode = useLiveData(doc.primaryMode$);
const mode = useMemo(() => { const mode = useMemo(() => {
if (isSharedPage && sharedMode) { if (isSharedPage && sharedMode) {

View File

@@ -57,7 +57,7 @@ export function createLinkedWidgetConfig(framework: FrameworkProvider) {
const MAX_DOCS = 6; const MAX_DOCS = 6;
const docsService = framework.get(DocsService); const docsService = framework.get(DocsService);
const isEdgeless = (d: DocMeta) => const isEdgeless = (d: DocMeta) =>
docsService.list.getMode(d.id) === 'edgeless'; docsService.list.getPrimaryMode(d.id) === 'edgeless';
return Promise.resolve([ return Promise.resolve([
{ {
name: 'Link to Doc', name: 'Link to Doc',

View File

@@ -279,28 +279,28 @@ export function patchDocModeService(
pageService.docModeService = { pageService.docModeService = {
setMode: (mode: DocMode, id?: string) => { setMode: (mode: DocMode, id?: string) => {
if (id) { if (id) {
docsService.list.setMode(id, mode); docsService.list.setPrimaryMode(id, mode);
} else { } else {
docService.doc.setMode(mode); docService.doc.setPrimaryMode(mode);
} }
}, },
getMode: (id?: string) => { getMode: (id?: string) => {
const mode = id const mode = id
? docsService.list.getMode(id) ? docsService.list.getPrimaryMode(id)
: docService.doc.getMode(); : docService.doc.getPrimaryMode();
return mode || DEFAULT_MODE; return mode || DEFAULT_MODE;
}, },
toggleMode: (id?: string) => { toggleMode: (id?: string) => {
const mode = id const mode = id
? docsService.list.toggleMode(id) ? docsService.list.togglePrimaryMode(id)
: docService.doc.toggleMode(); : docService.doc.togglePrimaryMode();
return mode || DEFAULT_MODE; return mode || DEFAULT_MODE;
}, },
onModeChange: (handler: (mode: DocMode) => void, id?: string) => { onModeChange: (handler: (mode: DocMode) => void, id?: string) => {
// eslint-disable-next-line rxjs/finnish // eslint-disable-next-line rxjs/finnish
const mode$ = id const mode$ = id
? docsService.list.observeMode(id) ? docsService.list.primaryMode$(id)
: docService.doc.observeMode(); : docService.doc.primaryMode$;
const sub = mode$.subscribe(m => handler(m || DEFAULT_MODE)); const sub = mode$.subscribe(m => handler(m || DEFAULT_MODE));
return { return {
dispose: sub.unsubscribe, dispose: sub.unsubscribe,
@@ -412,7 +412,7 @@ export function patchQuickSearchService(
? 'edgeless' ? 'edgeless'
: 'page'; : 'page';
const newDoc = docsService.createDoc({ const newDoc = docsService.createDoc({
mode, primaryMode: mode,
title: result.payload.title, title: result.payload.title,
}); });
resolve({ resolve({

View File

@@ -19,6 +19,7 @@ import { useExportPage } from '@affine/core/hooks/affine/use-export-page';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { EditorService } from '@affine/core/modules/editor';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { ViewService } from '@affine/core/modules/workbench/services/view'; import { ViewService } from '@affine/core/modules/workbench/services/view';
import { useDetailPageHeaderResponsive } from '@affine/core/pages/workspace/detail-page/use-header-responsive'; import { useDetailPageHeaderResponsive } from '@affine/core/pages/workspace/detail-page/use-header-responsive';
@@ -41,12 +42,7 @@ import {
TocIcon, TocIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import type { Doc } from '@blocksuite/store'; import type { Doc } from '@blocksuite/store';
import { import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
DocService,
useLiveData,
useService,
WorkspaceService,
} from '@toeverything/infra';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
@@ -75,9 +71,11 @@ export const PageHeaderMenuButton = ({
const workspace = useService(WorkspaceService).workspace; const workspace = useService(WorkspaceService).workspace;
const docCollection = workspace.docCollection; const docCollection = workspace.docCollection;
const doc = useService(DocService).doc; const editorService = useService(EditorService);
const isInTrash = useLiveData(doc.meta$.map(m => m.trash)); const isInTrash = useLiveData(
const currentMode = useLiveData(doc.mode$); editorService.editor.doc.meta$.map(meta => meta.trash)
);
const currentMode = useLiveData(editorService.editor.mode$);
const workbench = useService(WorkbenchService).workbench; const workbench = useService(WorkbenchService).workbench;
@@ -139,9 +137,9 @@ export const PageHeaderMenuButton = ({
setTrashModal({ setTrashModal({
open: true, open: true,
pageIds: [pageId], pageIds: [pageId],
pageTitles: [doc.meta$.value.title ?? ''], pageTitles: [editorService.editor.doc.meta$.value.title ?? ''],
}); });
}, [doc.meta$.value.title, pageId, setTrashModal]); }, [editorService, pageId, setTrashModal]);
const handleRename = useCallback(() => { const handleRename = useCallback(() => {
rename?.(); rename?.();
@@ -149,7 +147,7 @@ export const PageHeaderMenuButton = ({
}, [rename]); }, [rename]);
const handleSwitchMode = useCallback(() => { const handleSwitchMode = useCallback(() => {
doc.toggleMode(); editorService.editor.toggleMode();
track.$.header.docOptions.switchPageMode({ track.$.header.docOptions.switchPageMode({
mode: currentMode === 'page' ? 'edgeless' : 'page', mode: currentMode === 'page' ? 'edgeless' : 'page',
}); });
@@ -158,7 +156,7 @@ export const PageHeaderMenuButton = ({
? t['com.affine.toastMessage.edgelessMode']() ? t['com.affine.toastMessage.edgelessMode']()
: t['com.affine.toastMessage.pageMode']() : t['com.affine.toastMessage.pageMode']()
); );
}, [currentMode, doc, t]); }, [currentMode, editorService, t]);
const menuItemStyle = { const menuItemStyle = {
padding: '4px 12px', padding: '4px 12px',
transition: 'all 0.3s', transition: 'all 0.3s',
@@ -170,7 +168,7 @@ export const PageHeaderMenuButton = ({
} }
}, []); }, []);
const exportHandler = useExportPage(doc.blockSuiteDoc); const exportHandler = useExportPage(editorService.editor.doc.blockSuiteDoc);
const handleDuplicate = useCallback(() => { const handleDuplicate = useCallback(() => {
duplicate(pageId); duplicate(pageId);

View File

@@ -1,53 +1,52 @@
import type { InlineEditProps } from '@affine/component'; import type { InlineEditProps } from '@affine/component';
import { InlineEdit } from '@affine/component'; import { InlineEdit } from '@affine/component';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { track } from '@affine/core/mixpanel';
import { import {
useBlockSuiteDocMeta, DocsService,
useDocMetaHelper, useLiveData,
} from '@affine/core/hooks/use-block-suite-page-meta'; useService,
import type { DocCollection } from '@affine/core/shared'; WorkspaceService,
} from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { useCallback } from 'react';
import * as styles from './style.css'; import * as styles from './style.css';
export interface BlockSuiteHeaderTitleProps { export interface BlockSuiteHeaderTitleProps {
docCollection: DocCollection; docId: string;
pageId: string;
/** if set, title cannot be edited */ /** if set, title cannot be edited */
isPublic?: boolean;
inputHandleRef?: InlineEditProps['handleRef']; inputHandleRef?: InlineEditProps['handleRef'];
className?: string; className?: string;
onEditSave?: () => void;
} }
const inputAttrs = { const inputAttrs = {
'data-testid': 'title-content', 'data-testid': 'title-content',
} as HTMLAttributes<HTMLInputElement>; } as HTMLAttributes<HTMLInputElement>;
export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => { export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
const { docCollection, pageId, isPublic, inputHandleRef, onEditSave } = props; const { inputHandleRef, docId } = props;
const currentPage = docCollection.getDoc(pageId); const workspaceService = useService(WorkspaceService);
const pageMeta = useBlockSuiteDocMeta(docCollection).find( const isSharedMode = workspaceService.workspace.openOptions.isSharedMode;
meta => meta.id === currentPage?.id const docsService = useService(DocsService);
);
const title = pageMeta?.title;
const { setDocTitle } = useDocMetaHelper(docCollection);
const onChange = useCallback( const docRecord = useLiveData(docsService.list.doc$(docId));
(v: string) => { const docTitle = useLiveData(docRecord?.title$);
onEditSave?.();
setDocTitle(currentPage?.id || '', v); const onChange = useAsyncCallback(
async (v: string) => {
await docsService.changeDocTitle(docId, v);
track.$.header.actions.renameDoc();
}, },
[currentPage?.id, onEditSave, setDocTitle] [docId, docsService]
); );
return ( return (
<InlineEdit <InlineEdit
className={clsx(styles.title, props.className)} className={clsx(styles.title, props.className)}
autoSelect autoSelect
value={title} value={docTitle}
onChange={onChange} onChange={onChange}
editable={!isPublic} editable={!isSharedMode}
exitible={true} exitible={true}
placeholder="Untitled" placeholder="Untitled"
data-testid="title-edit-button" data-testid="title-edit-button"

View File

@@ -1,14 +1,10 @@
import { RadioGroup, type RadioItem, toast, Tooltip } from '@affine/component'; import { RadioGroup, type RadioItem, toast, Tooltip } from '@affine/component';
import { registerAffineCommand } from '@affine/core/commands'; import { registerAffineCommand } from '@affine/core/commands';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import { EditorService } from '@affine/core/modules/editor';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc'; import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
import { import { type DocMode, useLiveData, useService } from '@toeverything/infra';
type DocMode,
DocsService,
useLiveData,
useService,
} from '@toeverything/infra';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { switchItem } from './style.css'; import { switchItem } from './style.css';
@@ -33,30 +29,28 @@ const PageRadioItem: RadioItem = {
className: switchItem, className: switchItem,
}; };
export const EditorModeSwitch = ({ export const EditorModeSwitch = () => {
pageId,
isPublic,
publicMode,
}: EditorModeSwitchProps) => {
const t = useI18n(); const t = useI18n();
const docsService = useService(DocsService); const editor = useService(EditorService).editor;
const doc = useLiveData(docsService.list.doc$(pageId)); const trash = useLiveData(editor.doc.trash$);
const trash = useLiveData(doc?.trash$); const isSharedMode = editor.isSharedMode;
const currentMode = useLiveData(doc?.mode$); const currentMode = useLiveData(editor.mode$);
const togglePage = useCallback(() => { const togglePage = useCallback(() => {
if (currentMode === 'page' || isPublic || trash) return; if (currentMode === 'page' || isSharedMode || trash) return;
doc?.setMode('page'); editor.setMode('page');
editor.doc.setPrimaryMode('page');
toast(t['com.affine.toastMessage.pageMode']()); toast(t['com.affine.toastMessage.pageMode']());
track.$.header.actions.switchPageMode({ mode: 'page' }); track.$.header.actions.switchPageMode({ mode: 'page' });
}, [currentMode, doc, isPublic, t, trash]); }, [currentMode, editor, isSharedMode, t, trash]);
const toggleEdgeless = useCallback(() => { const toggleEdgeless = useCallback(() => {
if (currentMode === 'edgeless' || isPublic || trash) return; if (currentMode === 'edgeless' || isSharedMode || trash) return;
doc?.setMode('edgeless'); editor.setMode('edgeless');
editor.doc.setPrimaryMode('edgeless');
toast(t['com.affine.toastMessage.edgelessMode']()); toast(t['com.affine.toastMessage.edgelessMode']());
track.$.header.actions.switchPageMode({ mode: 'edgeless' }); track.$.header.actions.switchPageMode({ mode: 'edgeless' });
}, [currentMode, doc, isPublic, t, trash]); }, [currentMode, editor, isSharedMode, t, trash]);
const onModeChange = useCallback( const onModeChange = useCallback(
(mode: DocMode) => { (mode: DocMode) => {
@@ -66,13 +60,12 @@ export const EditorModeSwitch = ({
); );
const shouldHide = useCallback( const shouldHide = useCallback(
(mode: DocMode) => (mode: DocMode) => (trash || isSharedMode) && currentMode !== mode,
(trash && currentMode !== mode) || (isPublic && publicMode !== mode), [currentMode, isSharedMode, trash]
[currentMode, isPublic, publicMode, trash]
); );
useEffect(() => { useEffect(() => {
if (trash || isPublic || currentMode === undefined) return; if (trash || isSharedMode || currentMode === undefined) return;
return registerAffineCommand({ return registerAffineCommand({
id: 'affine:doc-mode-switch', id: 'affine:doc-mode-switch',
category: 'editor:page', category: 'editor:page',
@@ -87,14 +80,14 @@ export const EditorModeSwitch = ({
}, },
run: () => onModeChange(currentMode === 'edgeless' ? 'page' : 'edgeless'), run: () => onModeChange(currentMode === 'edgeless' ? 'page' : 'edgeless'),
}); });
}, [currentMode, isPublic, onModeChange, t, trash]); }, [currentMode, isSharedMode, onModeChange, t, trash]);
return ( return (
<Tooltip <Tooltip
content={t['Switch']()} content={t['Switch']()}
shortcut={['$alt', 'S']} shortcut={['$alt', 'S']}
side="bottom" side="bottom"
options={{ hidden: isPublic || trash }} options={{ hidden: trash || isSharedMode }}
> >
<div> <div>
<PureEditorModeSwitch <PureEditorModeSwitch
@@ -110,7 +103,7 @@ export const EditorModeSwitch = ({
export interface PureEditorModeSwitchProps { export interface PureEditorModeSwitchProps {
mode?: DocMode; mode?: DocMode;
setMode: (mode: DocMode) => void; setMode?: (mode: DocMode) => void;
hidePage?: boolean; hidePage?: boolean;
hideEdgeless?: boolean; hideEdgeless?: boolean;
} }

View File

@@ -9,19 +9,14 @@ import type { DocCollection } from '../../../shared';
export const usePageHelper = (docCollection: DocCollection) => { export const usePageHelper = (docCollection: DocCollection) => {
const workbench = useService(WorkbenchService).workbench; const workbench = useService(WorkbenchService).workbench;
const { createDoc } = useDocCollectionHelper(docCollection); const { createDoc } = useDocCollectionHelper(docCollection);
const docRecordList = useService(DocsService).list; const docsService = useService(DocsService);
const docRecordList = docsService.list;
const isPreferredEdgeless = useCallback(
(pageId: string) =>
docRecordList.doc$(pageId).value?.mode$.value === 'edgeless',
[docRecordList]
);
const createPageAndOpen = useCallback( const createPageAndOpen = useCallback(
(mode?: 'page' | 'edgeless', open?: boolean | 'new-tab') => { (mode?: 'page' | 'edgeless', open?: boolean | 'new-tab') => {
const page = createDoc(); const page = createDoc();
initEmptyPage(page); initEmptyPage(page);
docRecordList.doc$(page.id).value?.setMode(mode || 'page'); docRecordList.doc$(page.id).value?.setPrimaryMode(mode || 'page');
if (open !== false) if (open !== false)
workbench.openDoc(page.id, { workbench.openDoc(page.id, {
at: open === 'new-tab' ? 'new-tab' : 'active', at: open === 'new-tab' ? 'new-tab' : 'active',
@@ -82,16 +77,10 @@ export const usePageHelper = (docCollection: DocCollection) => {
return useMemo(() => { return useMemo(() => {
return { return {
isPreferredEdgeless,
createPage: (open?: boolean | 'new-tab') => createPage: (open?: boolean | 'new-tab') =>
createPageAndOpen('page', open), createPageAndOpen('page', open),
createEdgeless: createEdgelessAndOpen, createEdgeless: createEdgelessAndOpen,
importFile: importFileAndOpen, importFile: importFileAndOpen,
}; };
}, [ }, [createEdgelessAndOpen, createPageAndOpen, importFileAndOpen]);
isPreferredEdgeless,
createEdgelessAndOpen,
createPageAndOpen,
importFileAndOpen,
]);
}; };

View File

@@ -6,7 +6,6 @@ import type { AffineEditorContainer } from '@blocksuite/presets';
import type { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store'; import type { Doc as BlockSuiteDoc, DocCollection } from '@blocksuite/store';
import { import {
type DocMode, type DocMode,
DocService,
fontStyleOptions, fontStyleOptions,
useLiveData, useLiveData,
useService, useService,
@@ -17,6 +16,7 @@ import { memo, Suspense, useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
import { EditorService } from '../modules/editor';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor'; import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import * as styles from './page-detail-editor.css'; import * as styles from './page-detail-editor.css';
@@ -45,19 +45,10 @@ function useRouterHash() {
const PageDetailEditorMain = memo(function PageDetailEditorMain({ const PageDetailEditorMain = memo(function PageDetailEditorMain({
page, page,
onLoad, onLoad,
isPublic,
publishMode,
}: PageDetailEditorProps & { page: BlockSuiteDoc }) { }: PageDetailEditorProps & { page: BlockSuiteDoc }) {
const currentMode = useLiveData(useService(DocService).doc.mode$); const editor = useService(EditorService).editor;
const mode = useMemo(() => { const mode = useLiveData(editor.mode$);
const shareMode = publishMode || currentMode; const isSharedMode = editor.isSharedMode;
if (isPublic) {
return shareMode;
}
return currentMode;
}, [isPublic, publishMode, currentMode]);
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();
const value = useMemo(() => { const value = useMemo(() => {
@@ -97,8 +88,8 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
return ( return (
<Editor <Editor
className={clsx(styles.editor, { className={clsx(styles.editor, {
'full-screen': !isPublic && appSettings.fullWidthLayout, 'full-screen': !isSharedMode && appSettings.fullWidthLayout,
'is-public': isPublic, 'is-public': isSharedMode,
})} })}
style={ style={
{ {
@@ -107,7 +98,7 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
} }
mode={mode} mode={mode}
page={page} page={page}
shared={isPublic} shared={isSharedMode}
defaultSelectedBlockId={blockId} defaultSelectedBlockId={blockId}
onLoadEditor={onLoadEditor} onLoadEditor={onLoadEditor}
/> />

View File

@@ -9,7 +9,6 @@ import type { DocMeta } from '@blocksuite/store';
import { useService, WorkspaceService } from '@toeverything/infra'; import { useService, WorkspaceService } from '@toeverything/infra';
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
import { ListFloatingToolbar } from '../components/list-floating-toolbar'; import { ListFloatingToolbar } from '../components/list-floating-toolbar';
import { usePageItemGroupDefinitions } from '../group-definitions'; import { usePageItemGroupDefinitions } from '../group-definitions';
import { usePageHeaderColsDef } from '../header-col-def'; import { usePageHeaderColsDef } from '../header-col-def';
@@ -69,7 +68,6 @@ export const VirtualizedPageList = ({
const currentWorkspace = useService(WorkspaceService).workspace; const currentWorkspace = useService(WorkspaceService).workspace;
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection); const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
const pageOperations = usePageOperationsRenderer(); const pageOperations = usePageOperationsRenderer();
const { isPreferredEdgeless } = usePageHelper(currentWorkspace.docCollection);
const pageHeaderColsDef = usePageHeaderColsDef(); const pageHeaderColsDef = usePageHeaderColsDef();
const filteredPageMetas = useFilteredPageMetas(pageMetas, { const filteredPageMetas = useFilteredPageMetas(pageMetas, {
@@ -162,7 +160,6 @@ export const VirtualizedPageList = ({
onSelectedIdsChange={setSelectedPageIds} onSelectedIdsChange={setSelectedPageIds}
items={pageMetasToRender} items={pageMetasToRender}
rowAsLink rowAsLink
isPreferredEdgeless={isPreferredEdgeless}
docCollection={currentWorkspace.docCollection} docCollection={currentWorkspace.docCollection}
operationsRenderer={pageOperationRenderer} operationsRenderer={pageOperationRenderer}
itemRenderer={pageItemRenderer} itemRenderer={pageItemRenderer}

View File

@@ -201,7 +201,6 @@ export const ItemGroup = <T extends ListItem>({
const requiredPropNames = [ const requiredPropNames = [
'docCollection', 'docCollection',
'rowAsLink', 'rowAsLink',
'isPreferredEdgeless',
'operationsRenderer', 'operationsRenderer',
'selectedIds', 'selectedIds',
'onSelectedIdsChange', 'onSelectedIdsChange',
@@ -294,13 +293,12 @@ const PageTitle = ({ id }: { id: string }) => {
const UnifiedPageIcon = ({ const UnifiedPageIcon = ({
id, id,
docCollection, docCollection,
isPreferredEdgeless,
}: { }: {
id: string; id: string;
docCollection: DocCollection; docCollection: DocCollection;
isPreferredEdgeless?: (id: string) => boolean;
}) => { }) => {
const isEdgeless = isPreferredEdgeless ? isPreferredEdgeless(id) : false; const list = useService(DocsService).list;
const isEdgeless = useLiveData(list.primaryMode$(id)) === 'edgeless';
const { isJournal } = useJournalInfoHelper(docCollection, id); const { isJournal } = useJournalInfoHelper(docCollection, id);
if (isJournal) { if (isJournal) {
return <TodayIcon />; return <TodayIcon />;
@@ -340,13 +338,7 @@ function pageMetaToListItemProp(
updatedDate: item.updatedDate ? new Date(item.updatedDate) : undefined, updatedDate: item.updatedDate ? new Date(item.updatedDate) : undefined,
to: props.rowAsLink && !props.selectable ? `/${item.id}` : undefined, to: props.rowAsLink && !props.selectable ? `/${item.id}` : undefined,
onClick: toggleSelection, onClick: toggleSelection,
icon: ( icon: <UnifiedPageIcon id={item.id} docCollection={props.docCollection} />,
<UnifiedPageIcon
id={item.id}
docCollection={props.docCollection}
isPreferredEdgeless={props.isPreferredEdgeless}
/>
),
tags: tags:
item.tags item.tags
?.map(id => tagIdToTagOption(id, props.docCollection)) ?.map(id => tagIdToTagOption(id, props.docCollection))

View File

@@ -95,7 +95,6 @@ export interface ListProps<T> {
className?: string; className?: string;
hideHeader?: boolean; // whether or not to hide the header. default is false (showing header) hideHeader?: boolean; // whether or not to hide the header. default is false (showing header)
groupBy?: ItemGroupDefinition<T>[]; groupBy?: ItemGroupDefinition<T>[];
isPreferredEdgeless?: (pageId: string) => boolean; // determines the icon used for each row
rowAsLink?: boolean; rowAsLink?: boolean;
selectable?: 'toggle' | boolean; // show selection checkbox. toggle means showing a toggle selection in header on click; boolean == true means showing a selection checkbox for each item selectable?: 'toggle' | boolean; // show selection checkbox. toggle means showing a toggle selection in header on click; boolean == true means showing a selection checkbox for each item
selectedIds?: string[]; // selected page ids selectedIds?: string[]; // selected page ids

View File

@@ -192,7 +192,6 @@ export const EditCollection = ({
export type AllPageListConfig = { export type AllPageListConfig = {
allPages: DocMeta[]; allPages: DocMeta[];
docCollection: DocCollection; docCollection: DocCollection;
isEdgeless: (id: string) => boolean;
/** /**
* Return `undefined` if the page is not public * Return `undefined` if the page is not public
*/ */

View File

@@ -9,7 +9,7 @@ import {
ToggleCollapseIcon, ToggleCollapseIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import type { DocMeta } from '@blocksuite/store'; import type { DocMeta } from '@blocksuite/store';
import { useLiveData, useService } from '@toeverything/infra'; import { DocsService, useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import clsx from 'clsx'; import clsx from 'clsx';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@@ -42,6 +42,7 @@ export const RulesMode = ({
const [showPreview, setShowPreview] = useState(true); const [showPreview, setShowPreview] = useState(true);
const allowListPages: DocMeta[] = []; const allowListPages: DocMeta[] = [];
const rulesPages: DocMeta[] = []; const rulesPages: DocMeta[] = [];
const docsService = useService(DocsService);
const favAdapter = useService(CompatibleFavoriteItemsAdapter); const favAdapter = useService(CompatibleFavoriteItemsAdapter);
const favorites = useLiveData(favAdapter.favorites$); const favorites = useLiveData(favAdapter.favorites$);
allPageListConfig.allPages.forEach(meta => { allPageListConfig.allPages.forEach(meta => {
@@ -158,7 +159,8 @@ export const RulesMode = ({
alignItems: 'center', alignItems: 'center',
}} }}
> >
{allPageListConfig.isEdgeless(id) ? ( {docsService.list.getPrimaryMode(id) ===
'edgeless' ? (
<EdgelessIcon style={{ width: 16, height: 16 }} /> <EdgelessIcon style={{ width: 16, height: 16 }} />
) : ( ) : (
<PageIcon style={{ width: 16, height: 16 }} /> <PageIcon style={{ width: 16, height: 16 }} />
@@ -211,7 +213,6 @@ export const RulesMode = ({
className={styles.resultPages} className={styles.resultPages}
items={rulesPages} items={rulesPages}
docCollection={allPageListConfig.docCollection} docCollection={allPageListConfig.docCollection}
isPreferredEdgeless={allPageListConfig.isEdgeless}
operationsRenderer={operationsRenderer} operationsRenderer={operationsRenderer}
></List> ></List>
) : ( ) : (
@@ -230,7 +231,6 @@ export const RulesMode = ({
className={styles.resultPages} className={styles.resultPages}
items={allowListPages} items={allowListPages}
docCollection={allPageListConfig.docCollection} docCollection={allPageListConfig.docCollection}
isPreferredEdgeless={allPageListConfig.isEdgeless}
operationsRenderer={operationsRenderer} operationsRenderer={operationsRenderer}
></List> ></List>
</div> </div>

View File

@@ -7,7 +7,6 @@ import { Trans, useI18n } from '@affine/i18n';
import { FilterIcon } from '@blocksuite/icons/rc'; import { FilterIcon } from '@blocksuite/icons/rc';
import type { DocMeta } from '@blocksuite/store'; import type { DocMeta } from '@blocksuite/store';
import { import {
DocsService,
useLiveData, useLiveData,
useServices, useServices,
WorkspaceService, WorkspaceService,
@@ -57,17 +56,12 @@ export const SelectPage = ({
const clearSelected = useCallback(() => { const clearSelected = useCallback(() => {
onChange([]); onChange([]);
}, [onChange]); }, [onChange]);
const { const { workspaceService, compatibleFavoriteItemsAdapter, shareDocsService } =
workspaceService, useServices({
compatibleFavoriteItemsAdapter, ShareDocsService,
shareDocsService, WorkspaceService,
docsService, CompatibleFavoriteItemsAdapter,
} = useServices({ });
DocsService,
ShareDocsService,
WorkspaceService,
CompatibleFavoriteItemsAdapter,
});
const shareDocs = useLiveData(shareDocsService.shareDocs?.list$); const shareDocs = useLiveData(shareDocsService.shareDocs?.list$);
const workspace = workspaceService.workspace; const workspace = workspaceService.workspace;
const docCollection = workspace.docCollection; const docCollection = workspace.docCollection;
@@ -97,13 +91,6 @@ export const SelectPage = ({
[favourites] [favourites]
); );
const isEdgeless = useCallback(
(id: string) => {
return docsService.list.doc$(id).value?.mode$.value === 'edgeless';
},
[docsService.list]
);
const onToggleFavoritePage = useCallback( const onToggleFavoritePage = useCallback(
(page: DocMeta) => { (page: DocMeta) => {
const status = isFavorite(page); const status = isFavorite(page);
@@ -207,7 +194,6 @@ export const SelectPage = ({
selectable selectable
onSelectedIdsChange={onChange} onSelectedIdsChange={onChange}
selectedIds={value} selectedIds={value}
isPreferredEdgeless={isEdgeless}
operationsRenderer={operationsRenderer} operationsRenderer={operationsRenderer}
itemRenderer={pageItemRenderer} itemRenderer={pageItemRenderer}
headerRenderer={pageHeaderRenderer} headerRenderer={pageHeaderRenderer}

View File

@@ -6,7 +6,6 @@ import type { DocMeta } from '@blocksuite/store';
import { useService, WorkspaceService } from '@toeverything/infra'; import { useService, WorkspaceService } from '@toeverything/infra';
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
import { ListFloatingToolbar } from './components/list-floating-toolbar'; import { ListFloatingToolbar } from './components/list-floating-toolbar';
import { usePageHeaderColsDef } from './header-col-def'; import { usePageHeaderColsDef } from './header-col-def';
import { TrashOperationCell } from './operation-cell'; import { TrashOperationCell } from './operation-cell';
@@ -26,8 +25,6 @@ export const VirtualizedTrashList = () => {
trash: true, trash: true,
}); });
const { isPreferredEdgeless } = usePageHelper(docCollection);
const listRef = useRef<ItemListHandle>(null); const listRef = useRef<ItemListHandle>(null);
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false); const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]); const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
@@ -121,7 +118,6 @@ export const VirtualizedTrashList = () => {
selectable="toggle" selectable="toggle"
items={filteredPageMetas} items={filteredPageMetas}
rowAsLink rowAsLink
isPreferredEdgeless={isPreferredEdgeless}
onSelectionActiveChange={setShowFloatingToolbar} onSelectionActiveChange={setShowFloatingToolbar}
docCollection={currentWorkspace.docCollection} docCollection={currentWorkspace.docCollection}
operationsRenderer={pageOperationsRenderer} operationsRenderer={pageOperationsRenderer}

View File

@@ -3,10 +3,9 @@ import { popupWindow } from '@affine/core/utils';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { CloseIcon, NewIcon } from '@blocksuite/icons/rc'; import { CloseIcon, NewIcon } from '@blocksuite/icons/rc';
import { import {
DocsService,
GlobalContextService, GlobalContextService,
useLiveData, useLiveData,
useService, useServices,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { useSetAtom } from 'jotai/react'; import { useSetAtom } from 'jotai/react';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
@@ -33,12 +32,11 @@ type IslandItemNames = 'whatNew' | 'contact' | 'shortcuts';
const showList = environment.isDesktop ? DESKTOP_SHOW_LIST : DEFAULT_SHOW_LIST; const showList = environment.isDesktop ? DESKTOP_SHOW_LIST : DEFAULT_SHOW_LIST;
export const HelpIsland = () => { export const HelpIsland = () => {
const docId = useLiveData( const { globalContextService } = useServices({
useService(GlobalContextService).globalContext.docId.$ GlobalContextService,
); });
const docRecordList = useService(DocsService).list; const docId = useLiveData(globalContextService.globalContext.docId.$);
const doc = useLiveData(docId ? docRecordList.doc$(docId) : undefined); const docMode = useLiveData(globalContextService.globalContext.docMode.$);
const mode = useLiveData(doc?.mode$);
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
const [spread, setShowSpread] = useState(false); const [spread, setShowSpread] = useState(false);
const t = useI18n(); const t = useI18n();
@@ -69,7 +67,7 @@ export const HelpIsland = () => {
onClick={() => { onClick={() => {
setShowSpread(!spread); setShowSpread(!spread);
}} }}
inEdgelessPage={!!docId && mode === 'edgeless'} inEdgelessPage={!!docId && docMode === 'edgeless'}
> >
<StyledAnimateWrapper <StyledAnimateWrapper
style={{ height: spread ? `${showList.length * 40 + 4}px` : 0 }} style={{ height: spread ? `${showList.length * 40 + 4}px` : 0 }}

View File

@@ -10,8 +10,6 @@ import type { DocMeta } from '@blocksuite/store';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra'; import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
/** /**
* @deprecated very poor performance * @deprecated very poor performance
*/ */
@@ -27,7 +25,6 @@ export const useAllPageListConfig = () => {
const workspace = currentWorkspace.docCollection; const workspace = currentWorkspace.docCollection;
const pageMetas = useBlockSuiteDocMeta(workspace); const pageMetas = useBlockSuiteDocMeta(workspace);
const { isPreferredEdgeless } = usePageHelper(workspace);
const pageMap = useMemo( const pageMap = useMemo(
() => Object.fromEntries(pageMetas.map(page => [page.id, page])), () => Object.fromEntries(pageMetas.map(page => [page.id, page])),
[pageMetas] [pageMetas]
@@ -58,7 +55,6 @@ export const useAllPageListConfig = () => {
return useMemo<AllPageListConfig>(() => { return useMemo<AllPageListConfig>(() => {
return { return {
allPages: pageMetas, allPages: pageMetas,
isEdgeless: isPreferredEdgeless,
getPublicMode(id) { getPublicMode(id) {
const mode = shareDocs?.find(shareDoc => shareDoc.id === id)?.mode; const mode = shareDocs?.find(shareDoc => shareDoc.id === id)?.mode;
if (mode === PublicPageMode.Edgeless) { if (mode === PublicPageMode.Edgeless) {
@@ -83,7 +79,6 @@ export const useAllPageListConfig = () => {
}; };
}, [ }, [
pageMetas, pageMetas,
isPreferredEdgeless,
currentWorkspace.docCollection, currentWorkspace.docCollection,
shareDocs, shareDocs,
pageMap, pageMap,

View File

@@ -75,7 +75,8 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
const duplicate = useAsyncCallback( const duplicate = useAsyncCallback(
async (pageId: string, openPageAfterDuplication: boolean = true) => { async (pageId: string, openPageAfterDuplication: boolean = true) => {
const currentPageMode = pageRecordList.doc$(pageId).value?.mode$.value; const currentPagePrimaryMode =
pageRecordList.doc$(pageId).value?.primaryMode$.value;
const currentPageMeta = getDocMeta(pageId); const currentPageMeta = getDocMeta(pageId);
const newPage = createDoc(); const newPage = createDoc();
const currentPage = docCollection.getDoc(pageId); const currentPage = docCollection.getDoc(pageId);
@@ -99,7 +100,9 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
const newPageTitle = const newPageTitle =
currentPageMeta.title.replace(lastDigitRegex, '') + `(${newNumber})`; currentPageMeta.title.replace(lastDigitRegex, '') + `(${newNumber})`;
pageRecordList.doc$(newPage.id).value?.setMode(currentPageMode || 'page'); pageRecordList
.doc$(newPage.id)
.value?.setPrimaryMode(currentPagePrimaryMode || 'page');
setDocTitle(newPage.id, newPageTitle); setDocTitle(newPage.id, newPageTitle);
openPageAfterDuplication && openPage(docCollection.id, newPage.id); openPageAfterDuplication && openPage(docCollection.id, newPage.id);
}, },

View File

@@ -5,6 +5,7 @@ import {
registerAffineCommand, registerAffineCommand,
} from '@affine/core/commands'; } from '@affine/core/commands';
import { track } from '@affine/core/mixpanel'; import { track } from '@affine/core/mixpanel';
import type { Editor } from '@affine/core/modules/editor';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
@@ -23,10 +24,10 @@ import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
import { useExportPage } from './use-export-page'; import { useExportPage } from './use-export-page';
import { useTrashModalHelper } from './use-trash-modal-helper'; import { useTrashModalHelper } from './use-trash-modal-helper';
export function useRegisterBlocksuiteEditorCommands() { export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
const doc = useService(DocService).doc; const doc = useService(DocService).doc;
const docId = doc.id; const docId = doc.id;
const mode = useLiveData(doc.mode$); const mode = useLiveData(editor.mode$);
const t = useI18n(); const t = useI18n();
const workspace = useService(WorkspaceService).workspace; const workspace = useService(WorkspaceService).workspace;
const docCollection = workspace.docCollection; const docCollection = workspace.docCollection;
@@ -149,7 +150,7 @@ export function useRegisterBlocksuiteEditorCommands() {
mode: mode === 'page' ? 'edgeless' : 'page', mode: mode === 'page' ? 'edgeless' : 'page',
}); });
doc.toggleMode(); editor.toggleMode();
toast( toast(
mode === 'page' mode === 'page'
? t['com.affine.toastMessage.edgelessMode']() ? t['com.affine.toastMessage.edgelessMode']()
@@ -311,6 +312,7 @@ export function useRegisterBlocksuiteEditorCommands() {
unsubs.forEach(unsub => unsub()); unsubs.forEach(unsub => unsub());
}; };
}, [ }, [
editor,
favorite, favorite,
mode, mode,
onClickDelete, onClickDelete,

View File

@@ -101,7 +101,7 @@ const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
timeout(10000 /* 10s */), timeout(10000 /* 10s */),
mergeMap(({ mode, doc }) => { mergeMap(({ mode, doc }) => {
if (doc) { if (doc) {
docsList.setMode(doc.id, mode as DocMode); docsList.setPrimaryMode(doc.id, mode as DocMode);
workbench.openDoc(doc.id); workbench.openDoc(doc.id);
} }
return EMPTY; return EMPTY;

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
import { Scope } from '@toeverything/infra';
import type { Editor } from '../entities/editor';
export class EditorScope extends Scope<{
editor: Editor;
}> {}

View File

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

View File

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

View File

@@ -56,25 +56,25 @@ export const ExplorerDocNode = ({
const [collapsed, setCollapsed] = useState(true); const [collapsed, setCollapsed] = useState(true);
const docRecord = useLiveData(docsService.list.doc$(docId)); const docRecord = useLiveData(docsService.list.doc$(docId));
const docMode = useLiveData(docRecord?.mode$); const docPrimaryMode = useLiveData(docRecord?.primaryMode$);
const docTitle = useLiveData(docRecord?.title$); const docTitle = useLiveData(docRecord?.title$);
const isInTrash = useLiveData(docRecord?.trash$); const isInTrash = useLiveData(docRecord?.trash$);
const Icon = useCallback( const Icon = useCallback(
({ className }: { className?: string }) => { ({ className }: { className?: string }) => {
return isLinked ? ( return isLinked ? (
docMode === 'edgeless' ? ( docPrimaryMode === 'edgeless' ? (
<LinkedEdgelessIcon className={className} /> <LinkedEdgelessIcon className={className} />
) : ( ) : (
<LinkedPageIcon className={className} /> <LinkedPageIcon className={className} />
) )
) : docMode === 'edgeless' ? ( ) : docPrimaryMode === 'edgeless' ? (
<EdgelessIcon className={className} /> <EdgelessIcon className={className} />
) : ( ) : (
<PageIcon className={className} /> <PageIcon className={className} />
); );
}, },
[docMode, isLinked] [docPrimaryMode, isLinked]
); );
const children = useLiveData( const children = useLiveData(

View File

@@ -5,6 +5,7 @@ import { configureCloudModule } from './cloud';
import { configureCollectionModule } from './collection'; import { configureCollectionModule } from './collection';
import { configureDocLinksModule } from './doc-link'; import { configureDocLinksModule } from './doc-link';
import { configureDocsSearchModule } from './docs-search'; import { configureDocsSearchModule } from './docs-search';
import { configureEditorModule } from './editor';
import { configureExplorerModule } from './explorer'; import { configureExplorerModule } from './explorer';
import { configureFavoriteModule } from './favorite'; import { configureFavoriteModule } from './favorite';
import { configureFindInPageModule } from './find-in-page'; import { configureFindInPageModule } from './find-in-page';
@@ -39,4 +40,5 @@ export function configureCommonModules(framework: Framework) {
configureFavoriteModule(framework); configureFavoriteModule(framework);
configureExplorerModule(framework); configureExplorerModule(framework);
configureThemeEditorModule(framework); configureThemeEditorModule(framework);
configureEditorModule(framework);
} }

View File

@@ -17,7 +17,7 @@ import { useCallback, useEffect, useState } from 'react';
import { WorkbenchService } from '../../../workbench'; import { WorkbenchService } from '../../../workbench';
import { PeekViewService } from '../../services/peek-view'; import { PeekViewService } from '../../services/peek-view';
import { useDoc } from '../utils'; import { useEditor } from '../utils';
import * as styles from './doc-peek-view.css'; import * as styles from './doc-peek-view.css';
const logger = new DebugLogger('doc-peek-view'); const logger = new DebugLogger('doc-peek-view');
@@ -69,33 +69,36 @@ export function DocPeekPreview({
mode?: DocMode; mode?: DocMode;
xywh?: `[${number},${number},${number},${number}]`; xywh?: `[${number},${number},${number},${number}]`;
}) { }) {
const { doc, workspace, loading } = useDoc(docId); const { doc, workspace, loading } = useEditor(docId, mode);
const { jumpToTag } = useNavigateHelper(); const { jumpToTag } = useNavigateHelper();
const workbench = useService(WorkbenchService).workbench; const workbench = useService(WorkbenchService).workbench;
const peekView = useService(PeekViewService).peekView; const peekView = useService(PeekViewService).peekView;
const [editor, setEditor] = useState<AffineEditorContainer | null>(null); const [editorElement, setEditorElement] =
useState<AffineEditorContainer | null>(null);
const onRef = (editor: AffineEditorContainer) => { const onRef = (editor: AffineEditorContainer) => {
setEditor(editor); setEditorElement(editor);
}; };
const docs = useService(DocsService); const docs = useService(DocsService);
const [resolvedMode, setResolvedMode] = useState<DocMode | undefined>(mode); const [resolvedMode, setResolvedMode] = useState<DocMode | undefined>(mode);
useEffect(() => { useEffect(() => {
editor?.updateComplete editorElement?.updateComplete
.then(() => { .then(() => {
if (resolvedMode === 'edgeless') { if (resolvedMode === 'edgeless') {
fitViewport(editor, xywh); fitViewport(editorElement, xywh);
} }
}) })
.catch(console.error); .catch(console.error);
return; return;
}, [editor, resolvedMode, xywh]); }, [editorElement, resolvedMode, xywh]);
useEffect(() => { useEffect(() => {
if (!mode || !resolvedMode) { 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]); }, [docId, docs.list, resolvedMode, mode]);
@@ -115,14 +118,15 @@ export function DocPeekPreview({
useEffect(() => { useEffect(() => {
const disposableGroup = new DisposableGroup(); const disposableGroup = new DisposableGroup();
if (editor) { if (editorElement) {
editor.updateComplete editorElement.updateComplete
.then(() => { .then(() => {
if (!editor.host) { if (!editorElement.host) {
return; 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 // doc change event inside peek view should be handled by peek view
disposableGroup.add( disposableGroup.add(
rootService.slots.docLinkClicked.on(({ docId, blockId }) => { rootService.slots.docLinkClicked.on(({ docId, blockId }) => {
@@ -142,7 +146,7 @@ export function DocPeekPreview({
return () => { return () => {
disposableGroup.dispose(); disposableGroup.dispose();
}; };
}, [editor, jumpToTag, peekView, workspace.id]); }, [editorElement, jumpToTag, peekView, workspace.id]);
const openOutlinePanel = useCallback(() => { const openOutlinePanel = useCallback(() => {
workbench.openDoc(docId); workbench.openDoc(docId);
@@ -176,7 +180,7 @@ export function DocPeekPreview({
/> />
</FrameworkScope> </FrameworkScope>
<EditorOutlineViewer <EditorOutlineViewer
editor={editor} editor={editorElement}
show={resolvedMode === 'page'} show={resolvedMode === 'page'}
openOutlinePanel={openOutlinePanel} openOutlinePanel={openOutlinePanel}
/> />

View File

@@ -33,7 +33,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import useSWR from 'swr'; import useSWR from 'swr';
import { PeekViewService } from '../../services/peek-view'; import { PeekViewService } from '../../services/peek-view';
import { useDoc } from '../utils'; import { useEditor } from '../utils';
import { useZoomControls } from './hooks/use-zoom'; import { useZoomControls } from './hooks/use-zoom';
import * as styles from './index.css'; import * as styles from './index.css';
@@ -108,7 +108,7 @@ const ImagePreviewModalImpl = ({
onBlockIdChange: (blockId: string) => void; onBlockIdChange: (blockId: string) => void;
onClose: () => void; onClose: () => void;
}): ReactElement | null => { }): ReactElement | null => {
const { doc, workspace } = useDoc(docId); const { doc, workspace } = useEditor(docId);
const blocksuiteDoc = doc?.blockSuiteDoc; const blocksuiteDoc = doc?.blockSuiteDoc;
const docCollection = workspace.docCollection; const docCollection = workspace.docCollection;
const blockModel = useMemo(() => { const blockModel = useMemo(() => {

View File

@@ -20,7 +20,7 @@ import {
import { WorkbenchService } from '../../workbench'; import { WorkbenchService } from '../../workbench';
import { PeekViewService } from '../services/peek-view'; import { PeekViewService } from '../services/peek-view';
import * as styles from './peek-view-controls.css'; import * as styles from './peek-view-controls.css';
import { useDoc } from './utils'; import { useEditor } from './utils';
type ControlButtonProps = { type ControlButtonProps = {
nameKey: string; nameKey: string;
@@ -100,7 +100,7 @@ export const DocPeekViewControls = ({
const workbench = useService(WorkbenchService).workbench; const workbench = useService(WorkbenchService).workbench;
const { jumpToPageBlock } = useNavigateHelper(); const { jumpToPageBlock } = useNavigateHelper();
const t = useI18n(); const t = useI18n();
const { doc, workspace } = useDoc(docId); const { doc, workspace } = useEditor(docId);
const controls = useMemo(() => { const controls = useMemo(() => {
return [ return [
{ {
@@ -115,12 +115,15 @@ export const DocPeekViewControls = ({
nameKey: 'open', nameKey: 'open',
onClick: () => { onClick: () => {
// TODO(@Peng): for frame blocks, we should mimic "view in edgeless" button behavior // 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 blockId
? jumpToPageBlock(workspace.id, docId, blockId) ? jumpToPageBlock(workspace.id, docId, blockId)
: workbench.openDoc(docId); : workbench.openDoc(docId);
if (mode) {
doc?.setMode(mode);
}
peekView.close('none'); peekView.close('none');
}, },
}, },

View File

@@ -1,20 +1,24 @@
import type { Doc } from '@toeverything/infra'; import type { Doc, DocMode } from '@toeverything/infra';
import { import {
DocsService, DocsService,
useLiveData, useLiveData,
useService, useService,
WorkspaceService, WorkspaceService,
} from '@toeverything/infra'; } 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 currentWorkspace = useService(WorkspaceService).workspace;
const docsService = useService(DocsService); const docsService = useService(DocsService);
const docRecordList = docsService.list; const docRecordList = docsService.list;
const docListReady = useLiveData(docRecordList.isReady$); const docListReady = useLiveData(docRecordList.isReady$);
const docRecord = docRecordList.doc$(pageId).value; const docRecord = docRecordList.doc$(pageId).value;
const preferModeRef = useRef(preferMode);
const [doc, setDoc] = useState<Doc | null>(null); const [doc, setDoc] = useState<Doc | null>(null);
const [editor, setEditor] = useState<Editor | null>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!docRecord) { if (!docRecord) {
@@ -27,6 +31,19 @@ export const useDoc = (pageId: string) => {
}; };
}, [docRecord, docsService, pageId]); }, [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 // set sync engine priority target
useEffect(() => { useEffect(() => {
currentWorkspace.engine.doc.setPriority(pageId, 10); currentWorkspace.engine.doc.setPriority(pageId, 10);
@@ -35,5 +52,5 @@ export const useDoc = (pageId: string) => {
}; };
}, [currentWorkspace, pageId]); }, [currentWorkspace, pageId]);
return { doc, workspace: currentWorkspace, loading: !docListReady }; return { doc, editor, workspace: currentWorkspace, loading: !docListReady };
}; };

View File

@@ -63,13 +63,13 @@ export class CMDKQuickSearchService extends Service {
} else if (result.source === 'creation') { } else if (result.source === 'creation') {
if (result.id === 'creation:create-page') { if (result.id === 'creation:create-page') {
const newDoc = this.docsService.createDoc({ const newDoc = this.docsService.createDoc({
mode: 'page', primaryMode: 'page',
title: result.payload.title, title: result.payload.title,
}); });
this.workbenchService.workbench.openDoc(newDoc.id); this.workbenchService.workbench.openDoc(newDoc.id);
} else if (result.id === 'creation:create-edgeless') { } else if (result.id === 'creation:create-edgeless') {
const newDoc = this.docsService.createDoc({ const newDoc = this.docsService.createDoc({
mode: 'edgeless', primaryMode: 'edgeless',
title: result.payload.title, title: result.payload.title,
}); });
this.workbenchService.workbench.openDoc(newDoc.id); this.workbenchService.workbench.openDoc(newDoc.id);

View File

@@ -16,7 +16,7 @@ export class DocDisplayMetaService extends Service {
); );
const icon = journalDateString const icon = journalDateString
? TodayIcon ? TodayIcon
: docRecord.mode$.value === 'edgeless' : docRecord.primaryMode$.value === 'edgeless'
? EdgelessIcon ? EdgelessIcon
: PageIcon; : PageIcon;

View File

@@ -2,6 +2,7 @@ import { Scrollable } from '@affine/component';
import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor'; import { useActiveBlocksuiteEditor } from '@affine/core/hooks/use-block-suite-editor';
import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state'; import { usePageDocumentTitle } from '@affine/core/hooks/use-global-state';
import { AuthService } from '@affine/core/modules/cloud'; import { AuthService } from '@affine/core/modules/cloud';
import { type Editor, EditorsService } from '@affine/core/modules/editor';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { noop } from '@blocksuite/global/utils'; import { noop } from '@blocksuite/global/utils';
@@ -121,6 +122,7 @@ export const Component = () => {
const t = useI18n(); const t = useI18n();
const [workspace, setWorkspace] = useState<Workspace | null>(null); const [workspace, setWorkspace] = useState<Workspace | null>(null);
const [page, setPage] = useState<Doc | null>(null); const [page, setPage] = useState<Doc | null>(null);
const [editor, setEditor] = useState<Editor | null>(null);
const [_, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor(); const [_, setActiveBlocksuiteEditor] = useActiveBlocksuiteEditor();
const defaultCloudProvider = workspacesService.framework.get( const defaultCloudProvider = workspacesService.framework.get(
@@ -177,6 +179,10 @@ export const Component = () => {
); );
setPage(doc); setPage(doc);
const editor = doc.scope.get(EditorsService).createEditor(publishMode);
setEditor(editor);
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
@@ -188,6 +194,7 @@ export const Component = () => {
workspaceArrayBuffer, workspaceArrayBuffer,
workspaceId, workspaceId,
workspacesService, workspacesService,
publishMode,
]); ]);
const pageTitle = useLiveData(page?.title$); const pageTitle = useLiveData(page?.title$);
@@ -204,58 +211,60 @@ export const Component = () => {
[setActiveBlocksuiteEditor] [setActiveBlocksuiteEditor]
); );
if (!workspace || !page) { if (!workspace || !page || !editor) {
return; return;
} }
return ( return (
<FrameworkScope scope={workspace.scope}> <FrameworkScope scope={workspace.scope}>
<FrameworkScope scope={page.scope}> <FrameworkScope scope={page.scope}>
<AppContainer> <FrameworkScope scope={editor.scope}>
<MainContainer> <AppContainer>
<div className={styles.root}> <MainContainer>
<div className={styles.mainContainer}> <div className={styles.root}>
<ShareHeader <div className={styles.mainContainer}>
pageId={page.id} <ShareHeader
publishMode={publishMode} pageId={page.id}
docCollection={page.blockSuiteDoc.collection} publishMode={publishMode}
/> docCollection={page.blockSuiteDoc.collection}
<Scrollable.Root> />
<Scrollable.Viewport <Scrollable.Root>
className={clsx( <Scrollable.Viewport
'affine-page-viewport', className={clsx(
styles.editorContainer 'affine-page-viewport',
)} styles.editorContainer
> )}
<PageDetailEditor >
isPublic <PageDetailEditor
publishMode={publishMode} isPublic
docCollection={page.blockSuiteDoc.collection} publishMode={publishMode}
pageId={page.id} docCollection={page.blockSuiteDoc.collection}
onLoad={onEditorLoad} pageId={page.id}
/> onLoad={onEditorLoad}
{publishMode === 'page' ? <ShareFooter /> : null} />
</Scrollable.Viewport> {publishMode === 'page' ? <ShareFooter /> : null}
<Scrollable.Scrollbar /> </Scrollable.Viewport>
</Scrollable.Root> <Scrollable.Scrollbar />
{loginStatus !== 'authenticated' ? ( </Scrollable.Root>
<a {loginStatus !== 'authenticated' ? (
href="https://affine.pro" <a
target="_blank" href="https://affine.pro"
className={styles.link} target="_blank"
rel="noreferrer" className={styles.link}
> rel="noreferrer"
<span className={styles.linkText}> >
{t['com.affine.share-page.footer.built-with']()} <span className={styles.linkText}>
</span> {t['com.affine.share-page.footer.built-with']()}
<Logo1Icon fontSize={20} /> </span>
</a> <Logo1Icon fontSize={20} />
) : null} </a>
) : null}
</div>
</div> </div>
</div> </MainContainer>
</MainContainer> <PeekViewManagerModal />
<PeekViewManagerModal /> </AppContainer>
</AppContainer> </FrameworkScope>
</FrameworkScope> </FrameworkScope>
</FrameworkScope> </FrameworkScope>
); );

View File

@@ -17,12 +17,8 @@ export function ShareHeader({
}) { }) {
return ( return (
<div className={styles.header}> <div className={styles.header}>
<EditorModeSwitch isPublic pageId={pageId} publicMode={publishMode} /> <EditorModeSwitch />
<BlocksuiteHeaderTitle <BlocksuiteHeaderTitle docId={pageId} />
docCollection={docCollection}
pageId={pageId}
isPublic={true}
/>
<div className={styles.spacer} /> <div className={styles.spacer} />
<ShareHeaderRightItem <ShareHeaderRightItem
workspaceId={docCollection.id} workspaceId={docCollection.id}

View File

@@ -15,15 +15,10 @@ import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite
import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands'; import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands';
import { useDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title'; import { useDocCollectionPageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import { track } from '@affine/core/mixpanel'; import { EditorService } from '@affine/core/modules/editor';
import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench'; import { ViewIcon, ViewTitle } from '@affine/core/modules/workbench';
import type { Doc } from '@blocksuite/store'; import type { Doc } from '@blocksuite/store';
import { import { useLiveData, useService, type Workspace } from '@toeverything/infra';
DocService,
useLiveData,
useService,
type Workspace,
} from '@toeverything/infra';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom, useAtomValue } from 'jotai';
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
@@ -81,7 +76,7 @@ export function JournalPageHeader({ page, workspace }: PageHeaderProps) {
<Header className={styles.header} ref={containerRef}> <Header className={styles.header} ref={containerRef}>
<ViewTitle title={title} /> <ViewTitle title={title} />
<ViewIcon icon="journal" /> <ViewIcon icon="journal" />
<EditorModeSwitch pageId={page?.id} /> <EditorModeSwitch />
<div className={styles.journalWeekPicker}> <div className={styles.journalWeekPicker}>
<JournalWeekDatePicker <JournalWeekDatePicker
docCollection={workspace.docCollection} docCollection={workspace.docCollection}
@@ -125,22 +120,17 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
}, []); }, []);
const title = useDocCollectionPageTitle(workspace.docCollection, page?.id); const title = useDocCollectionPageTitle(workspace.docCollection, page?.id);
const doc = useService(DocService).doc; const editor = useService(EditorService).editor;
const currentMode = useLiveData(doc.mode$); const currentMode = useLiveData(editor.mode$);
const onEditSave = useCallback(() => {
track.$.header.actions.renameDoc();
}, []);
return ( return (
<Header className={styles.header} ref={containerRef}> <Header className={styles.header} ref={containerRef}>
<ViewTitle title={title} /> <ViewTitle title={title} />
<ViewIcon icon={currentMode ?? 'page'} /> <ViewIcon icon={currentMode ?? 'page'} />
<EditorModeSwitch pageId={page?.id} /> <EditorModeSwitch />
<BlocksuiteHeaderTitle <BlocksuiteHeaderTitle
docId={page.id}
inputHandleRef={titleInputHandleRef} inputHandleRef={titleInputHandleRef}
pageId={page?.id}
docCollection={workspace.docCollection}
onEditSave={onEditSave}
/> />
<div className={styles.iconButtonContainer}> <div className={styles.iconButtonContainer}>
{hideCollect ? null : ( {hideCollect ? null : (

View File

@@ -6,6 +6,8 @@ import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer'; import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import type { Editor } from '@affine/core/modules/editor';
import { EditorService, EditorsService } from '@affine/core/modules/editor';
import { RecentDocsService } from '@affine/core/modules/quicksearch'; import { RecentDocsService } from '@affine/core/modules/quicksearch';
import { ViewService } from '@affine/core/modules/workbench/services/view'; import { ViewService } from '@affine/core/modules/workbench/services/view';
import type { PageRootService } from '@blocksuite/blocks'; import type { PageRootService } from '@blocksuite/blocks';
@@ -30,6 +32,7 @@ import {
GlobalContextService, GlobalContextService,
useLiveData, useLiveData,
useService, useService,
useServices,
WorkspaceService, WorkspaceService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -71,18 +74,37 @@ import { EditorJournalPanel } from './tabs/journal';
import { EditorOutlinePanel } from './tabs/outline'; import { EditorOutlinePanel } from './tabs/outline';
const DetailPageImpl = memo(function DetailPageImpl() { const DetailPageImpl = memo(function DetailPageImpl() {
const workbench = useService(WorkbenchService).workbench; const {
const view = useService(ViewService).view; workbenchService,
viewService,
editorService,
docService,
workspaceService,
globalContextService,
} = useServices({
WorkbenchService,
ViewService,
EditorService,
DocService,
WorkspaceService,
GlobalContextService,
});
const workbench = workbenchService.workbench;
const editor = editorService.editor;
const view = viewService.view;
const workspace = workspaceService.workspace;
const docCollection = workspace.docCollection;
const globalContext = globalContextService.globalContext;
const doc = docService.doc;
const mode = useLiveData(editor.mode$);
const activeSidebarTab = useLiveData(view.activeSidebarTab$); const activeSidebarTab = useLiveData(view.activeSidebarTab$);
const doc = useService(DocService).doc;
const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash)); const isInTrash = useLiveData(doc.meta$.map(meta => meta.trash));
const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper(); const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
const [editor, setEditor] = useState<AffineEditorContainer | null>(null); const [editorContainer, setEditorContainer] =
const workspace = useService(WorkspaceService).workspace; useState<AffineEditorContainer | null>(null);
const globalContext = useService(GlobalContextService).globalContext;
const docCollection = workspace.docCollection;
const mode = useLiveData(doc.mode$);
const isSideBarOpen = useLiveData(workbench.sidebarOpen$); const isSideBarOpen = useLiveData(workbench.sidebarOpen$);
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();
const chatPanelRef = useRef<ChatPanel | null>(null); const chatPanelRef = useRef<ChatPanel | null>(null);
@@ -94,9 +116,9 @@ const DetailPageImpl = memo(function DetailPageImpl() {
useEffect(() => { useEffect(() => {
if (isActiveView) { if (isActiveView) {
setActiveBlockSuiteEditor(editor); setActiveBlockSuiteEditor(editorContainer);
} }
}, [editor, isActiveView, setActiveBlockSuiteEditor]); }, [editorContainer, isActiveView, setActiveBlockSuiteEditor]);
useEffect(() => { useEffect(() => {
const disposable = AIProvider.slots.requestOpenWithChat.on(params => { const disposable = AIProvider.slots.requestOpenWithChat.on(params => {
@@ -152,7 +174,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
return; return;
}, [globalContext, isActiveView, isInTrash]); }, [globalContext, isActiveView, isInTrash]);
useRegisterBlocksuiteEditorCommands(); useRegisterBlocksuiteEditorCommands(editor);
const title = useLiveData(doc.title$); const title = useLiveData(doc.title$);
usePageDocumentTitle(title); usePageDocumentTitle(title);
@@ -218,7 +240,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
); );
} }
setEditor(editor); setEditorContainer(editor);
return () => { return () => {
disposable.dispose(); disposable.dispose();
@@ -236,7 +258,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
}, [workbench, view]); }, [workbench, view]);
return ( return (
<> <FrameworkScope scope={editor.scope}>
<ViewHeader> <ViewHeader>
<DetailPageHeader page={doc.blockSuiteDoc} workspace={workspace} /> <DetailPageHeader page={doc.blockSuiteDoc} workspace={workspace} />
</ViewHeader> </ViewHeader>
@@ -271,7 +293,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
/> />
</Scrollable.Root> </Scrollable.Root>
<EditorOutlineViewer <EditorOutlineViewer
editor={editor} editor={editorContainer}
show={mode === 'page' && !isSideBarOpen} show={mode === 'page' && !isSideBarOpen}
openOutlinePanel={openOutlinePanel} openOutlinePanel={openOutlinePanel}
/> />
@@ -281,7 +303,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
</ViewBody> </ViewBody>
<ViewSidebarTab tabId="chat" icon={<AiIcon />} unmountOnInactive={false}> <ViewSidebarTab tabId="chat" icon={<AiIcon />} unmountOnInactive={false}>
<EditorChatPanel editor={editor} ref={chatPanelRef} /> <EditorChatPanel editor={editorContainer} ref={chatPanelRef} />
</ViewSidebarTab> </ViewSidebarTab>
<ViewSidebarTab tabId="journal" icon={<TodayIcon />}> <ViewSidebarTab tabId="journal" icon={<TodayIcon />}>
@@ -289,16 +311,16 @@ const DetailPageImpl = memo(function DetailPageImpl() {
</ViewSidebarTab> </ViewSidebarTab>
<ViewSidebarTab tabId="outline" icon={<TocIcon />}> <ViewSidebarTab tabId="outline" icon={<TocIcon />}>
<EditorOutlinePanel editor={editor} /> <EditorOutlinePanel editor={editorContainer} />
</ViewSidebarTab> </ViewSidebarTab>
<ViewSidebarTab tabId="frame" icon={<FrameIcon />}> <ViewSidebarTab tabId="frame" icon={<FrameIcon />}>
<EditorFramePanel editor={editor} /> <EditorFramePanel editor={editorContainer} />
</ViewSidebarTab> </ViewSidebarTab>
<GlobalPageHistoryModal /> <GlobalPageHistoryModal />
<PageAIOnboarding /> <PageAIOnboarding />
</> </FrameworkScope>
); );
}); });
@@ -310,6 +332,7 @@ export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
const docRecord = useLiveData(docRecordList.doc$(pageId)); const docRecord = useLiveData(docRecordList.doc$(pageId));
const [doc, setDoc] = useState<Doc | null>(null); const [doc, setDoc] = useState<Doc | null>(null);
const [editor, setEditor] = useState<Editor | null>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!docRecord) { if (!docRecord) {
@@ -322,6 +345,19 @@ export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
}; };
}, [docRecord, docsService, pageId]); }, [docRecord, docsService, pageId]);
useLayoutEffect(() => {
if (!doc) {
return;
}
const editor = doc.scope
.get(EditorsService)
.createEditor(doc.getPrimaryMode() || 'page');
setEditor(editor);
return () => {
editor.dispose();
};
}, [doc]);
// set sync engine priority target // set sync engine priority target
useEffect(() => { useEffect(() => {
currentWorkspace.engine.doc.setPriority(pageId, 10); currentWorkspace.engine.doc.setPriority(pageId, 10);
@@ -346,13 +382,15 @@ export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
return <PageNotFound noPermission />; return <PageNotFound noPermission />;
} }
if (!doc) { if (!doc || !editor) {
return <PageDetailSkeleton key="current-page-is-null" />; return <PageDetailSkeleton key="current-page-is-null" />;
} }
return ( return (
<FrameworkScope scope={doc.scope}> <FrameworkScope scope={doc.scope}>
<DetailPageImpl /> <FrameworkScope scope={editor.scope}>
<DetailPageImpl />
</FrameworkScope>
</FrameworkScope> </FrameworkScope>
); );
}; };

View File

@@ -48,7 +48,7 @@ interface PageItemProps
right?: ReactNode; right?: ReactNode;
} }
const PageItem = ({ docRecord, right, className, ...attrs }: PageItemProps) => { const PageItem = ({ docRecord, right, className, ...attrs }: PageItemProps) => {
const mode = useLiveData(docRecord.mode$); const mode = useLiveData(docRecord.primaryMode$);
const workspace = useService(WorkspaceService).workspace; const workspace = useService(WorkspaceService).workspace;
const title = useDocCollectionPageTitle( const title = useDocCollectionPageTitle(
workspace.docCollection, workspace.docCollection,

View File

@@ -1,9 +1,10 @@
import { EditorService } from '@affine/core/modules/editor';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { useViewPosition } from '@affine/core/modules/workbench/view/use-view-position'; import { useViewPosition } from '@affine/core/modules/workbench/view/use-view-position';
import { DocService, useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
export const useDetailPageHeaderResponsive = (availableWidth: number) => { export const useDetailPageHeaderResponsive = (availableWidth: number) => {
const mode = useLiveData(useService(DocService).doc.mode$); const mode = useLiveData(useService(EditorService).editor.mode$);
const workbench = useService(WorkbenchService).workbench; const workbench = useService(WorkbenchService).workbench;
const viewPosition = useViewPosition(); const viewPosition = useViewPosition();