fix(core): title could not be changed when creating a new doc (#8203)

Before change, the title could not be modified from outside the editor without refreshing:

https://github.com/user-attachments/assets/536acba1-4e31-418a-bc1a-8578e3128bba

after:

https://github.com/user-attachments/assets/30a4b270-b8b1-4787-acef-0ab2a72a8f74
This commit is contained in:
JimmFly
2024-09-12 07:55:22 +00:00
parent cc5a6e6d40
commit 2cba8a4ccd
27 changed files with 138 additions and 145 deletions
@@ -39,11 +39,11 @@ export class DocRecord extends Entity<{ id: string }> {
} }
moveToTrash() { moveToTrash() {
return this.setMeta({ trash: true }); return this.setMeta({ trash: true, trashDate: Date.now() });
} }
restoreFromTrash() { restoreFromTrash() {
return this.setMeta({ trash: false }); return this.setMeta({ trash: false, trashDate: undefined });
} }
title$ = this.meta$.map(meta => meta.title ?? ''); title$ = this.meta$.map(meta => meta.title ?? '');
@@ -35,6 +35,9 @@ export class Workspace extends Entity {
idGenerator: () => nanoid(), idGenerator: () => nanoid(),
schema: globalBlockSuiteSchema, schema: globalBlockSuiteSchema,
}); });
this._docCollection.slots.docCreated.on(id => {
this.engine.doc.markAsReady(id);
});
} }
return this._docCollection; return this._docCollection;
} }
@@ -62,7 +62,7 @@ const DetailPageImpl = () => {
const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper(); const { openPage, jumpToPageBlock, jumpToTag } = useNavigateHelper();
const editorContainer = useLiveData(editor.editorContainer$); const editorContainer = useLiveData(editor.editorContainer$);
const { setDocReadonly } = useDocMetaHelper(workspace.docCollection); const { setDocReadonly } = useDocMetaHelper();
// TODO(@eyhn): remove jotai here // TODO(@eyhn): remove jotai here
const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor(); const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor();
@@ -22,9 +22,7 @@ import { MobileSignInModal } from '../views/sign-in/modal';
export function MobileCurrentWorkspaceModals() { export function MobileCurrentWorkspaceModals() {
const currentWorkspace = useService(WorkspaceService).workspace; const currentWorkspace = useService(WorkspaceService).workspace;
const { trashModal, setTrashModal, handleOnConfirm } = useTrashModalHelper( const { trashModal, setTrashModal, handleOnConfirm } = useTrashModalHelper();
currentWorkspace.docCollection
);
const deletePageTitles = trashModal.pageTitles; const deletePageTitles = trashModal.pageTitles;
const trashConfirmOpen = trashModal.open; const trashConfirmOpen = trashModal.open;
const onTrashConfirmOpenChange = useCallback( const onTrashConfirmOpenChange = useCallback(
@@ -9,7 +9,7 @@ import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { import {
initEmptyPage, DocsService,
useLiveData, useLiveData,
useService, useService,
WorkspacesService, WorkspacesService,
@@ -179,6 +179,7 @@ export const CreateWorkspaceModal = ({
const [step, setStep] = useState<CreateWorkspaceStep>(); const [step, setStep] = useState<CreateWorkspaceStep>();
const t = useI18n(); const t = useI18n();
const workspacesService = useService(WorkspacesService); const workspacesService = useService(WorkspacesService);
const docsService = useService(DocsService);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// TODO(@Peng): maybe refactor using xstate? // TODO(@Peng): maybe refactor using xstate?
@@ -242,9 +243,8 @@ export const CreateWorkspaceModal = ({
async workspace => { async workspace => {
workspace.meta.initialize(); workspace.meta.initialize();
workspace.meta.setName(name); workspace.meta.setName(name);
const page = workspace.createDoc(); const page = docsService.createDoc();
defaultDocId = page.id; defaultDocId = page.id;
initEmptyPage(page);
} }
); );
onCreate(id, defaultDocId); onCreate(id, defaultDocId);
@@ -252,7 +252,7 @@ export const CreateWorkspaceModal = ({
setLoading(false); setLoading(false);
}, },
[loading, onCreate, workspacesService] [docsService, loading, onCreate, workspacesService]
); );
const onOpenChange = useCallback( const onOpenChange = useCallback(
@@ -39,7 +39,6 @@ export const EmptyDocs = ({
undefined, undefined,
isNewTabTrigger(e) ? 'new-tab' : true isNewTabTrigger(e) ? 'new-tab' : true
); );
doc.load();
if (tag) tag.tag(doc.id); if (tag) tag.tag(doc.id);
}, },
@@ -239,7 +239,7 @@ export const useRestorePage = (
const { trigger: recover, isMutating } = useMutation({ const { trigger: recover, isMutating } = useMutation({
mutation: recoverDocMutation, mutation: recoverDocMutation,
}); });
const { getDocMeta, setDocTitle } = useDocMetaHelper(docCollection); const { getDocMeta, setDocTitle } = useDocMetaHelper();
const onRestore = useMemo(() => { const onRestore = useMemo(() => {
return async (version: string, update: Uint8Array) => { return async (version: string, update: Uint8Array) => {
@@ -95,7 +95,7 @@ export function AffinePageReference({
mode?: DocMode; mode?: DocMode;
params?: URLSearchParams; params?: URLSearchParams;
}) { }) {
const pageMetaHelper = useDocMetaHelper(docCollection); const pageMetaHelper = useDocMetaHelper();
const journalHelper = useJournalHelper(docCollection); const journalHelper = useJournalHelper(docCollection);
const t = useI18n(); const t = useI18n();
@@ -120,7 +120,7 @@ export const AppearanceSettings = () => {
) : null} ) : null}
{runtimeConfig.enableThemeEditor ? <ThemeEditorSetting /> : null} {runtimeConfig.enableThemeEditor ? <ThemeEditorSetting /> : null}
</SettingWrapper> </SettingWrapper>
{/* //TODO(@JimmFly): remove Page component when stable release */} {/* // TODO(@JimmFly): remove Page component when stable release */}
<Page /> <Page />
{runtimeConfig.enableNewSettingUnstableApi ? ( {runtimeConfig.enableNewSettingUnstableApi ? (
<SettingWrapper title={t['com.affine.appearanceSettings.date.title']()}> <SettingWrapper title={t['com.affine.appearanceSettings.date.title']()}>
@@ -459,7 +459,7 @@ export const General = () => {
<FontFamilySettings /> <FontFamilySettings />
<CustomFontFamilySettings /> <CustomFontFamilySettings />
<NewDocDefaultModeSettings /> <NewDocDefaultModeSettings />
{/* //TODO(@akumatus): implement these settings {/* // TODO(@akumatus): implement these settings
<DeFaultCodeBlockSettings /> <DeFaultCodeBlockSettings />
<SpellCheckSettings /> */} <SpellCheckSettings /> */}
</SettingWrapper> </SettingWrapper>
@@ -81,12 +81,12 @@ export const PageHeaderMenuButton = ({
const { favorite, toggleFavorite } = useFavorite(pageId); const { favorite, toggleFavorite } = useFavorite(pageId);
const { duplicate } = useBlockSuiteMetaHelper(docCollection); const { duplicate } = useBlockSuiteMetaHelper();
const { importFile } = usePageHelper(docCollection); const { importFile } = usePageHelper(docCollection);
const { setTrashModal } = useTrashModalHelper(docCollection); const { setTrashModal } = useTrashModalHelper();
const [isEditing, setEditing] = useState(!page.readonly); const [isEditing, setEditing] = useState(!page.readonly);
const { setDocReadonly } = useDocMetaHelper(docCollection); const { setDocReadonly } = useDocMetaHelper();
const view = useService(ViewService).view; const view = useService(ViewService).view;
@@ -1,8 +1,7 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import { useDocCollectionHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import type { DocMode } from '@blocksuite/blocks'; import type { DocMode } from '@blocksuite/blocks';
import { DocsService, initEmptyPage, useServices } from '@toeverything/infra'; import { DocsService, useServices } from '@toeverything/infra';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { DocCollection } from '../../../shared'; import type { DocCollection } from '../../../shared';
@@ -13,13 +12,11 @@ export const usePageHelper = (docCollection: DocCollection) => {
WorkbenchService, WorkbenchService,
}); });
const workbench = workbenchService.workbench; const workbench = workbenchService.workbench;
const { createDoc } = useDocCollectionHelper(docCollection);
const docRecordList = docsService.list; const docRecordList = docsService.list;
const createPageAndOpen = useCallback( const createPageAndOpen = useCallback(
(mode?: DocMode, open?: boolean | 'new-tab') => { (mode?: DocMode, open?: boolean | 'new-tab') => {
const page = createDoc(); const page = docsService.createDoc();
initEmptyPage(page);
if (mode) { if (mode) {
docRecordList.doc$(page.id).value?.setPrimaryMode(mode); docRecordList.doc$(page.id).value?.setPrimaryMode(mode);
} }
@@ -30,7 +27,7 @@ export const usePageHelper = (docCollection: DocCollection) => {
}); });
return page; return page;
}, },
[createDoc, docRecordList, workbench] [docRecordList, docsService, workbench]
); );
const createEdgelessAndOpen = useCallback( const createEdgelessAndOpen = useCallback(
@@ -19,7 +19,7 @@ import {
SearchIcon, SearchIcon,
ViewLayersIcon, ViewLayersIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import type { Doc as BlockSuiteDoc } from '@blocksuite/store'; import type { DocRecord } from '@toeverything/infra';
import { import {
useLiveData, useLiveData,
useService, useService,
@@ -122,7 +122,7 @@ export const CollectionPageListHeader = ({
const { openConfirmModal } = useConfirmModal(); const { openConfirmModal } = useConfirmModal();
const createAndAddDocument = useCallback( const createAndAddDocument = useCallback(
(createDocumentFn: () => BlockSuiteDoc) => { (createDocumentFn: () => DocRecord) => {
const newDoc = createDocumentFn(); const newDoc = createDocumentFn();
collectionService.addPageToCollection(collection.id, newDoc.id); collectionService.addPageToCollection(collection.id, newDoc.id);
}, },
@@ -130,7 +130,7 @@ export const CollectionPageListHeader = ({
); );
const onConfirmAddDocument = useCallback( const onConfirmAddDocument = useCallback(
(createDocumentFn: () => BlockSuiteDoc) => { (createDocumentFn: () => DocRecord) => {
openConfirmModal({ openConfirmModal({
title: t['com.affine.collection.add-doc.confirm.title'](), title: t['com.affine.collection.add-doc.confirm.title'](),
description: t['com.affine.collection.add-doc.confirm.description'](), description: t['com.affine.collection.add-doc.confirm.description'](),
@@ -122,7 +122,7 @@ export const VirtualizedPageList = ({
return <PageListHeader />; return <PageListHeader />;
}, [collection, currentWorkspace.id, tag]); }, [collection, currentWorkspace.id, tag]);
const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection); const { setTrashModal } = useTrashModalHelper();
const handleMultiDelete = useCallback(() => { const handleMultiDelete = useCallback(() => {
if (filteredSelectedPageIds.length === 0) { if (filteredSelectedPageIds.length === 0) {
@@ -80,10 +80,10 @@ export const PageOperationCell = ({
featureFlagService.flags.enable_multi_view.$ featureFlagService.flags.enable_multi_view.$
); );
const currentWorkspace = workspaceService.workspace; const currentWorkspace = workspaceService.workspace;
const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection); const { setTrashModal } = useTrashModalHelper();
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc')); const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
const workbench = workbenchService.workbench; const workbench = workbenchService.workbench;
const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection); const { duplicate } = useBlockSuiteMetaHelper();
const blocksuiteDoc = currentWorkspace.docCollection.getDoc(page.id); const blocksuiteDoc = currentWorkspace.docCollection.getDoc(page.id);
const [openInfoModal, setOpenInfoModal] = useState(false); const [openInfoModal, setOpenInfoModal] = useState(false);
@@ -93,7 +93,7 @@ export const PageOperationCell = ({
}, []); }, []);
const onDisablePublicSharing = useCallback(() => { const onDisablePublicSharing = useCallback(() => {
//TODO(@EYHN): implement disable public sharing // TODO(@EYHN): implement disable public sharing
toast('Successfully disabled', { toast('Successfully disabled', {
portal: document.body, portal: document.body,
}); });
@@ -23,7 +23,7 @@ const TagListTitleCell = ({
> >
{title || t['Untitled']()} {title || t['Untitled']()}
</div> </div>
{/* //TODO(@EYHN): when indexer is ready, add this back {/* // TODO(@EYHN): when indexer is ready, add this back
<div <div
data-testid="page-list-item-preview-text" data-testid="page-list-item-preview-text"
className={styles.titleCellPreview} className={styles.titleCellPreview}
@@ -18,8 +18,7 @@ import { VirtualizedList } from './virtualized-list';
export const VirtualizedTrashList = () => { export const VirtualizedTrashList = () => {
const currentWorkspace = useService(WorkspaceService).workspace; const currentWorkspace = useService(WorkspaceService).workspace;
const docCollection = currentWorkspace.docCollection; const docCollection = currentWorkspace.docCollection;
const { restoreFromTrash, permanentlyDeletePage } = const { restoreFromTrash, permanentlyDeletePage } = useBlockSuiteMetaHelper();
useBlockSuiteMetaHelper(docCollection);
const pageMetas = useBlockSuiteDocMeta(docCollection); const pageMetas = useBlockSuiteDocMeta(docCollection);
const filteredPageMetas = useFilteredPageMetas(pageMetas, { const filteredPageMetas = useFilteredPageMetas(pageMetas, {
trash: true, trash: true,
@@ -19,7 +19,7 @@ export const TrashPageFooter = () => {
const t = useI18n(); const t = useI18n();
const { appSettings } = useAppSettingHelper(); const { appSettings } = useAppSettingHelper();
const { jumpToSubPath } = useNavigateHelper(); const { jumpToSubPath } = useNavigateHelper();
const { restoreFromTrash } = useBlockSuiteMetaHelper(docCollection); const { restoreFromTrash } = useBlockSuiteMetaHelper();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const hintText = t['com.affine.cmdk.affine.editor.trash-footer-hint'](); const hintText = t['com.affine.cmdk.affine.editor.trash-footer-hint']();
@@ -99,11 +99,7 @@ export const RootAppSidebar = (): ReactElement => {
const onClickNewPage = useAsyncCallback( const onClickNewPage = useAsyncCallback(
async (e?: MouseEvent) => { async (e?: MouseEvent) => {
const page = pageHelper.createPage( pageHelper.createPage(undefined, isNewTabTrigger(e) ? 'new-tab' : true);
undefined,
isNewTabTrigger(e) ? 'new-tab' : true
);
page.load();
track.$.navigationPanel.$.createDoc(); track.$.navigationPanel.$.createDoc();
}, },
[pageHelper] [pageHelper]
@@ -1,86 +1,59 @@
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { useDocCollectionHelper } from '@affine/core/hooks/use-block-suite-workspace-helper'; import { useDocCollectionHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
import { CollectionService } from '@affine/core/modules/collection';
import type { DocMode } from '@blocksuite/blocks'; import type { DocMode } from '@blocksuite/blocks';
import { DocsService, useService } from '@toeverything/infra'; import { DocsService, useService, WorkspaceService } from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import type { DocCollection } from '../../shared';
import { useNavigateHelper } from '../use-navigate-helper'; import { useNavigateHelper } from '../use-navigate-helper';
export function useBlockSuiteMetaHelper(docCollection: DocCollection) { export function useBlockSuiteMetaHelper() {
const { setDocMeta, getDocMeta, setDocReadonly, setDocTitle } = const workspace = useService(WorkspaceService).workspace;
useDocMetaHelper(docCollection); const { setDocMeta, getDocMeta, setDocTitle, setDocReadonly } =
const { createDoc } = useDocCollectionHelper(docCollection); useDocMetaHelper();
const { createDoc } = useDocCollectionHelper(workspace.docCollection);
const { openPage } = useNavigateHelper(); const { openPage } = useNavigateHelper();
const collectionService = useService(CollectionService); const docRecordList = useService(DocsService).list;
const pageRecordList = useService(DocsService).list;
// TODO-Doma // TODO-Doma
// "Remove" may cause ambiguity here. Consider renaming as "moveToTrash". // "Remove" may cause ambiguity here. Consider renaming as "moveToTrash".
const removeToTrash = useCallback( const removeToTrash = useCallback(
(pageId: string) => { (docId: string) => {
setDocMeta(pageId, { const docRecord = docRecordList.doc$(docId).value;
trash: true, if (docRecord) {
trashDate: Date.now(), docRecord.moveToTrash();
}); setDocReadonly(docId, true);
setDocReadonly(pageId, true); }
collectionService.deletePagesFromCollections([pageId]);
}, },
[collectionService, setDocMeta, setDocReadonly] [docRecordList, setDocReadonly]
); );
const restoreFromTrash = useCallback( const restoreFromTrash = useCallback(
(pageId: string) => { (docId: string) => {
setDocMeta(pageId, { const docRecord = docRecordList.doc$(docId).value;
trash: false, if (docRecord) {
trashDate: undefined, docRecord.restoreFromTrash();
}); setDocReadonly(docId, false);
setDocReadonly(pageId, false); }
}, },
[setDocMeta, setDocReadonly] [docRecordList, setDocReadonly]
); );
const permanentlyDeletePage = useCallback( const permanentlyDeletePage = useCallback(
(pageId: string) => { (pageId: string) => {
docCollection.removeDoc(pageId); workspace.docCollection.removeDoc(pageId);
}, },
[docCollection] [workspace]
);
/**
* see {@link useBlockSuiteWorkspacePageIsPublic}
*/
const publicPage = useCallback(
(pageId: string) => {
setDocMeta(pageId, {
isPublic: true,
});
},
[setDocMeta]
);
/**
* see {@link useBlockSuiteWorkspacePageIsPublic}
*/
const cancelPublicPage = useCallback(
(pageId: string) => {
setDocMeta(pageId, {
isPublic: false,
});
},
[setDocMeta]
); );
const duplicate = useAsyncCallback( const duplicate = useAsyncCallback(
async (pageId: string, openPageAfterDuplication: boolean = true) => { async (pageId: string, openPageAfterDuplication: boolean = true) => {
const currentPagePrimaryMode = const currentPagePrimaryMode =
pageRecordList.doc$(pageId).value?.primaryMode$.value; docRecordList.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 = workspace.docCollection.getDoc(pageId);
newPage.load(); newPage.load();
if (!currentPageMeta || !currentPage) { if (!currentPageMeta || !currentPage) {
@@ -95,33 +68,31 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
}); });
const lastDigitRegex = /\((\d+)\)$/; const lastDigitRegex = /\((\d+)\)$/;
const match = currentPageMeta.title.match(lastDigitRegex); const match = currentPageMeta?.title?.match(lastDigitRegex);
const newNumber = match ? parseInt(match[1], 10) + 1 : 1; const newNumber = match ? parseInt(match[1], 10) + 1 : 1;
const newPageTitle = const newPageTitle =
currentPageMeta.title.replace(lastDigitRegex, '') + `(${newNumber})`; currentPageMeta?.title?.replace(lastDigitRegex, '') + `(${newNumber})`;
pageRecordList docRecordList
.doc$(newPage.id) .doc$(newPage.id)
.value?.setPrimaryMode(currentPagePrimaryMode || ('page' as DocMode)); .value?.setPrimaryMode(currentPagePrimaryMode || ('page' as DocMode));
setDocTitle(newPage.id, newPageTitle); setDocTitle(newPage.id, newPageTitle);
openPageAfterDuplication && openPage(docCollection.id, newPage.id); openPageAfterDuplication &&
openPage(workspace.docCollection.id, newPage.id);
}, },
[ [
docCollection, docRecordList,
createDoc,
getDocMeta, getDocMeta,
openPage, createDoc,
pageRecordList, workspace.docCollection,
setDocMeta, setDocMeta,
setDocTitle, setDocTitle,
openPage,
] ]
); );
return { return {
publicPage,
cancelPublicPage,
removeToTrash, removeToTrash,
restoreFromTrash, restoreFromTrash,
permanentlyDeletePage, permanentlyDeletePage,
@@ -30,7 +30,6 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
const mode = useLiveData(editor.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 favAdapter = useService(CompatibleFavoriteItemsAdapter); const favAdapter = useService(CompatibleFavoriteItemsAdapter);
const favorite = useLiveData(favAdapter.isFavorite$(docId, 'doc')); const favorite = useLiveData(favAdapter.isFavorite$(docId, 'doc'));
@@ -50,9 +49,9 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
setInfoModalState(true); setInfoModalState(true);
}, [setInfoModalState]); }, [setInfoModalState]);
const { duplicate } = useBlockSuiteMetaHelper(docCollection); const { duplicate } = useBlockSuiteMetaHelper();
const exportHandler = useExportPage(); const exportHandler = useExportPage();
const { setTrashModal } = useTrashModalHelper(docCollection); const { setTrashModal } = useTrashModalHelper();
const onClickDelete = useCallback( const onClickDelete = useCallback(
(title: string) => { (title: string) => {
setTrashModal({ setTrashModal({
@@ -1,18 +1,16 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import type { DocCollection } from '@blocksuite/store';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { trashModalAtom } from '../../atoms/trash-modal'; import { trashModalAtom } from '../../atoms/trash-modal';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
export function useTrashModalHelper(docCollection: DocCollection) { export function useTrashModalHelper() {
const t = useI18n(); const t = useI18n();
const [trashModal, setTrashModal] = useAtom(trashModalAtom); const [trashModal, setTrashModal] = useAtom(trashModalAtom);
const { pageIds } = trashModal; const { pageIds } = trashModal;
const { removeToTrash } = useBlockSuiteMetaHelper(docCollection); const { removeToTrash } = useBlockSuiteMetaHelper();
const handleOnConfirm = useCallback(() => { const handleOnConfirm = useCallback(() => {
pageIds.forEach(pageId => { pageIds.forEach(pageId => {
removeToTrash(pageId); removeToTrash(pageId);
@@ -1,8 +1,8 @@
import type { RootBlockModel } from '@blocksuite/blocks';
import { assertExists } from '@blocksuite/global/utils';
import type { DocCollection, DocMeta } from '@blocksuite/store'; import type { DocCollection, DocMeta } from '@blocksuite/store';
import { useMemo } from 'react'; import { DocsService, useService, WorkspaceService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import { useAsyncCallback } from './affine-async-hooks';
import { useAllBlockSuiteDocMeta } from './use-all-block-suite-page-meta'; import { useAllBlockSuiteDocMeta } from './use-all-block-suite-page-meta';
import { useJournalHelper } from './use-journal'; import { useJournalHelper } from './use-journal';
@@ -23,34 +23,54 @@ export function useBlockSuiteDocMeta(docCollection: DocCollection) {
); );
} }
export function useDocMetaHelper(docCollection: DocCollection) { export function useDocMetaHelper() {
const workspaceService = useService(WorkspaceService);
const docsService = useService(DocsService);
const setDocTitle = useAsyncCallback(
async (docId: string, newTitle: string) => {
await docsService.changeDocTitle(docId, newTitle);
},
[docsService]
);
const setDocMeta = useCallback(
(docId: string, docMeta: Partial<DocMeta>) => {
const doc = docsService.list.doc$(docId).value;
if (doc) {
doc.setMeta(docMeta);
}
},
[docsService]
);
const getDocMeta = useCallback(
(docId: string) => {
const doc = docsService.list.doc$(docId).value;
return doc?.meta$.value;
},
[docsService]
);
const setDocReadonly = useCallback(
(docId: string, readonly: boolean) => {
const doc = workspaceService.workspace.docCollection.getDoc(docId);
if (doc?.blockCollection) {
workspaceService.workspace.docCollection.awarenessStore.setReadonly(
doc.blockCollection,
readonly
);
}
},
[workspaceService]
);
return useMemo( return useMemo(
() => ({ () => ({
setDocTitle: (docId: string, newTitle: string) => { setDocTitle,
const page = docCollection.getDoc(docId); setDocMeta,
assertExists(page); getDocMeta,
const pageBlock = page setDocReadonly,
.getBlockByFlavour('affine:page')
.at(0) as RootBlockModel;
assertExists(pageBlock);
page.transact(() => {
pageBlock.title.delete(0, pageBlock.title.length);
pageBlock.title.insert(newTitle, 0);
});
docCollection.meta.setDocMeta(docId, { title: newTitle });
},
setDocReadonly: (docId: string, readonly: boolean) => {
const page = docCollection.getDoc(docId);
assertExists(page);
page.awarenessStore.setReadonly(page.blockCollection, readonly);
},
setDocMeta: (docId: string, docMeta: Partial<DocMeta>) => {
docCollection.meta.setDocMeta(docId, docMeta);
},
getDocMeta: (docId: string) => {
return docCollection.meta.getDocMeta(docId);
},
}), }),
[docCollection] [getDocMeta, setDocMeta, setDocReadonly, setDocTitle]
); );
} }
@@ -7,6 +7,7 @@ import {
} from '@affine/component'; } from '@affine/component';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { IsFavoriteIcon } from '@affine/core/components/pure/icons'; import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-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 { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties'; import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
@@ -14,6 +15,7 @@ import { WorkbenchService } from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { import {
DeleteIcon, DeleteIcon,
DuplicateIcon,
InformationIcon, InformationIcon,
LinkedPageIcon, LinkedPageIcon,
OpenInNewIcon, OpenInNewIcon,
@@ -69,6 +71,11 @@ export const useExplorerDocNodeOperations = (
}, [docId, compatibleFavoriteItemsAdapter]) }, [docId, compatibleFavoriteItemsAdapter])
); );
const { duplicate } = useBlockSuiteMetaHelper();
const handleDuplicate = useCallback(() => {
duplicate(docId, true);
track.$.navigationPanel.docs.createDoc();
}, [docId, duplicate]);
const handleOpenInfoModal = useCallback(() => { const handleOpenInfoModal = useCallback(() => {
track.$.docInfoPanel.$.open(); track.$.docInfoPanel.$.open();
options.openInfoModal(); options.openInfoModal();
@@ -173,6 +180,14 @@ export const useExplorerDocNodeOperations = (
</MenuItem> </MenuItem>
), ),
}, },
{
index: 99,
view: (
<MenuItem prefixIcon={<DuplicateIcon />} onClick={handleDuplicate}>
{t['com.affine.header.option.duplicate']()}
</MenuItem>
),
},
{ {
index: 99, index: 99,
view: ( view: (
@@ -230,6 +245,7 @@ export const useExplorerDocNodeOperations = (
enableMultiView, enableMultiView,
favorite, favorite,
handleAddLinkedPage, handleAddLinkedPage,
handleDuplicate,
handleMoveToTrash, handleMoveToTrash,
handleOpenInNewTab, handleOpenInNewTab,
handleOpenInSplitView, handleOpenInSplitView,
@@ -89,7 +89,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
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);
const { setDocReadonly } = useDocMetaHelper(workspace.docCollection); const { setDocReadonly } = useDocMetaHelper();
const isActiveView = useIsActiveView(); const isActiveView = useIsActiveView();
// TODO(@eyhn): remove jotai here // TODO(@eyhn): remove jotai here
@@ -301,9 +301,8 @@ const ConflictList = ({
className, className,
...attrs ...attrs
}: ConflictListProps) => { }: ConflictListProps) => {
const workspace = useService(WorkspaceService).workspace;
const currentDoc = useService(DocService).doc; const currentDoc = useService(DocService).doc;
const { setTrashModal } = useTrashModalHelper(workspace.docCollection); const { setTrashModal } = useTrashModalHelper();
const handleOpenTrashModal = useCallback( const handleOpenTrashModal = useCallback(
(docRecord: DocRecord) => { (docRecord: DocRecord) => {
@@ -91,9 +91,7 @@ export const Setting = () => {
export function CurrentWorkspaceModals() { export function CurrentWorkspaceModals() {
const currentWorkspace = useService(WorkspaceService).workspace; const currentWorkspace = useService(WorkspaceService).workspace;
const { trashModal, setTrashModal, handleOnConfirm } = useTrashModalHelper( const { trashModal, setTrashModal, handleOnConfirm } = useTrashModalHelper();
currentWorkspace.docCollection
);
const deletePageTitles = trashModal.pageTitles; const deletePageTitles = trashModal.pageTitles;
const trashConfirmOpen = trashModal.open; const trashConfirmOpen = trashModal.open;
const onTrashConfirmOpenChange = useCallback( const onTrashConfirmOpenChange = useCallback(