mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-24 05:07:06 +08:00
feat(editor): improve dated doc creation (#14791)
#### PR Dependency Tree * **PR #14791** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal)
This commit is contained in:
@@ -2,22 +2,51 @@ import { toast } from '@affine/component';
|
|||||||
import { getStoreManager } from '@affine/core/blocksuite/manager/store';
|
import { getStoreManager } from '@affine/core/blocksuite/manager/store';
|
||||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
|
import {
|
||||||
|
EditorSettingService,
|
||||||
|
resolveNewDocTitle,
|
||||||
|
} from '@affine/core/modules/editor-setting';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { getAFFiNEWorkspaceSchema } from '@affine/core/modules/workspace';
|
import { getAFFiNEWorkspaceSchema } from '@affine/core/modules/workspace';
|
||||||
import { type DocMode } from '@blocksuite/affine/model';
|
import { type DocMode } from '@blocksuite/affine/model';
|
||||||
import type { Workspace } from '@blocksuite/affine/store';
|
import type { Workspace } from '@blocksuite/affine/store';
|
||||||
import { useServices } from '@toeverything/infra';
|
import { LiveData, useLiveData, useServices } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
export const usePageHelper = (docCollection: Workspace) => {
|
export const usePageHelper = (docCollection: Workspace) => {
|
||||||
const { docsService, workbenchService, appSidebarService } = useServices({
|
const {
|
||||||
|
docsService,
|
||||||
|
workbenchService,
|
||||||
|
appSidebarService,
|
||||||
|
editorSettingService,
|
||||||
|
} = useServices({
|
||||||
DocsService,
|
DocsService,
|
||||||
WorkbenchService,
|
WorkbenchService,
|
||||||
AppSidebarService,
|
AppSidebarService,
|
||||||
|
EditorSettingService,
|
||||||
});
|
});
|
||||||
const workbench = workbenchService.workbench;
|
const workbench = workbenchService.workbench;
|
||||||
const docRecordList = docsService.list;
|
const docRecordList = docsService.list;
|
||||||
const appSidebar = appSidebarService.sidebar;
|
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(
|
const createPageAndOpen = useCallback(
|
||||||
(
|
(
|
||||||
@@ -31,7 +60,7 @@ export const usePageHelper = (docCollection: Workspace) => {
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
appSidebar.setHovering(false);
|
appSidebar.setHovering(false);
|
||||||
const page = docsService.createDoc();
|
const page = createBlankDoc();
|
||||||
|
|
||||||
if (mode) {
|
if (mode) {
|
||||||
docRecordList.doc$(page.id).value?.setPrimaryMode(mode);
|
docRecordList.doc$(page.id).value?.setPrimaryMode(mode);
|
||||||
@@ -45,7 +74,7 @@ export const usePageHelper = (docCollection: Workspace) => {
|
|||||||
}
|
}
|
||||||
return page;
|
return page;
|
||||||
},
|
},
|
||||||
[appSidebar, docRecordList, docsService, workbench]
|
[appSidebar, createBlankDoc, docRecordList, workbench]
|
||||||
);
|
);
|
||||||
|
|
||||||
const createEdgelessAndOpen = useCallback(
|
const createEdgelessAndOpen = useCallback(
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { useLiveData, useService, useServices } from '@toeverything/infra';
|
|||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||||
import {
|
import {
|
||||||
type CollectionMeta,
|
type CollectionMeta,
|
||||||
CollectionService,
|
CollectionService,
|
||||||
@@ -340,13 +341,16 @@ export const CollectionOperationCell = ({
|
|||||||
compatibleFavoriteItemsAdapter: favAdapter,
|
compatibleFavoriteItemsAdapter: favAdapter,
|
||||||
workspaceDialogService,
|
workspaceDialogService,
|
||||||
collectionService,
|
collectionService,
|
||||||
docsService,
|
workspaceService,
|
||||||
} = useServices({
|
} = useServices({
|
||||||
CompatibleFavoriteItemsAdapter,
|
CompatibleFavoriteItemsAdapter,
|
||||||
WorkspaceDialogService,
|
WorkspaceDialogService,
|
||||||
CollectionService,
|
CollectionService,
|
||||||
DocsService,
|
WorkspaceService,
|
||||||
});
|
});
|
||||||
|
const { createPage } = usePageHelper(
|
||||||
|
workspaceService.workspace.docCollection
|
||||||
|
);
|
||||||
const collectionId = collectionMeta.id;
|
const collectionId = collectionMeta.id;
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const favourite = useLiveData(
|
const favourite = useLiveData(
|
||||||
@@ -409,9 +413,9 @@ export const CollectionOperationCell = ({
|
|||||||
}, [favAdapter, collectionId, t]);
|
}, [favAdapter, collectionId, t]);
|
||||||
|
|
||||||
const createAndAddDocument = useCallback(() => {
|
const createAndAddDocument = useCallback(() => {
|
||||||
const newDoc = docsService.createDoc();
|
const newDoc = createPage(undefined, { show: false });
|
||||||
collectionService.addDocToCollection(collectionId, newDoc.id);
|
collectionService.addDocToCollection(collectionId, newDoc.id);
|
||||||
}, [docsService, collectionService, collectionId]);
|
}, [collectionService, collectionId, createPage]);
|
||||||
|
|
||||||
const onConfirmAddDocToCollection = useCallback(() => {
|
const onConfirmAddDocToCollection = useCallback(() => {
|
||||||
openConfirmModal({
|
openConfirmModal({
|
||||||
|
|||||||
+19
-25
@@ -430,26 +430,22 @@ const NewDocDefaultModeSettings = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNewDocDateTitleFormatItems = (
|
const getDateTitleFormatLabel = (format: NewDocDateTitleFormat) => {
|
||||||
t: ReturnType<typeof useI18n>
|
return `com.affine.settings.editorSettings.general.auto-date-title.format.${format.toLowerCase()}` as const;
|
||||||
): 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 = () => {
|
export const NewDocDateTitleSettings = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { editorSettingService } = useServices({ EditorSettingService });
|
const { editorSettingService } = useServices({ EditorSettingService });
|
||||||
const settings = useLiveData(editorSettingService.editorSetting.settings$);
|
const settings = useLiveData(editorSettingService.editorSetting.settings$);
|
||||||
|
const formatItems = useMemo(
|
||||||
const formatItems = useMemo(() => getNewDocDateTitleFormatItems(t), [t]);
|
() =>
|
||||||
|
newDocDateTitleFormatOptions.map(value => ({
|
||||||
|
value,
|
||||||
|
label: t.t(getDateTitleFormatLabel(value)),
|
||||||
|
})),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
const onToggleAutoDateTitle = useCallback(
|
const onToggleAutoDateTitle = useCallback(
|
||||||
(checked: boolean) => {
|
(checked: boolean) => {
|
||||||
@@ -494,17 +490,15 @@ export const NewDocDateTitleSettings = () => {
|
|||||||
>
|
>
|
||||||
<Menu
|
<Menu
|
||||||
contentOptions={menuContentOptions}
|
contentOptions={menuContentOptions}
|
||||||
items={formatItems.map(item => {
|
items={formatItems.map(item => (
|
||||||
return (
|
<MenuItem
|
||||||
<MenuItem
|
key={item.value}
|
||||||
key={item.value}
|
selected={item.value === settings.newDocDateTitleFormat}
|
||||||
selected={item.value === settings.newDocDateTitleFormat}
|
onSelect={() => onDateTitleFormatChange(item.value)}
|
||||||
onSelect={() => onDateTitleFormatChange(item.value)}
|
>
|
||||||
>
|
{item.label}
|
||||||
{item.label}
|
</MenuItem>
|
||||||
</MenuItem>
|
))}
|
||||||
);
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<MenuTrigger
|
<MenuTrigger
|
||||||
className={styles.menuTrigger}
|
className={styles.menuTrigger}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export { DocRecordList } from './entities/record-list';
|
|||||||
export { DocCreated } from './events';
|
export { DocCreated } from './events';
|
||||||
export { DocScope } from './scopes/doc';
|
export { DocScope } from './scopes/doc';
|
||||||
export { DocService } from './services/doc';
|
export { DocService } from './services/doc';
|
||||||
export { DocsQueryService, DocsService } from './services/docs';
|
export { DocsService } from './services/docs';
|
||||||
|
|
||||||
import type { Framework } from '@toeverything/infra';
|
import type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ import { DocRecordList } from './entities/record-list';
|
|||||||
import { DocCreateMiddleware } from './providers/doc-create-middleware';
|
import { DocCreateMiddleware } from './providers/doc-create-middleware';
|
||||||
import { DocScope } from './scopes/doc';
|
import { DocScope } from './scopes/doc';
|
||||||
import { DocService } from './services/doc';
|
import { DocService } from './services/doc';
|
||||||
import { DocsQueryService, DocsService } from './services/docs';
|
import { DocsService } from './services/docs';
|
||||||
import { DocPropertiesStore } from './stores/doc-properties';
|
import { DocPropertiesStore } from './stores/doc-properties';
|
||||||
import { DocsStore } from './stores/docs';
|
import { DocsStore } from './stores/docs';
|
||||||
|
|
||||||
@@ -26,11 +26,10 @@ export { DocCreateMiddleware } from './providers/doc-create-middleware';
|
|||||||
export function configureDocModule(framework: Framework) {
|
export function configureDocModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
.scope(WorkspaceScope)
|
.scope(WorkspaceScope)
|
||||||
.service(DocsQueryService, [DocsStore, DocPropertiesStore])
|
|
||||||
.service(DocsService, [
|
.service(DocsService, [
|
||||||
DocsStore,
|
DocsStore,
|
||||||
|
DocPropertiesStore,
|
||||||
[DocCreateMiddleware],
|
[DocCreateMiddleware],
|
||||||
DocsQueryService,
|
|
||||||
])
|
])
|
||||||
.store(DocPropertiesStore, [WorkspaceService, WorkspaceDBService])
|
.store(DocPropertiesStore, [WorkspaceService, WorkspaceDBService])
|
||||||
.store(DocsStore, [WorkspaceService, DocPropertiesStore])
|
.store(DocsStore, [WorkspaceService, DocPropertiesStore])
|
||||||
|
|||||||
@@ -22,7 +22,15 @@ import { getDuplicatedDocTitle } from './duplicate-title';
|
|||||||
|
|
||||||
const logger = new DebugLogger('DocsService');
|
const logger = new DebugLogger('DocsService');
|
||||||
|
|
||||||
export class DocsQueryService extends Service {
|
export class DocsService extends Service {
|
||||||
|
list = this.framework.createEntity(DocRecordList);
|
||||||
|
|
||||||
|
pool = new ObjectPool<string, Doc>({
|
||||||
|
onDelete(obj) {
|
||||||
|
obj.scope.dispose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all property values of a property, used for search
|
* Get all property values of a property, used for search
|
||||||
*
|
*
|
||||||
@@ -80,60 +88,11 @@ export class DocsQueryService extends Service {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly store: DocsStore,
|
private readonly store: DocsStore,
|
||||||
private readonly docPropertiesStore: DocPropertiesStore
|
private readonly docPropertiesStore: DocPropertiesStore,
|
||||||
|
private readonly docCreateMiddlewares: DocCreateMiddleware[]
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export class DocsService extends Service {
|
|
||||||
list = this.framework.createEntity(DocRecordList);
|
|
||||||
|
|
||||||
pool = new ObjectPool<string, Doc>({
|
|
||||||
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) {
|
loaded(docId: string) {
|
||||||
const exists = this.pool.get(docId);
|
const exists = this.pool.get(docId);
|
||||||
@@ -206,9 +165,6 @@ export class DocsService extends Service {
|
|||||||
if (options.isTemplate) {
|
if (options.isTemplate) {
|
||||||
docRecord.setProperty('isTemplate', true);
|
docRecord.setProperty('isTemplate', true);
|
||||||
}
|
}
|
||||||
if (options.title?.trim()) {
|
|
||||||
docRecord.setMeta({ title: options.title });
|
|
||||||
}
|
|
||||||
for (const middleware of this.docCreateMiddlewares) {
|
for (const middleware of this.docCreateMiddlewares) {
|
||||||
middleware.afterCreate?.(docRecord, options);
|
middleware.afterCreate?.(docRecord, options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,13 @@ import { describe, expect, test } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
buildNewDocDateTitle,
|
buildNewDocDateTitle,
|
||||||
getUniqueNewDocDateTitle,
|
getUniqueNewDocDateTitle,
|
||||||
|
resolveNewDocTitle,
|
||||||
} from '../utils/date-title';
|
} from '../utils/date-title';
|
||||||
|
|
||||||
describe('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');
|
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');
|
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');
|
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');
|
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', () => {
|
test('increments to the next available duplicate suffix', () => {
|
||||||
expect(
|
expect(
|
||||||
getUniqueNewDocDateTitle({
|
getUniqueNewDocDateTitle({
|
||||||
@@ -59,13 +34,37 @@ describe('date-title', () => {
|
|||||||
).toBe('23-03-2026(4)');
|
).toBe('23-03-2026(4)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not suffix when only duplicate-style titles exist', () => {
|
test('keeps provided titles unchanged', () => {
|
||||||
expect(
|
expect(
|
||||||
getUniqueNewDocDateTitle({
|
resolveNewDocTitle({
|
||||||
existingTitles: ['23-03-2026(2)'],
|
title: 'Typed by user',
|
||||||
|
autoTitleEnabled: true,
|
||||||
|
existingTitles: ['23-03-2026'],
|
||||||
format: 'DD-MM-YYYY',
|
format: 'DD-MM-YYYY',
|
||||||
date: '2026-03-23',
|
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)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
-177
@@ -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<string, unknown>) => {
|
|
||||||
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<string, unknown>;
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,15 +1,10 @@
|
|||||||
import { LiveData, Service } from '@toeverything/infra';
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
import type {
|
import type { DocCreateMiddleware, DocRecord } from '../../doc';
|
||||||
DocCreateMiddleware,
|
|
||||||
DocRecord,
|
|
||||||
DocsQueryService,
|
|
||||||
} from '../../doc';
|
|
||||||
import type { DocCreateOptions } from '../../doc/types';
|
import type { DocCreateOptions } from '../../doc/types';
|
||||||
import type { AppThemeService } from '../../theme';
|
import type { AppThemeService } from '../../theme';
|
||||||
import type { EdgelessDefaultTheme } from '../schema';
|
import type { EdgelessDefaultTheme } from '../schema';
|
||||||
import type { EditorSettingService } from '../services/editor-setting';
|
import type { EditorSettingService } from '../services/editor-setting';
|
||||||
import { getUniqueNewDocDateTitle } from '../utils/date-title';
|
|
||||||
|
|
||||||
const getValueByDefaultTheme = (
|
const getValueByDefaultTheme = (
|
||||||
defaultTheme: EdgelessDefaultTheme,
|
defaultTheme: EdgelessDefaultTheme,
|
||||||
@@ -33,42 +28,23 @@ export class EditorSettingDocCreateMiddleware
|
|||||||
extends Service
|
extends Service
|
||||||
implements DocCreateMiddleware
|
implements DocCreateMiddleware
|
||||||
{
|
{
|
||||||
private readonly allDocTitles$: LiveData<{ id: string; title: string }[]>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly editorSettingService: EditorSettingService,
|
private readonly editorSettingService: EditorSettingService,
|
||||||
private readonly appThemeService: AppThemeService,
|
private readonly appThemeService: AppThemeService
|
||||||
private readonly docsQueryService: DocsQueryService
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.allDocTitles$ = LiveData.from(this.docsQueryService.allDocTitle$(), []);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCurrentDocTitles() {
|
|
||||||
return this.allDocTitles$.value.map(doc => doc.title).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeCreate(docCreateOptions: DocCreateOptions): DocCreateOptions {
|
beforeCreate(docCreateOptions: DocCreateOptions): DocCreateOptions {
|
||||||
// clone the docCreateOptions to avoid mutating the original object
|
// clone the docCreateOptions to avoid mutating the original object
|
||||||
docCreateOptions = {
|
docCreateOptions = {
|
||||||
...docCreateOptions,
|
...docCreateOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const settings = this.editorSettingService.editorSetting.settings$.value;
|
const preferMode =
|
||||||
const preferMode = settings.newDocDefaultMode;
|
this.editorSettingService.editorSetting.settings$.value.newDocDefaultMode;
|
||||||
const mode = preferMode === 'ask' ? 'page' : preferMode;
|
const mode = preferMode === 'ask' ? 'page' : preferMode;
|
||||||
docCreateOptions.primaryMode ??= mode;
|
docCreateOptions.primaryMode ??= mode;
|
||||||
|
|
||||||
if (
|
|
||||||
!docCreateOptions.title?.trim() &&
|
|
||||||
settings.autoTitleNewDocWithCurrentDate
|
|
||||||
) {
|
|
||||||
docCreateOptions.title = getUniqueNewDocDateTitle({
|
|
||||||
existingTitles: this.getCurrentDocTitles(),
|
|
||||||
format: settings.newDocDateTitleFormat,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
docCreateOptions.docProps = {
|
docCreateOptions.docProps = {
|
||||||
...docCreateOptions.docProps,
|
...docCreateOptions.docProps,
|
||||||
note: this.editorSettingService.editorSetting.get('affine:note'),
|
note: this.editorSettingService.editorSetting.get('affine:note'),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type Framework } from '@toeverything/infra';
|
|||||||
|
|
||||||
import { ServersService } from '../cloud';
|
import { ServersService } from '../cloud';
|
||||||
import { DesktopApiService } from '../desktop-api';
|
import { DesktopApiService } from '../desktop-api';
|
||||||
import { DocCreateMiddleware, DocsQueryService } from '../doc';
|
import { DocCreateMiddleware } from '../doc';
|
||||||
import { I18n } from '../i18n';
|
import { I18n } from '../i18n';
|
||||||
import { GlobalState, GlobalStateService } from '../storage';
|
import { GlobalState, GlobalStateService } from '../storage';
|
||||||
import { AppThemeService } from '../theme';
|
import { AppThemeService } from '../theme';
|
||||||
@@ -21,6 +21,7 @@ export {
|
|||||||
newDocDateTitleFormatOptions,
|
newDocDateTitleFormatOptions,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
export { EditorSettingService } from './services/editor-setting';
|
export { EditorSettingService } from './services/editor-setting';
|
||||||
|
export { resolveNewDocTitle } from './utils/date-title';
|
||||||
|
|
||||||
export function configureEditorSettingModule(framework: Framework) {
|
export function configureEditorSettingModule(framework: Framework) {
|
||||||
framework
|
framework
|
||||||
@@ -34,7 +35,6 @@ export function configureEditorSettingModule(framework: Framework) {
|
|||||||
.impl(DocCreateMiddleware, EditorSettingDocCreateMiddleware, [
|
.impl(DocCreateMiddleware, EditorSettingDocCreateMiddleware, [
|
||||||
EditorSettingService,
|
EditorSettingService,
|
||||||
AppThemeService,
|
AppThemeService,
|
||||||
DocsQueryService,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,3 +39,21 @@ export const getUniqueNewDocDateTitle = ({
|
|||||||
|
|
||||||
return `${baseTitle}(${duplicateIndex})`;
|
return `${baseTitle}(${duplicateIndex})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const resolveNewDocTitle = ({
|
||||||
|
title,
|
||||||
|
autoTitleEnabled,
|
||||||
|
existingTitles,
|
||||||
|
format,
|
||||||
|
date = new Date(),
|
||||||
|
}: {
|
||||||
|
title?: string;
|
||||||
|
autoTitleEnabled: boolean;
|
||||||
|
existingTitles: Iterable<string>;
|
||||||
|
format: NewDocDateTitleFormat;
|
||||||
|
date?: dayjs.ConfigType;
|
||||||
|
}) => {
|
||||||
|
if (title?.trim()) return title;
|
||||||
|
if (!autoTitleEnabled) return undefined;
|
||||||
|
return getUniqueNewDocDateTitle({ existingTitles, format, date });
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user