mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-19 02:51:47 +08:00
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:
+152
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user