diff --git a/packages/frontend/core/src/blocksuite/block-suite-page-list/utils.tsx b/packages/frontend/core/src/blocksuite/block-suite-page-list/utils.tsx index 0405f012aa..2058e284c4 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-page-list/utils.tsx +++ b/packages/frontend/core/src/blocksuite/block-suite-page-list/utils.tsx @@ -2,22 +2,51 @@ import { toast } from '@affine/component'; import { getStoreManager } from '@affine/core/blocksuite/manager/store'; import { AppSidebarService } from '@affine/core/modules/app-sidebar'; import { DocsService } from '@affine/core/modules/doc'; +import { + EditorSettingService, + resolveNewDocTitle, +} from '@affine/core/modules/editor-setting'; import { WorkbenchService } from '@affine/core/modules/workbench'; import { getAFFiNEWorkspaceSchema } from '@affine/core/modules/workspace'; import { type DocMode } from '@blocksuite/affine/model'; import type { Workspace } from '@blocksuite/affine/store'; -import { useServices } from '@toeverything/infra'; +import { LiveData, useLiveData, useServices } from '@toeverything/infra'; import { useCallback, useMemo } from 'react'; export const usePageHelper = (docCollection: Workspace) => { - const { docsService, workbenchService, appSidebarService } = useServices({ + const { + docsService, + workbenchService, + appSidebarService, + editorSettingService, + } = useServices({ DocsService, WorkbenchService, AppSidebarService, + EditorSettingService, }); const workbench = workbenchService.workbench; const docRecordList = docsService.list; const appSidebar = appSidebarService.sidebar; + const settings = useLiveData(editorSettingService.editorSetting.settings$); + const allDocTitles = useLiveData( + useMemo(() => LiveData.from(docsService.allDocTitle$(), []), [docsService]) + ); + + const createBlankDoc = useCallback(() => { + const title = resolveNewDocTitle({ + autoTitleEnabled: settings.autoTitleNewDocWithCurrentDate, + existingTitles: allDocTitles.map(doc => doc.title).filter(Boolean), + format: settings.newDocDateTitleFormat, + }); + + return docsService.createDoc(title ? { title } : undefined); + }, [ + allDocTitles, + docsService, + settings.autoTitleNewDocWithCurrentDate, + settings.newDocDateTitleFormat, + ]); const createPageAndOpen = useCallback( ( @@ -31,7 +60,7 @@ export const usePageHelper = (docCollection: Workspace) => { } ) => { appSidebar.setHovering(false); - const page = docsService.createDoc(); + const page = createBlankDoc(); if (mode) { docRecordList.doc$(page.id).value?.setPrimaryMode(mode); @@ -45,7 +74,7 @@ export const usePageHelper = (docCollection: Workspace) => { } return page; }, - [appSidebar, docRecordList, docsService, workbench] + [appSidebar, createBlankDoc, docRecordList, workbench] ); const createEdgelessAndOpen = useCallback( diff --git a/packages/frontend/core/src/components/page-list/operation-cell.tsx b/packages/frontend/core/src/components/page-list/operation-cell.tsx index 524237b5d2..e08eabd54f 100644 --- a/packages/frontend/core/src/components/page-list/operation-cell.tsx +++ b/packages/frontend/core/src/components/page-list/operation-cell.tsx @@ -37,6 +37,7 @@ import { useLiveData, useService, useServices } from '@toeverything/infra'; import type { MouseEvent } from 'react'; import { useCallback, useState } from 'react'; +import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils'; import { type CollectionMeta, CollectionService, @@ -340,13 +341,16 @@ export const CollectionOperationCell = ({ compatibleFavoriteItemsAdapter: favAdapter, workspaceDialogService, collectionService, - docsService, + workspaceService, } = useServices({ CompatibleFavoriteItemsAdapter, WorkspaceDialogService, CollectionService, - DocsService, + WorkspaceService, }); + const { createPage } = usePageHelper( + workspaceService.workspace.docCollection + ); const collectionId = collectionMeta.id; const { openConfirmModal } = useConfirmModal(); const favourite = useLiveData( @@ -409,9 +413,9 @@ export const CollectionOperationCell = ({ }, [favAdapter, collectionId, t]); const createAndAddDocument = useCallback(() => { - const newDoc = docsService.createDoc(); + const newDoc = createPage(undefined, { show: false }); collectionService.addDocToCollection(collectionId, newDoc.id); - }, [docsService, collectionService, collectionId]); + }, [collectionService, collectionId, createPage]); const onConfirmAddDocToCollection = useCallback(() => { openConfirmModal({ diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx index 959db72802..5a17c9e75a 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.tsx @@ -430,26 +430,22 @@ const NewDocDefaultModeSettings = () => { ); }; -export const getNewDocDateTitleFormatItems = ( - t: ReturnType -): Array<{ - value: NewDocDateTitleFormat; - label: string; -}> => { - return newDocDateTitleFormatOptions.map(value => ({ - value, - label: t.t( - `com.affine.settings.editorSettings.general.auto-date-title.format.${value.toLowerCase()}` - ), - })); +const getDateTitleFormatLabel = (format: NewDocDateTitleFormat) => { + return `com.affine.settings.editorSettings.general.auto-date-title.format.${format.toLowerCase()}` as const; }; export const NewDocDateTitleSettings = () => { const t = useI18n(); const { editorSettingService } = useServices({ EditorSettingService }); const settings = useLiveData(editorSettingService.editorSetting.settings$); - - const formatItems = useMemo(() => getNewDocDateTitleFormatItems(t), [t]); + const formatItems = useMemo( + () => + newDocDateTitleFormatOptions.map(value => ({ + value, + label: t.t(getDateTitleFormatLabel(value)), + })), + [t] + ); const onToggleAutoDateTitle = useCallback( (checked: boolean) => { @@ -494,17 +490,15 @@ export const NewDocDateTitleSettings = () => { > { - return ( - onDateTitleFormatChange(item.value)} - > - {item.label} - - ); - })} + items={formatItems.map(item => ( + onDateTitleFormatChange(item.value)} + > + {item.label} + + ))} > ({ + onDelete(obj) { + obj.scope.dispose(); + }, + }); + /** * Get all property values of a property, used for search * @@ -80,60 +88,11 @@ export class DocsQueryService extends Service { constructor( private readonly store: DocsStore, - private readonly docPropertiesStore: DocPropertiesStore + private readonly docPropertiesStore: DocPropertiesStore, + private readonly docCreateMiddlewares: DocCreateMiddleware[] ) { super(); } -} - -export class DocsService extends Service { - list = this.framework.createEntity(DocRecordList); - - pool = new ObjectPool({ - onDelete(obj) { - obj.scope.dispose(); - }, - }); - - constructor( - private readonly store: DocsStore, - private readonly docCreateMiddlewares: DocCreateMiddleware[], - private readonly docsQueryService: DocsQueryService - ) { - super(); - } - - propertyValues$(propertyKey: string) { - return this.docsQueryService.propertyValues$(propertyKey); - } - - allDocsCreatedDate$() { - return this.docsQueryService.allDocsCreatedDate$(); - } - - allDocsUpdatedDate$() { - return this.docsQueryService.allDocsUpdatedDate$(); - } - - allDocsTagIds$() { - return this.docsQueryService.allDocsTagIds$(); - } - - allDocIds$() { - return this.docsQueryService.allDocIds$(); - } - - allNonTrashDocIds$() { - return this.docsQueryService.allNonTrashDocIds$(); - } - - allTrashDocIds$() { - return this.docsQueryService.allTrashDocIds$(); - } - - allDocTitle$() { - return this.docsQueryService.allDocTitle$(); - } loaded(docId: string) { const exists = this.pool.get(docId); @@ -206,9 +165,6 @@ export class DocsService extends Service { if (options.isTemplate) { docRecord.setProperty('isTemplate', true); } - if (options.title?.trim()) { - docRecord.setMeta({ title: options.title }); - } for (const middleware of this.docCreateMiddlewares) { middleware.afterCreate?.(docRecord, options); } diff --git a/packages/frontend/core/src/modules/editor-setting/__tests__/date-title.spec.ts b/packages/frontend/core/src/modules/editor-setting/__tests__/date-title.spec.ts index ed05899109..4b42e7df2a 100644 --- a/packages/frontend/core/src/modules/editor-setting/__tests__/date-title.spec.ts +++ b/packages/frontend/core/src/modules/editor-setting/__tests__/date-title.spec.ts @@ -4,18 +4,13 @@ import { describe, expect, test } from 'vitest'; import { buildNewDocDateTitle, getUniqueNewDocDateTitle, + resolveNewDocTitle, } from '../utils/date-title'; describe('date-title', () => { - test('formats dates using DD-MM-YYYY', () => { + test('formats dates using supported numeric formats', () => { expect(buildNewDocDateTitle('2026-03-23', 'DD-MM-YYYY')).toBe('23-03-2026'); - }); - - test('formats dates using MM-DD-YYYY', () => { expect(buildNewDocDateTitle('2026-03-23', 'MM-DD-YYYY')).toBe('03-23-2026'); - }); - - test('formats dates using YYYY-MM-DD', () => { expect(buildNewDocDateTitle('2026-03-23', 'YYYY-MM-DD')).toBe('2026-03-23'); }); @@ -24,26 +19,6 @@ describe('date-title', () => { expect(buildNewDocDateTitle('2026-03-23', 'journal')).toBe('Mar 23, 2026'); }); - test('returns the base title when there is no collision', () => { - expect( - getUniqueNewDocDateTitle({ - existingTitles: ['Some title'], - format: 'DD-MM-YYYY', - date: '2026-03-23', - }) - ).toBe('23-03-2026'); - }); - - test('suffixes duplicate titles starting at (2)', () => { - expect( - getUniqueNewDocDateTitle({ - existingTitles: ['23-03-2026'], - format: 'DD-MM-YYYY', - date: '2026-03-23', - }) - ).toBe('23-03-2026(2)'); - }); - test('increments to the next available duplicate suffix', () => { expect( getUniqueNewDocDateTitle({ @@ -59,13 +34,37 @@ describe('date-title', () => { ).toBe('23-03-2026(4)'); }); - test('does not suffix when only duplicate-style titles exist', () => { + test('keeps provided titles unchanged', () => { expect( - getUniqueNewDocDateTitle({ - existingTitles: ['23-03-2026(2)'], + resolveNewDocTitle({ + title: 'Typed by user', + autoTitleEnabled: true, + existingTitles: ['23-03-2026'], format: 'DD-MM-YYYY', date: '2026-03-23', }) - ).toBe('23-03-2026'); + ).toBe('Typed by user'); + }); + + test('returns undefined for blank titles when the feature is disabled', () => { + expect( + resolveNewDocTitle({ + autoTitleEnabled: false, + existingTitles: ['23-03-2026'], + format: 'DD-MM-YYYY', + date: '2026-03-23', + }) + ).toBeUndefined(); + }); + + test('generates a unique title for blank docs when enabled', () => { + expect( + resolveNewDocTitle({ + autoTitleEnabled: true, + existingTitles: ['23-03-2026', '23-03-2026(2)'], + format: 'DD-MM-YYYY', + date: '2026-03-23', + }) + ).toBe('23-03-2026(3)'); }); }); diff --git a/packages/frontend/core/src/modules/editor-setting/__tests__/doc-create-middleware.spec.ts b/packages/frontend/core/src/modules/editor-setting/__tests__/doc-create-middleware.spec.ts deleted file mode 100644 index 586fecd85f..0000000000 --- a/packages/frontend/core/src/modules/editor-setting/__tests__/doc-create-middleware.spec.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { getOrCreateI18n } from '@affine/i18n'; -import { Framework, Service } from '@toeverything/infra'; -import { of } from 'rxjs'; -import { afterEach, describe, expect, test, vi } from 'vitest'; - -import { EditorSettingDocCreateMiddleware } from '../impls/doc-create-middleware'; - -const createDocsQueryService = (titles: string[]) => { - return { - ['allDocTitle$']: () => - of( - titles.map((title, index) => ({ - id: `doc-${index}`, - title, - })) - ), - }; -}; - -const createEditorSettingService = (overrides?: Record) => { - return { - editorSetting: { - ['settings$']: { - value: { - newDocDefaultMode: 'page', - autoTitleNewDocWithCurrentDate: false, - newDocDateTitleFormat: 'DD-MM-YYYY', - ...overrides, - }, - }, - get: vi.fn((key: string) => { - if (key === 'affine:note') { - return undefined; - } - if (key === 'edgelessDefaultTheme') { - return 'specified'; - } - return undefined; - }), - }, - }; -}; - -const appThemeService = { - appTheme: { - ['theme$']: { - value: 'light', - }, - }, -}; - -const createMiddleware = ({ - settings, - titles, -}: { - settings?: Record; - titles?: string[]; -}) => { - class MockEditorSettingService extends Service { - editorSetting = createEditorSettingService(settings).editorSetting; - } - - class MockAppThemeService extends Service { - appTheme = appThemeService.appTheme; - } - - class MockDocsQueryService extends Service { - ['allDocTitle$'] = - createDocsQueryService(titles ?? [])['allDocTitle$']; - } - - const framework = new Framework(); - framework - .service(MockEditorSettingService) - .service(MockAppThemeService) - .service(MockDocsQueryService) - .service(EditorSettingDocCreateMiddleware, [ - MockEditorSettingService as never, - MockAppThemeService as never, - MockDocsQueryService as never, - ]); - - return framework.provider().get(EditorSettingDocCreateMiddleware); -}; - -describe('EditorSettingDocCreateMiddleware', () => { - afterEach(() => { - vi.useRealTimers(); - }); - - test('adds an auto date title for blank docs when enabled', () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-03-23T09:00:00.000Z')); - - const middleware = createMiddleware({ - settings: { - autoTitleNewDocWithCurrentDate: true, - newDocDateTitleFormat: 'DD-MM-YYYY', - }, - }); - - expect(middleware.beforeCreate({})).toMatchObject({ - title: '23-03-2026', - primaryMode: 'page', - }); - }); - - test('keeps blank docs untitled when the feature is disabled', () => { - const middleware = createMiddleware({}); - - expect(middleware.beforeCreate({})).toMatchObject({ - primaryMode: 'page', - }); - expect(middleware.beforeCreate({}).title).toBeUndefined(); - }); - - test('does not override explicitly provided titles', () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-03-23T09:00:00.000Z')); - - const middleware = createMiddleware({ - settings: { - autoTitleNewDocWithCurrentDate: true, - }, - titles: ['23-03-2026'], - }); - - expect( - middleware.beforeCreate({ - title: 'Typed by user', - }).title - ).toBe('Typed by user'); - }); - - test('uses the next duplicate suffix when the date title already exists', () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-03-23T09:00:00.000Z')); - - const middleware = createMiddleware({ - settings: { - autoTitleNewDocWithCurrentDate: true, - }, - titles: ['23-03-2026', '23-03-2026(2)'], - }); - - expect(middleware.beforeCreate({}).title).toBe('23-03-2026(3)'); - }); - - test('uses the selected format for the generated title', () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-03-23T09:00:00.000Z')); - - const middleware = createMiddleware({ - settings: { - autoTitleNewDocWithCurrentDate: true, - newDocDateTitleFormat: 'YYYY-MM-DD', - }, - }); - - expect(middleware.beforeCreate({}).title).toBe('2026-03-23'); - }); - - test('supports month-name formats for generated titles', () => { - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-03-23T09:00:00.000Z')); - getOrCreateI18n(); - - const middleware = createMiddleware({ - settings: { - autoTitleNewDocWithCurrentDate: true, - newDocDateTitleFormat: 'journal', - }, - }); - - expect(middleware.beforeCreate({}).title).toBe('Mar 23, 2026'); - }); -}); diff --git a/packages/frontend/core/src/modules/editor-setting/impls/doc-create-middleware.ts b/packages/frontend/core/src/modules/editor-setting/impls/doc-create-middleware.ts index 02141b15db..5c2763ad98 100644 --- a/packages/frontend/core/src/modules/editor-setting/impls/doc-create-middleware.ts +++ b/packages/frontend/core/src/modules/editor-setting/impls/doc-create-middleware.ts @@ -1,15 +1,10 @@ -import { LiveData, Service } from '@toeverything/infra'; +import { Service } from '@toeverything/infra'; -import type { - DocCreateMiddleware, - DocRecord, - DocsQueryService, -} from '../../doc'; +import type { DocCreateMiddleware, DocRecord } from '../../doc'; import type { DocCreateOptions } from '../../doc/types'; import type { AppThemeService } from '../../theme'; import type { EdgelessDefaultTheme } from '../schema'; import type { EditorSettingService } from '../services/editor-setting'; -import { getUniqueNewDocDateTitle } from '../utils/date-title'; const getValueByDefaultTheme = ( defaultTheme: EdgelessDefaultTheme, @@ -33,42 +28,23 @@ export class EditorSettingDocCreateMiddleware extends Service implements DocCreateMiddleware { - private readonly allDocTitles$: LiveData<{ id: string; title: string }[]>; - constructor( private readonly editorSettingService: EditorSettingService, - private readonly appThemeService: AppThemeService, - private readonly docsQueryService: DocsQueryService + private readonly appThemeService: AppThemeService ) { super(); - this.allDocTitles$ = LiveData.from(this.docsQueryService.allDocTitle$(), []); } - - private getCurrentDocTitles() { - return this.allDocTitles$.value.map(doc => doc.title).filter(Boolean); - } - beforeCreate(docCreateOptions: DocCreateOptions): DocCreateOptions { // clone the docCreateOptions to avoid mutating the original object docCreateOptions = { ...docCreateOptions, }; - const settings = this.editorSettingService.editorSetting.settings$.value; - const preferMode = settings.newDocDefaultMode; + const preferMode = + this.editorSettingService.editorSetting.settings$.value.newDocDefaultMode; const mode = preferMode === 'ask' ? 'page' : preferMode; docCreateOptions.primaryMode ??= mode; - if ( - !docCreateOptions.title?.trim() && - settings.autoTitleNewDocWithCurrentDate - ) { - docCreateOptions.title = getUniqueNewDocDateTitle({ - existingTitles: this.getCurrentDocTitles(), - format: settings.newDocDateTitleFormat, - }); - } - docCreateOptions.docProps = { ...docCreateOptions.docProps, note: this.editorSettingService.editorSetting.get('affine:note'), diff --git a/packages/frontend/core/src/modules/editor-setting/index.ts b/packages/frontend/core/src/modules/editor-setting/index.ts index 819350dde9..2ef5e5af79 100644 --- a/packages/frontend/core/src/modules/editor-setting/index.ts +++ b/packages/frontend/core/src/modules/editor-setting/index.ts @@ -2,7 +2,7 @@ import { type Framework } from '@toeverything/infra'; import { ServersService } from '../cloud'; import { DesktopApiService } from '../desktop-api'; -import { DocCreateMiddleware, DocsQueryService } from '../doc'; +import { DocCreateMiddleware } from '../doc'; import { I18n } from '../i18n'; import { GlobalState, GlobalStateService } from '../storage'; import { AppThemeService } from '../theme'; @@ -21,6 +21,7 @@ export { newDocDateTitleFormatOptions, } from './schema'; export { EditorSettingService } from './services/editor-setting'; +export { resolveNewDocTitle } from './utils/date-title'; export function configureEditorSettingModule(framework: Framework) { framework @@ -34,7 +35,6 @@ export function configureEditorSettingModule(framework: Framework) { .impl(DocCreateMiddleware, EditorSettingDocCreateMiddleware, [ EditorSettingService, AppThemeService, - DocsQueryService, ]); } diff --git a/packages/frontend/core/src/modules/editor-setting/utils/date-title.ts b/packages/frontend/core/src/modules/editor-setting/utils/date-title.ts index f82aa58f25..8b21a966de 100644 --- a/packages/frontend/core/src/modules/editor-setting/utils/date-title.ts +++ b/packages/frontend/core/src/modules/editor-setting/utils/date-title.ts @@ -39,3 +39,21 @@ export const getUniqueNewDocDateTitle = ({ return `${baseTitle}(${duplicateIndex})`; }; + +export const resolveNewDocTitle = ({ + title, + autoTitleEnabled, + existingTitles, + format, + date = new Date(), +}: { + title?: string; + autoTitleEnabled: boolean; + existingTitles: Iterable; + format: NewDocDateTitleFormat; + date?: dayjs.ConfigType; +}) => { + if (title?.trim()) return title; + if (!autoTitleEnabled) return undefined; + return getUniqueNewDocDateTitle({ existingTitles, format, date }); +};