diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.spec.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.spec.tsx new file mode 100644 index 0000000000..46af745ac1 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/editor/general.spec.tsx @@ -0,0 +1,152 @@ +/** + * @vitest-environment happy-dom + */ + +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import type { PropsWithChildren } from 'react'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +const editorSettingSet = vi.fn(); + +const editorSettingService = { + editorSetting: { + ['settings$']: { + value: { + autoTitleNewDocWithCurrentDate: true, + newDocDateTitleFormat: 'DD-MM-YYYY', + }, + }, + set: editorSettingSet, + }, +}; + +vi.mock('@affine/i18n', () => { + const translations: Record = { + 'com.affine.settings.editorSettings.general.auto-date-title.title': + 'Auto-title new docs with current date', + 'com.affine.settings.editorSettings.general.auto-date-title.description': + "Automatically title blank new docs with today's date.", + 'com.affine.settings.editorSettings.general.auto-date-title.format.title': + 'New doc date format', + 'com.affine.settings.editorSettings.general.auto-date-title.format.description': + 'Choose the date format used for automatic new doc titles.', + 'com.affine.settings.editorSettings.general.auto-date-title.format.dd-mm-yyyy': + 'DD-MM-YYYY', + 'com.affine.settings.editorSettings.general.auto-date-title.format.mm-dd-yyyy': + 'MM-DD-YYYY', + 'com.affine.settings.editorSettings.general.auto-date-title.format.yyyy-mm-dd': + 'YYYY-MM-DD', + 'com.affine.settings.editorSettings.general.auto-date-title.format.journal': + 'Journal style (localized)', + }; + + const useI18n = () => + new Proxy( + {}, + { + get: (_, key: string) => { + if (key === 't') { + return (translationKey: string) => + translations[translationKey] ?? translationKey; + } + return () => translations[key] ?? key; + }, + } + ); + + return { + Trans: ({ children }: PropsWithChildren) => children, + useI18n, + }; +}); + +vi.mock('@toeverything/infra', async importOriginal => { + const actual = (await importOriginal()) as Record; + + return { + ...actual, + useLiveData: (value: { value: unknown } | unknown) => { + if (value && typeof value === 'object' && 'value' in value) { + return value.value; + } + return value; + }, + useService: vi.fn(), + useServices: () => ({ + editorSettingService, + }), + }; +}); + +import { NewDocDateTitleSettings } from './general'; + +describe('NewDocDateTitleSettings', () => { + beforeEach(() => { + editorSettingSet.mockReset(); + editorSettingService.editorSetting['settings$'].value = { + autoTitleNewDocWithCurrentDate: true, + newDocDateTitleFormat: 'DD-MM-YYYY', + }; + }); + + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + test('persists the auto title toggle through EditorSettingService', () => { + render(); + + fireEvent.click(screen.getByRole('checkbox')); + + expect(editorSettingSet).toHaveBeenCalledWith( + 'autoTitleNewDocWithCurrentDate', + false + ); + }); + + test('persists the selected date format through EditorSettingService', () => { + render(); + + fireEvent.pointerDown( + screen.getByTestId('new-doc-date-title-format-trigger') + ); + fireEvent.click(screen.getByRole('menuitem', { name: 'YYYY-MM-DD' })); + + expect(editorSettingSet).toHaveBeenCalledWith( + 'newDocDateTitleFormat', + 'YYYY-MM-DD' + ); + }); + + test('renders all supported date format options', () => { + render(); + + const trigger = screen.getByTestId('new-doc-date-title-format-trigger'); + + expect(trigger.textContent).toContain('DD-MM-YYYY'); + + fireEvent.pointerDown(trigger); + + expect(screen.getByRole('menuitem', { name: 'DD-MM-YYYY' })).toBeTruthy(); + expect(screen.getByRole('menuitem', { name: 'MM-DD-YYYY' })).toBeTruthy(); + expect(screen.getByRole('menuitem', { name: 'YYYY-MM-DD' })).toBeTruthy(); + expect( + screen.getByRole('menuitem', { name: 'Journal style (localized)' }) + ).toBeTruthy(); + }); + + test('hides the date format row when auto title is disabled', () => { + editorSettingService.editorSetting['settings$'].value = { + autoTitleNewDocWithCurrentDate: false, + newDocDateTitleFormat: 'DD-MM-YYYY', + }; + + render(); + + expect( + screen.queryByTestId('new-doc-date-title-format-trigger') + ).toBeNull(); + expect(screen.queryByText('New doc date format')).toBeNull(); + }); +}); 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 27dd165284..959db72802 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 @@ -25,6 +25,8 @@ import { EditorSettingService, type FontFamily, fontStyleOptions, + type NewDocDateTitleFormat, + newDocDateTitleFormatOptions, } from '@affine/core/modules/editor-setting'; import { SpellCheckSettingService } from '@affine/core/modules/editor-setting/services/spell-check-setting'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; @@ -428,6 +430,99 @@ 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()}` + ), + })); +}; + +export const NewDocDateTitleSettings = () => { + const t = useI18n(); + const { editorSettingService } = useServices({ EditorSettingService }); + const settings = useLiveData(editorSettingService.editorSetting.settings$); + + const formatItems = useMemo(() => getNewDocDateTitleFormatItems(t), [t]); + + const onToggleAutoDateTitle = useCallback( + (checked: boolean) => { + editorSettingService.editorSetting.set( + 'autoTitleNewDocWithCurrentDate', + checked + ); + }, + [editorSettingService.editorSetting] + ); + + const onDateTitleFormatChange = useCallback( + (value: NewDocDateTitleFormat) => { + editorSettingService.editorSetting.set('newDocDateTitleFormat', value); + }, + [editorSettingService.editorSetting] + ); + + return ( + <> + + + + {settings.autoTitleNewDocWithCurrentDate ? ( + + { + return ( + onDateTitleFormatChange(item.value)} + > + {item.label} + + ); + })} + > + + { + formatItems.find( + item => item.value === settings.newDocDateTitleFormat + )?.label + } + + + + ) : null} + + ); +}; + const AISettings = () => { const t = useI18n(); const { openConfirmModal } = useConfirmModal(); @@ -573,6 +668,7 @@ export const General = () => { + {BUILD_CONFIG.isElectron && } {environment.isLinux && } {/* // TODO(@akumatus): implement these settings diff --git a/packages/frontend/core/src/modules/doc/index.ts b/packages/frontend/core/src/modules/doc/index.ts index fc51e6ed05..cdd6be0a8c 100644 --- a/packages/frontend/core/src/modules/doc/index.ts +++ b/packages/frontend/core/src/modules/doc/index.ts @@ -4,7 +4,7 @@ export { DocRecordList } from './entities/record-list'; export { DocCreated } from './events'; export { DocScope } from './scopes/doc'; export { DocService } from './services/doc'; -export { DocsService } from './services/docs'; +export { DocsQueryService, DocsService } from './services/docs'; import type { Framework } from '@toeverything/infra'; @@ -17,7 +17,7 @@ import { DocRecordList } from './entities/record-list'; import { DocCreateMiddleware } from './providers/doc-create-middleware'; import { DocScope } from './scopes/doc'; import { DocService } from './services/doc'; -import { DocsService } from './services/docs'; +import { DocsQueryService, DocsService } from './services/docs'; import { DocPropertiesStore } from './stores/doc-properties'; import { DocsStore } from './stores/docs'; @@ -26,10 +26,11 @@ export { DocCreateMiddleware } from './providers/doc-create-middleware'; export function configureDocModule(framework: Framework) { framework .scope(WorkspaceScope) + .service(DocsQueryService, [DocsStore, DocPropertiesStore]) .service(DocsService, [ DocsStore, - DocPropertiesStore, [DocCreateMiddleware], + DocsQueryService, ]) .store(DocPropertiesStore, [WorkspaceService, WorkspaceDBService]) .store(DocsStore, [WorkspaceService, DocPropertiesStore]) diff --git a/packages/frontend/core/src/modules/doc/services/docs.ts b/packages/frontend/core/src/modules/doc/services/docs.ts index abb78b06ac..fd1ab71613 100644 --- a/packages/frontend/core/src/modules/doc/services/docs.ts +++ b/packages/frontend/core/src/modules/doc/services/docs.ts @@ -22,15 +22,7 @@ import { getDuplicatedDocTitle } from './duplicate-title'; const logger = new DebugLogger('DocsService'); -export class DocsService extends Service { - list = this.framework.createEntity(DocRecordList); - - pool = new ObjectPool({ - onDelete(obj) { - obj.scope.dispose(); - }, - }); - +export class DocsQueryService extends Service { /** * Get all property values of a property, used for search * @@ -88,11 +80,60 @@ export class DocsService extends Service { constructor( private readonly store: DocsStore, - private readonly docPropertiesStore: DocPropertiesStore, - private readonly docCreateMiddlewares: DocCreateMiddleware[] + private readonly docPropertiesStore: DocPropertiesStore ) { 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); @@ -165,6 +206,9 @@ 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 new file mode 100644 index 0000000000..ed05899109 --- /dev/null +++ b/packages/frontend/core/src/modules/editor-setting/__tests__/date-title.spec.ts @@ -0,0 +1,71 @@ +import { getOrCreateI18n } from '@affine/i18n'; +import { describe, expect, test } from 'vitest'; + +import { + buildNewDocDateTitle, + getUniqueNewDocDateTitle, +} from '../utils/date-title'; + +describe('date-title', () => { + test('formats dates using DD-MM-YYYY', () => { + 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'); + }); + + test('formats dates using journal style', () => { + getOrCreateI18n(); + 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({ + existingTitles: [ + '23-03-2026', + '23-03-2026(2)', + '23-03-2026(3)', + 'Another doc', + ], + format: 'DD-MM-YYYY', + date: '2026-03-23', + }) + ).toBe('23-03-2026(4)'); + }); + + test('does not suffix when only duplicate-style titles exist', () => { + expect( + getUniqueNewDocDateTitle({ + existingTitles: ['23-03-2026(2)'], + format: 'DD-MM-YYYY', + date: '2026-03-23', + }) + ).toBe('23-03-2026'); + }); +}); 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 new file mode 100644 index 0000000000..586fecd85f --- /dev/null +++ b/packages/frontend/core/src/modules/editor-setting/__tests__/doc-create-middleware.spec.ts @@ -0,0 +1,177 @@ +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 5c2763ad98..02141b15db 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,10 +1,15 @@ -import { Service } from '@toeverything/infra'; +import { LiveData, Service } from '@toeverything/infra'; -import type { DocCreateMiddleware, DocRecord } from '../../doc'; +import type { + DocCreateMiddleware, + DocRecord, + DocsQueryService, +} 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, @@ -28,23 +33,42 @@ 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 appThemeService: AppThemeService, + private readonly docsQueryService: DocsQueryService ) { 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 preferMode = - this.editorSettingService.editorSetting.settings$.value.newDocDefaultMode; + const settings = this.editorSettingService.editorSetting.settings$.value; + const preferMode = settings.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 414e666e83..819350dde9 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 } from '../doc'; +import { DocCreateMiddleware, DocsQueryService } from '../doc'; import { I18n } from '../i18n'; import { GlobalState, GlobalStateService } from '../storage'; import { AppThemeService } from '../theme'; @@ -14,8 +14,12 @@ import { EditorSettingProvider } from './provider/editor-setting-provider'; import { EditorSettingService } from './services/editor-setting'; import { SpellCheckSettingService } from './services/spell-check-setting'; import { TraySettingService } from './services/tray-settings'; -export type { FontFamily } from './schema'; -export { EditorSettingSchema, fontStyleOptions } from './schema'; +export type { FontFamily, NewDocDateTitleFormat } from './schema'; +export { + EditorSettingSchema, + fontStyleOptions, + newDocDateTitleFormatOptions, +} from './schema'; export { EditorSettingService } from './services/editor-setting'; export function configureEditorSettingModule(framework: Framework) { @@ -30,6 +34,7 @@ export function configureEditorSettingModule(framework: Framework) { .impl(DocCreateMiddleware, EditorSettingDocCreateMiddleware, [ EditorSettingService, AppThemeService, + DocsQueryService, ]); } diff --git a/packages/frontend/core/src/modules/editor-setting/schema.ts b/packages/frontend/core/src/modules/editor-setting/schema.ts index b7ff0e65d0..3537dc2f92 100644 --- a/packages/frontend/core/src/modules/editor-setting/schema.ts +++ b/packages/frontend/core/src/modules/editor-setting/schema.ts @@ -5,6 +5,14 @@ export const BSEditorSettingSchema = GeneralSettingSchema; export type FontFamily = 'Sans' | 'Serif' | 'Mono' | 'Custom'; export type EdgelessDefaultTheme = 'auto' | 'dark' | 'light' | 'specified'; +export const newDocDateTitleFormatOptions = [ + 'DD-MM-YYYY', + 'MM-DD-YYYY', + 'YYYY-MM-DD', + 'journal', +] as const; +export type NewDocDateTitleFormat = + (typeof newDocDateTitleFormatOptions)[number]; export const fontStyleOptions = [ { key: 'Sans', value: 'var(--affine-font-sans-family)' }, @@ -21,6 +29,10 @@ const AffineEditorSettingSchema = z.object({ customFontFamily: z.string().default(''), fontSize: z.number().min(12).max(24).default(16), newDocDefaultMode: z.enum(['edgeless', 'page', 'ask']).default('page'), + autoTitleNewDocWithCurrentDate: z.boolean().default(false), + newDocDateTitleFormat: z + .enum(newDocDateTitleFormatOptions) + .default('DD-MM-YYYY'), fullWidthLayout: z.boolean().default(false), displayDocInfo: z.boolean().default(true), displayBiDirectionalLink: z.boolean().default(true), 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 new file mode 100644 index 0000000000..f82aa58f25 --- /dev/null +++ b/packages/frontend/core/src/modules/editor-setting/utils/date-title.ts @@ -0,0 +1,41 @@ +import { i18nTime } from '@affine/i18n'; +import dayjs from 'dayjs'; + +import type { NewDocDateTitleFormat } from '../schema'; + +export const buildNewDocDateTitle = ( + date: dayjs.ConfigType, + format: NewDocDateTitleFormat +) => { + if (format === 'journal') { + return i18nTime(date, { + absolute: { accuracy: 'day' }, + }); + } + + return dayjs(date).format(format); +}; + +export const getUniqueNewDocDateTitle = ({ + existingTitles, + format, + date = new Date(), +}: { + existingTitles: Iterable; + format: NewDocDateTitleFormat; + date?: dayjs.ConfigType; +}) => { + const normalizedTitles = new Set(existingTitles); + const baseTitle = buildNewDocDateTitle(date, format); + + if (!normalizedTitles.has(baseTitle)) { + return baseTitle; + } + + let duplicateIndex = 2; + while (normalizedTitles.has(`${baseTitle}(${duplicateIndex})`)) { + duplicateIndex += 1; + } + + return `${baseTitle}(${duplicateIndex})`; +}; diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index afb78e7497..f50a7a4753 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -15,7 +15,7 @@ "ja": 96, "ko": 97, "nb-NO": 47, - "pl": 98, + "pl": 97, "pt-BR": 96, "ru": 98, "sv-SE": 96, diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index b471171e48..3ef4afe655 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -5490,6 +5490,38 @@ export function useAFFiNEI18N(): { * `New doc default mode` */ ["com.affine.settings.editorSettings.general.default-new-doc.title"](): string; + /** + * `Auto-title new docs with current date` + */ + ["com.affine.settings.editorSettings.general.auto-date-title.title"](): string; + /** + * `Automatically title blank new docs with today's date.` + */ + ["com.affine.settings.editorSettings.general.auto-date-title.description"](): string; + /** + * `New doc date format` + */ + ["com.affine.settings.editorSettings.general.auto-date-title.format.title"](): string; + /** + * `Choose the date format used for automatic new doc titles.` + */ + ["com.affine.settings.editorSettings.general.auto-date-title.format.description"](): string; + /** + * `DD-MM-YYYY` + */ + ["com.affine.settings.editorSettings.general.auto-date-title.format.dd-mm-yyyy"](): string; + /** + * `MM-DD-YYYY` + */ + ["com.affine.settings.editorSettings.general.auto-date-title.format.mm-dd-yyyy"](): string; + /** + * `YYYY-MM-DD` + */ + ["com.affine.settings.editorSettings.general.auto-date-title.format.yyyy-mm-dd"](): string; + /** + * `Journal style (localized)` + */ + ["com.affine.settings.editorSettings.general.auto-date-title.format.journal"](): string; /** * `Customize your text experience.` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index f35b4a6422..b18a39ef79 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1366,6 +1366,14 @@ "com.affine.settings.editorSettings.general.default-code-block.wrap.title": "Wrap code in code blocks", "com.affine.settings.editorSettings.general.default-new-doc.description": "Default mode for new doc.", "com.affine.settings.editorSettings.general.default-new-doc.title": "New doc default mode", + "com.affine.settings.editorSettings.general.auto-date-title.title": "Auto-title new docs with current date", + "com.affine.settings.editorSettings.general.auto-date-title.description": "Automatically title blank new docs with today's date.", + "com.affine.settings.editorSettings.general.auto-date-title.format.title": "New doc date format", + "com.affine.settings.editorSettings.general.auto-date-title.format.description": "Choose the date format used for automatic new doc titles.", + "com.affine.settings.editorSettings.general.auto-date-title.format.dd-mm-yyyy": "DD-MM-YYYY", + "com.affine.settings.editorSettings.general.auto-date-title.format.mm-dd-yyyy": "MM-DD-YYYY", + "com.affine.settings.editorSettings.general.auto-date-title.format.yyyy-mm-dd": "YYYY-MM-DD", + "com.affine.settings.editorSettings.general.auto-date-title.format.journal": "Journal style (localized)", "com.affine.settings.editorSettings.general.font-family.custom.description": "Customize your text experience.", "com.affine.settings.editorSettings.general.font-family.custom.title": "Custom font family", "com.affine.settings.editorSettings.general.font-family.description": "Choose your editor's font family.",