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

View File

@@ -9,7 +9,7 @@ import { apis } from '@affine/electron-api';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useI18n } from '@affine/i18n';
import {
initEmptyPage,
DocsService,
useLiveData,
useService,
WorkspacesService,
@@ -179,6 +179,7 @@ export const CreateWorkspaceModal = ({
const [step, setStep] = useState<CreateWorkspaceStep>();
const t = useI18n();
const workspacesService = useService(WorkspacesService);
const docsService = useService(DocsService);
const [loading, setLoading] = useState(false);
// TODO(@Peng): maybe refactor using xstate?
@@ -242,9 +243,8 @@ export const CreateWorkspaceModal = ({
async workspace => {
workspace.meta.initialize();
workspace.meta.setName(name);
const page = workspace.createDoc();
const page = docsService.createDoc();
defaultDocId = page.id;
initEmptyPage(page);
}
);
onCreate(id, defaultDocId);
@@ -252,7 +252,7 @@ export const CreateWorkspaceModal = ({
setLoading(false);
},
[loading, onCreate, workspacesService]
[docsService, loading, onCreate, workspacesService]
);
const onOpenChange = useCallback(

View File

@@ -39,7 +39,6 @@ export const EmptyDocs = ({
undefined,
isNewTabTrigger(e) ? 'new-tab' : true
);
doc.load();
if (tag) tag.tag(doc.id);
},

View File

@@ -239,7 +239,7 @@ export const useRestorePage = (
const { trigger: recover, isMutating } = useMutation({
mutation: recoverDocMutation,
});
const { getDocMeta, setDocTitle } = useDocMetaHelper(docCollection);
const { getDocMeta, setDocTitle } = useDocMetaHelper();
const onRestore = useMemo(() => {
return async (version: string, update: Uint8Array) => {

View File

@@ -95,7 +95,7 @@ export function AffinePageReference({
mode?: DocMode;
params?: URLSearchParams;
}) {
const pageMetaHelper = useDocMetaHelper(docCollection);
const pageMetaHelper = useDocMetaHelper();
const journalHelper = useJournalHelper(docCollection);
const t = useI18n();

View File

@@ -120,7 +120,7 @@ export const AppearanceSettings = () => {
) : null}
{runtimeConfig.enableThemeEditor ? <ThemeEditorSetting /> : null}
</SettingWrapper>
{/* //TODO(@JimmFly): remove Page component when stable release */}
{/* // TODO(@JimmFly): remove Page component when stable release */}
<Page />
{runtimeConfig.enableNewSettingUnstableApi ? (
<SettingWrapper title={t['com.affine.appearanceSettings.date.title']()}>

View File

@@ -459,7 +459,7 @@ export const General = () => {
<FontFamilySettings />
<CustomFontFamilySettings />
<NewDocDefaultModeSettings />
{/* //TODO(@akumatus): implement these settings
{/* // TODO(@akumatus): implement these settings
<DeFaultCodeBlockSettings />
<SpellCheckSettings /> */}
</SettingWrapper>

View File

@@ -81,12 +81,12 @@ export const PageHeaderMenuButton = ({
const { favorite, toggleFavorite } = useFavorite(pageId);
const { duplicate } = useBlockSuiteMetaHelper(docCollection);
const { duplicate } = useBlockSuiteMetaHelper();
const { importFile } = usePageHelper(docCollection);
const { setTrashModal } = useTrashModalHelper(docCollection);
const { setTrashModal } = useTrashModalHelper();
const [isEditing, setEditing] = useState(!page.readonly);
const { setDocReadonly } = useDocMetaHelper(docCollection);
const { setDocReadonly } = useDocMetaHelper();
const view = useService(ViewService).view;

View File

@@ -1,8 +1,7 @@
import { toast } from '@affine/component';
import { useDocCollectionHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
import { WorkbenchService } from '@affine/core/modules/workbench';
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 type { DocCollection } from '../../../shared';
@@ -13,13 +12,11 @@ export const usePageHelper = (docCollection: DocCollection) => {
WorkbenchService,
});
const workbench = workbenchService.workbench;
const { createDoc } = useDocCollectionHelper(docCollection);
const docRecordList = docsService.list;
const createPageAndOpen = useCallback(
(mode?: DocMode, open?: boolean | 'new-tab') => {
const page = createDoc();
initEmptyPage(page);
const page = docsService.createDoc();
if (mode) {
docRecordList.doc$(page.id).value?.setPrimaryMode(mode);
}
@@ -30,7 +27,7 @@ export const usePageHelper = (docCollection: DocCollection) => {
});
return page;
},
[createDoc, docRecordList, workbench]
[docRecordList, docsService, workbench]
);
const createEdgelessAndOpen = useCallback(

View File

@@ -19,7 +19,7 @@ import {
SearchIcon,
ViewLayersIcon,
} from '@blocksuite/icons/rc';
import type { Doc as BlockSuiteDoc } from '@blocksuite/store';
import type { DocRecord } from '@toeverything/infra';
import {
useLiveData,
useService,
@@ -122,7 +122,7 @@ export const CollectionPageListHeader = ({
const { openConfirmModal } = useConfirmModal();
const createAndAddDocument = useCallback(
(createDocumentFn: () => BlockSuiteDoc) => {
(createDocumentFn: () => DocRecord) => {
const newDoc = createDocumentFn();
collectionService.addPageToCollection(collection.id, newDoc.id);
},
@@ -130,7 +130,7 @@ export const CollectionPageListHeader = ({
);
const onConfirmAddDocument = useCallback(
(createDocumentFn: () => BlockSuiteDoc) => {
(createDocumentFn: () => DocRecord) => {
openConfirmModal({
title: t['com.affine.collection.add-doc.confirm.title'](),
description: t['com.affine.collection.add-doc.confirm.description'](),

View File

@@ -122,7 +122,7 @@ export const VirtualizedPageList = ({
return <PageListHeader />;
}, [collection, currentWorkspace.id, tag]);
const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection);
const { setTrashModal } = useTrashModalHelper();
const handleMultiDelete = useCallback(() => {
if (filteredSelectedPageIds.length === 0) {

View File

@@ -80,10 +80,10 @@ export const PageOperationCell = ({
featureFlagService.flags.enable_multi_view.$
);
const currentWorkspace = workspaceService.workspace;
const { setTrashModal } = useTrashModalHelper(currentWorkspace.docCollection);
const { setTrashModal } = useTrashModalHelper();
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
const workbench = workbenchService.workbench;
const { duplicate } = useBlockSuiteMetaHelper(currentWorkspace.docCollection);
const { duplicate } = useBlockSuiteMetaHelper();
const blocksuiteDoc = currentWorkspace.docCollection.getDoc(page.id);
const [openInfoModal, setOpenInfoModal] = useState(false);
@@ -93,7 +93,7 @@ export const PageOperationCell = ({
}, []);
const onDisablePublicSharing = useCallback(() => {
//TODO(@EYHN): implement disable public sharing
// TODO(@EYHN): implement disable public sharing
toast('Successfully disabled', {
portal: document.body,
});

View File

@@ -23,7 +23,7 @@ const TagListTitleCell = ({
>
{title || t['Untitled']()}
</div>
{/* //TODO(@EYHN): when indexer is ready, add this back
{/* // TODO(@EYHN): when indexer is ready, add this back
<div
data-testid="page-list-item-preview-text"
className={styles.titleCellPreview}

View File

@@ -18,8 +18,7 @@ import { VirtualizedList } from './virtualized-list';
export const VirtualizedTrashList = () => {
const currentWorkspace = useService(WorkspaceService).workspace;
const docCollection = currentWorkspace.docCollection;
const { restoreFromTrash, permanentlyDeletePage } =
useBlockSuiteMetaHelper(docCollection);
const { restoreFromTrash, permanentlyDeletePage } = useBlockSuiteMetaHelper();
const pageMetas = useBlockSuiteDocMeta(docCollection);
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
trash: true,

View File

@@ -19,7 +19,7 @@ export const TrashPageFooter = () => {
const t = useI18n();
const { appSettings } = useAppSettingHelper();
const { jumpToSubPath } = useNavigateHelper();
const { restoreFromTrash } = useBlockSuiteMetaHelper(docCollection);
const { restoreFromTrash } = useBlockSuiteMetaHelper();
const [open, setOpen] = useState(false);
const hintText = t['com.affine.cmdk.affine.editor.trash-footer-hint']();

View File

@@ -99,11 +99,7 @@ export const RootAppSidebar = (): ReactElement => {
const onClickNewPage = useAsyncCallback(
async (e?: MouseEvent) => {
const page = pageHelper.createPage(
undefined,
isNewTabTrigger(e) ? 'new-tab' : true
);
page.load();
pageHelper.createPage(undefined, isNewTabTrigger(e) ? 'new-tab' : true);
track.$.navigationPanel.$.createDoc();
},
[pageHelper]

View File

@@ -1,86 +1,59 @@
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
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 { DocsService, useService } from '@toeverything/infra';
import { DocsService, useService, WorkspaceService } from '@toeverything/infra';
import { useCallback } from 'react';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import type { DocCollection } from '../../shared';
import { useNavigateHelper } from '../use-navigate-helper';
export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
const { setDocMeta, getDocMeta, setDocReadonly, setDocTitle } =
useDocMetaHelper(docCollection);
const { createDoc } = useDocCollectionHelper(docCollection);
export function useBlockSuiteMetaHelper() {
const workspace = useService(WorkspaceService).workspace;
const { setDocMeta, getDocMeta, setDocTitle, setDocReadonly } =
useDocMetaHelper();
const { createDoc } = useDocCollectionHelper(workspace.docCollection);
const { openPage } = useNavigateHelper();
const collectionService = useService(CollectionService);
const pageRecordList = useService(DocsService).list;
const docRecordList = useService(DocsService).list;
// TODO-Doma
// "Remove" may cause ambiguity here. Consider renaming as "moveToTrash".
const removeToTrash = useCallback(
(pageId: string) => {
setDocMeta(pageId, {
trash: true,
trashDate: Date.now(),
});
setDocReadonly(pageId, true);
collectionService.deletePagesFromCollections([pageId]);
(docId: string) => {
const docRecord = docRecordList.doc$(docId).value;
if (docRecord) {
docRecord.moveToTrash();
setDocReadonly(docId, true);
}
},
[collectionService, setDocMeta, setDocReadonly]
[docRecordList, setDocReadonly]
);
const restoreFromTrash = useCallback(
(pageId: string) => {
setDocMeta(pageId, {
trash: false,
trashDate: undefined,
});
setDocReadonly(pageId, false);
(docId: string) => {
const docRecord = docRecordList.doc$(docId).value;
if (docRecord) {
docRecord.restoreFromTrash();
setDocReadonly(docId, false);
}
},
[setDocMeta, setDocReadonly]
[docRecordList, setDocReadonly]
);
const permanentlyDeletePage = useCallback(
(pageId: string) => {
docCollection.removeDoc(pageId);
workspace.docCollection.removeDoc(pageId);
},
[docCollection]
);
/**
* 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]
[workspace]
);
const duplicate = useAsyncCallback(
async (pageId: string, openPageAfterDuplication: boolean = true) => {
const currentPagePrimaryMode =
pageRecordList.doc$(pageId).value?.primaryMode$.value;
docRecordList.doc$(pageId).value?.primaryMode$.value;
const currentPageMeta = getDocMeta(pageId);
const newPage = createDoc();
const currentPage = docCollection.getDoc(pageId);
const currentPage = workspace.docCollection.getDoc(pageId);
newPage.load();
if (!currentPageMeta || !currentPage) {
@@ -95,33 +68,31 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
});
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 newPageTitle =
currentPageMeta.title.replace(lastDigitRegex, '') + `(${newNumber})`;
currentPageMeta?.title?.replace(lastDigitRegex, '') + `(${newNumber})`;
pageRecordList
docRecordList
.doc$(newPage.id)
.value?.setPrimaryMode(currentPagePrimaryMode || ('page' as DocMode));
setDocTitle(newPage.id, newPageTitle);
openPageAfterDuplication && openPage(docCollection.id, newPage.id);
openPageAfterDuplication &&
openPage(workspace.docCollection.id, newPage.id);
},
[
docCollection,
createDoc,
docRecordList,
getDocMeta,
openPage,
pageRecordList,
createDoc,
workspace.docCollection,
setDocMeta,
setDocTitle,
openPage,
]
);
return {
publicPage,
cancelPublicPage,
removeToTrash,
restoreFromTrash,
permanentlyDeletePage,

View File

@@ -30,7 +30,6 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
const mode = useLiveData(editor.mode$);
const t = useI18n();
const workspace = useService(WorkspaceService).workspace;
const docCollection = workspace.docCollection;
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
const favorite = useLiveData(favAdapter.isFavorite$(docId, 'doc'));
@@ -50,9 +49,9 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
setInfoModalState(true);
}, [setInfoModalState]);
const { duplicate } = useBlockSuiteMetaHelper(docCollection);
const { duplicate } = useBlockSuiteMetaHelper();
const exportHandler = useExportPage();
const { setTrashModal } = useTrashModalHelper(docCollection);
const { setTrashModal } = useTrashModalHelper();
const onClickDelete = useCallback(
(title: string) => {
setTrashModal({

View File

@@ -1,18 +1,16 @@
import { toast } from '@affine/component';
import { useI18n } from '@affine/i18n';
import type { DocCollection } from '@blocksuite/store';
import { useAtom } from 'jotai';
import { useCallback } from 'react';
import { trashModalAtom } from '../../atoms/trash-modal';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
export function useTrashModalHelper(docCollection: DocCollection) {
export function useTrashModalHelper() {
const t = useI18n();
const [trashModal, setTrashModal] = useAtom(trashModalAtom);
const { pageIds } = trashModal;
const { removeToTrash } = useBlockSuiteMetaHelper(docCollection);
const { removeToTrash } = useBlockSuiteMetaHelper();
const handleOnConfirm = useCallback(() => {
pageIds.forEach(pageId => {
removeToTrash(pageId);

View File

@@ -1,8 +1,8 @@
import type { RootBlockModel } from '@blocksuite/blocks';
import { assertExists } from '@blocksuite/global/utils';
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 { 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(
() => ({
setDocTitle: (docId: string, newTitle: string) => {
const page = docCollection.getDoc(docId);
assertExists(page);
const pageBlock = page
.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);
},
setDocTitle,
setDocMeta,
getDocMeta,
setDocReadonly,
}),
[docCollection]
[getDocMeta, setDocMeta, setDocReadonly, setDocTitle]
);
}

View File

@@ -7,6 +7,7 @@ import {
} from '@affine/component';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
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 { track } from '@affine/core/mixpanel';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/properties';
@@ -14,6 +15,7 @@ import { WorkbenchService } from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import {
DeleteIcon,
DuplicateIcon,
InformationIcon,
LinkedPageIcon,
OpenInNewIcon,
@@ -69,6 +71,11 @@ export const useExplorerDocNodeOperations = (
}, [docId, compatibleFavoriteItemsAdapter])
);
const { duplicate } = useBlockSuiteMetaHelper();
const handleDuplicate = useCallback(() => {
duplicate(docId, true);
track.$.navigationPanel.docs.createDoc();
}, [docId, duplicate]);
const handleOpenInfoModal = useCallback(() => {
track.$.docInfoPanel.$.open();
options.openInfoModal();
@@ -173,6 +180,14 @@ export const useExplorerDocNodeOperations = (
</MenuItem>
),
},
{
index: 99,
view: (
<MenuItem prefixIcon={<DuplicateIcon />} onClick={handleDuplicate}>
{t['com.affine.header.option.duplicate']()}
</MenuItem>
),
},
{
index: 99,
view: (
@@ -230,6 +245,7 @@ export const useExplorerDocNodeOperations = (
enableMultiView,
favorite,
handleAddLinkedPage,
handleDuplicate,
handleMoveToTrash,
handleOpenInNewTab,
handleOpenInSplitView,

View File

@@ -89,7 +89,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
const isSideBarOpen = useLiveData(workbench.sidebarOpen$);
const { appSettings } = useAppSettingHelper();
const chatPanelRef = useRef<ChatPanel | null>(null);
const { setDocReadonly } = useDocMetaHelper(workspace.docCollection);
const { setDocReadonly } = useDocMetaHelper();
const isActiveView = useIsActiveView();
// TODO(@eyhn): remove jotai here

View File

@@ -301,9 +301,8 @@ const ConflictList = ({
className,
...attrs
}: ConflictListProps) => {
const workspace = useService(WorkspaceService).workspace;
const currentDoc = useService(DocService).doc;
const { setTrashModal } = useTrashModalHelper(workspace.docCollection);
const { setTrashModal } = useTrashModalHelper();
const handleOpenTrashModal = useCallback(
(docRecord: DocRecord) => {

View File

@@ -91,9 +91,7 @@ export const Setting = () => {
export function CurrentWorkspaceModals() {
const currentWorkspace = useService(WorkspaceService).workspace;
const { trashModal, setTrashModal, handleOnConfirm } = useTrashModalHelper(
currentWorkspace.docCollection
);
const { trashModal, setTrashModal, handleOnConfirm } = useTrashModalHelper();
const deletePageTitles = trashModal.pageTitles;
const trashConfirmOpen = trashModal.open;
const onTrashConfirmOpenChange = useCallback(