feat(core): at menu journal entries (#8877) (#8935)

fix AF-1790
fix AF-1774
added `DateSelectorDialog` for selecting a date through blocksuite;
added `AtMenuConfigService` for constructing the at menu config
fix AF-1776
fix PD-1942

resubmitted to replace #8877
This commit is contained in:
pengx17
2024-11-27 03:08:11 +00:00
parent e3a8f1e9bd
commit 83c587f8ad
18 changed files with 965 additions and 272 deletions
@@ -1,196 +1,9 @@
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { JournalService } from '@affine/core/modules/journal';
import { I18n } from '@affine/i18n';
import { track } from '@affine/track';
import type { EditorHost } from '@blocksuite/affine/block-std';
import type {
AffineInlineEditor,
LinkedWidgetConfig,
} from '@blocksuite/affine/blocks';
import { LinkedWidgetUtils } from '@blocksuite/affine/blocks';
import type { DocMeta } from '@blocksuite/affine/store';
import { type FrameworkProvider, WorkspaceService } from '@toeverything/infra';
import { AtMenuConfigService } from '@affine/core/modules/at-menu-config/services';
import type { LinkedWidgetConfig } from '@blocksuite/affine/blocks';
import { type FrameworkProvider } from '@toeverything/infra';
function createNewDocMenuGroup(
framework: FrameworkProvider,
query: string,
abort: () => void,
editorHost: EditorHost,
inlineEditor: AffineInlineEditor
) {
const originalNewDocMenuGroup = LinkedWidgetUtils.createNewDocMenuGroup(
query,
abort,
editorHost,
inlineEditor
);
const items = Array.isArray(originalNewDocMenuGroup.items)
? originalNewDocMenuGroup.items
: originalNewDocMenuGroup.items.value;
// Patch the import item, to use the custom import dialog.
const importItemIndex = items.findIndex(item => item.key === 'import');
if (importItemIndex === -1) {
return originalNewDocMenuGroup;
}
const originalImportItem = items[importItemIndex];
const customImportItem = {
...originalImportItem,
action: () => {
abort();
track.doc.editor.atMenu.import();
framework
.get(WorkspaceDialogService)
.open('import', undefined, payload => {
if (!payload) {
return;
}
// If the imported file is a workspace file, insert the entry page node.
const { docIds, entryId, isWorkspaceFile } = payload;
if (isWorkspaceFile && entryId) {
LinkedWidgetUtils.insertLinkedNode({
inlineEditor,
docId: entryId,
});
return;
}
// Otherwise, insert all the doc nodes.
for (const docId of docIds) {
LinkedWidgetUtils.insertLinkedNode({
inlineEditor,
docId,
});
}
});
},
};
// only replace the original import item
items.splice(importItemIndex, 1, customImportItem);
return originalNewDocMenuGroup;
}
// TODO: fix the type
export function createLinkedWidgetConfig(
framework: FrameworkProvider
): Partial<LinkedWidgetConfig> {
return {
getMenus: (
query: string,
abort: () => void,
editorHost: EditorHost,
inlineEditor: AffineInlineEditor
) => {
const currentWorkspace = framework.get(WorkspaceService).workspace;
const rawMetas = currentWorkspace.docCollection.meta.docMetas;
const journalService = framework.get(JournalService);
const isJournal = (d: DocMeta) =>
!!journalService.journalDate$(d.id).value;
const docDisplayMetaService = framework.get(DocDisplayMetaService);
const docMetas = rawMetas
.filter(meta => {
if (isJournal(meta) && !meta.updatedDate) {
return false;
}
return !meta.trash;
})
.map(meta => {
const title = docDisplayMetaService.title$(meta.id, {
reference: true,
}).value;
return {
...meta,
title: I18n.t(title),
};
})
.filter(({ title }) => isFuzzyMatch(title, query));
// TODO need i18n if BlockSuite supported
const MAX_DOCS = 6;
return Promise.resolve([
{
name: 'Link to Doc',
items: docMetas.map(doc => ({
key: doc.id,
name: doc.title,
icon: docDisplayMetaService
.icon$(doc.id, {
type: 'lit',
reference: true,
})
.value(),
action: () => {
abort();
LinkedWidgetUtils.insertLinkedNode({
inlineEditor,
docId: doc.id,
});
track.doc.editor.atMenu.linkDoc();
},
})),
maxDisplay: MAX_DOCS,
overflowText: `${docMetas.length - MAX_DOCS} more docs`,
},
createNewDocMenuGroup(
framework,
query,
abort,
editorHost,
inlineEditor
),
]);
},
mobile: {
useScreenHeight: BUILD_CONFIG.isIOS,
scrollContainer: window,
scrollTopOffset: () => {
const header = document.querySelector('header');
if (!header) return 0;
const { y, height } = header.getBoundingClientRect();
return y + height;
},
},
};
}
/**
* Checks if the name is a fuzzy match of the query.
*
* @example
* ```ts
* const name = 'John Smith';
* const query = 'js';
* const isMatch = isFuzzyMatch(name, query);
* // isMatch: true
* ```
*/
function isFuzzyMatch(name: string, query: string) {
const pureName = name
.trim()
.toLowerCase()
.split('')
.filter(char => char !== ' ')
.join('');
const regex = new RegExp(
query
.split('')
.filter(char => char !== ' ')
.map(item => `${escapeRegExp(item)}.*`)
.join(''),
'i'
);
return regex.test(pureName);
}
function escapeRegExp(input: string) {
// escape regex characters in the input string to prevent regex format errors
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return framework.get(AtMenuConfigService).getConfig();
}
@@ -1,23 +1,17 @@
import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { JournalService } from '@affine/core/modules/journal';
import {
JOURNAL_DATE_FORMAT,
JournalService,
type MaybeDate,
} from '@affine/core/modules/journal';
import { i18nTime } from '@affine/i18n';
import { track } from '@affine/track';
import { Text } from '@blocksuite/affine/store';
import {
type DocProps,
DocsService,
initDocFromProps,
useService,
useServices,
} from '@toeverything/infra';
import { DocsService, useService, useServices } from '@toeverything/infra';
import dayjs from 'dayjs';
import { useCallback, useMemo } from 'react';
import { WorkbenchService } from '../../modules/workbench';
type MaybeDate = Date | string | number;
export const JOURNAL_DATE_FORMAT = 'YYYY-MM-DD';
function isJournalString(j?: string | false) {
return j ? !!j?.match(/^\d{4}-\d{2}-\d{2}$/) : false;
}
@@ -33,73 +27,27 @@ function toDayjs(j?: string | false) {
* @deprecated use `JournalService` directly
*/
export const useJournalHelper = () => {
const { docsService, editorSettingService, journalService } = useServices({
const { journalService } = useServices({
DocsService,
EditorSettingService,
JournalService,
});
/**
* @internal
*/
const _createJournal = useCallback(
(maybeDate: MaybeDate) => {
const day = dayjs(maybeDate);
const title = day.format(JOURNAL_DATE_FORMAT);
const docRecord = docsService.createDoc();
const { doc, release } = docsService.open(docRecord.id);
docsService.list.setPrimaryMode(docRecord.id, 'page');
// set created date to match the journal date
docRecord.setMeta({
createDate: dayjs()
.set('year', day.year())
.set('month', day.month())
.set('date', day.date())
.toDate()
.getTime(),
});
const docProps: DocProps = {
page: { title: new Text(title) },
note: editorSettingService.editorSetting.get('affine:note'),
};
initDocFromProps(doc.blockSuiteDoc, docProps);
release();
journalService.setJournalDate(docRecord.id, title);
return docRecord;
},
[docsService, editorSettingService.editorSetting, journalService]
);
/**
* query all journals by date
*/
const getJournalsByDate = useCallback(
(maybeDate: MaybeDate) => {
return journalService.getJournalsByDate(
dayjs(maybeDate).format(JOURNAL_DATE_FORMAT)
);
},
[journalService]
);
/**
* get journal by date, create one if not exist
*/
const getJournalByDate = useCallback(
(maybeDate: MaybeDate) => {
const pages = getJournalsByDate(maybeDate);
if (pages.length) return pages[0];
return _createJournal(maybeDate);
return journalService.ensureJournalByDate(maybeDate);
},
[_createJournal, getJournalsByDate]
[journalService]
);
return useMemo(
() => ({
getJournalsByDate,
getJournalByDate,
}),
[getJournalsByDate, getJournalByDate]
[getJournalByDate]
);
};