feat: add auto-date titles for new documents (#14716)

## Summary

Adds an Editor setting to automatically title blank new documents with
the current date.
fixes https://github.com/toeverything/AFFiNE/issues/14709
https://www.loom.com/share/953b4eafcfb247839e977dca6f457229

## What Changed

- Added `Auto-title new docs with current date` under Editor settings
- Added `New doc date format`, shown only when auto-title is enabled
- Supported formats:
  - `DD-MM-YYYY`
  - `MM-DD-YYYY`
  - `YYYY-MM-DD`
  - `Journal style (localized)`
- Kept titles unique by appending duplicate-style suffixes:
  - `2026-03-24`
  - `2026-03-24(2)`
  - `2026-03-24(3)`

## Behavior

- Only applies to blank new docs
- Does not override explicitly provided titles
- Uses the existing journal-style localized formatter for the localized
option

## Implementation Notes

- Extended editor setting schema with:
  - `autoTitleNewDocWithCurrentDate`
  - `newDocDateTitleFormat`
- Added a helper for generating unique date-based titles
- Wired title generation into doc creation middleware
- Synced created titles into doc metadata so uniqueness works
consistently

## Tests

- Added unit coverage for:
  - date title formatting
  - duplicate suffix generation
  - doc creation middleware behavior
  - settings UI behavior


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* General settings: toggle to auto-insert current date into new document
titles; selectable formats: DD-MM-YYYY, MM-DD-YYYY, YYYY-MM-DD, and
localized "journal". Date-format chooser appears only when enabled.

* **Behavior**
* Blank new-docs are auto-populated per chosen format; user-provided
titles are preserved. Auto-generated titles avoid collisions by
appending incrementing suffixes.

* **Localization**
* Added translations for the setting, description, format chooser, and
all format labels.

* **Tests**
* Added UI and unit tests covering formatting, uniqueness, middleware
behavior, and interaction.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
chauhan_s
2026-04-05 14:40:01 +05:30
committed by GitHub
parent fc5329a1be
commit 558400b7db
13 changed files with 686 additions and 23 deletions
@@ -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<string, string> = {
'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<string, unknown>;
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(<NewDocDateTitleSettings />);
fireEvent.click(screen.getByRole('checkbox'));
expect(editorSettingSet).toHaveBeenCalledWith(
'autoTitleNewDocWithCurrentDate',
false
);
});
test('persists the selected date format through EditorSettingService', () => {
render(<NewDocDateTitleSettings />);
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(<NewDocDateTitleSettings />);
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(<NewDocDateTitleSettings />);
expect(
screen.queryByTestId('new-doc-date-title-format-trigger')
).toBeNull();
expect(screen.queryByText('New doc date format')).toBeNull();
});
});
@@ -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<typeof useI18n>
): 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 (
<>
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.auto-date-title.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.auto-date-title.description'
]()}
>
<Switch
checked={settings.autoTitleNewDocWithCurrentDate}
onChange={onToggleAutoDateTitle}
/>
</SettingRow>
{settings.autoTitleNewDocWithCurrentDate ? (
<SettingRow
name={t[
'com.affine.settings.editorSettings.general.auto-date-title.format.title'
]()}
desc={t[
'com.affine.settings.editorSettings.general.auto-date-title.format.description'
]()}
>
<Menu
contentOptions={menuContentOptions}
items={formatItems.map(item => {
return (
<MenuItem
key={item.value}
selected={item.value === settings.newDocDateTitleFormat}
onSelect={() => onDateTitleFormatChange(item.value)}
>
{item.label}
</MenuItem>
);
})}
>
<MenuTrigger
className={styles.menuTrigger}
data-testid="new-doc-date-title-format-trigger"
>
{
formatItems.find(
item => item.value === settings.newDocDateTitleFormat
)?.label
}
</MenuTrigger>
</Menu>
</SettingRow>
) : null}
</>
);
};
const AISettings = () => {
const t = useI18n();
const { openConfirmModal } = useConfirmModal();
@@ -573,6 +668,7 @@ export const General = () => {
<CustomFontFamilySettings />
<FontSizeSettings />
<NewDocDefaultModeSettings />
<NewDocDateTitleSettings />
{BUILD_CONFIG.isElectron && <SpellCheckSettings />}
{environment.isLinux && <MiddleClickPasteSettings />}
{/* // TODO(@akumatus): implement these settings