diff --git a/packages/common/infra/src/modules/doc/entities/record-list.ts b/packages/common/infra/src/modules/doc/entities/record-list.ts index 2cdfcb556fdb2..3b33386b43f04 100644 --- a/packages/common/infra/src/modules/doc/entities/record-list.ts +++ b/packages/common/infra/src/modules/doc/entities/record-list.ts @@ -17,13 +17,7 @@ export class DocRecordList extends Entity { this.store.watchDocIds().pipe( map(ids => ids.map(id => { - const exists = this.pool.get(id); - if (exists) { - return exists; - } - const record = this.framework.createEntity(DocRecord, { id }); - this.pool.set(id, record); - return record; + return this.doc(id); }) ) ), @@ -52,6 +46,16 @@ export class DocRecordList extends Entity { false ); + public doc(id: string) { + const exists = this.pool.get(id); + if (exists) { + return exists; + } + const record = this.framework.createEntity(DocRecord, { id }); + this.pool.set(id, record); + return record; + } + public doc$(id: string) { return this.docs$.map(record => record.find(record => record.id === id)); } diff --git a/packages/common/infra/src/modules/doc/entities/record.ts b/packages/common/infra/src/modules/doc/entities/record.ts index eeca7c59820b0..1136c6b474b15 100644 --- a/packages/common/infra/src/modules/doc/entities/record.ts +++ b/packages/common/infra/src/modules/doc/entities/record.ts @@ -31,6 +31,10 @@ export class DocRecord extends Entity<{ id: string }> { { id: this.id } ); + get properties() { + return this.docPropertiesStore.getDocProperties(this.id) as DocProperties; + } + customProperty$(propertyId: string) { return this.properties$.selector( p => p['custom:' + propertyId] @@ -74,6 +78,10 @@ export class DocRecord extends Entity<{ id: string }> { return this.setMeta({ trash: false, trashDate: undefined }); } + get title() { + return this.docsStore.meta.getDocMeta(this.id)?.title ?? ''; + } + title$ = this.meta$.map(meta => meta.title ?? ''); trash$ = this.meta$.map(meta => meta.trash ?? false); diff --git a/packages/common/infra/src/modules/doc/stores/docs.ts b/packages/common/infra/src/modules/doc/stores/docs.ts index 99854744c5b42..fdc170da111b6 100644 --- a/packages/common/infra/src/modules/doc/stores/docs.ts +++ b/packages/common/infra/src/modules/doc/stores/docs.ts @@ -24,6 +24,10 @@ export class DocsStore extends Store { return this.workspaceService.workspace.docCollection.createDoc(); } + get meta() { + return this.workspaceService.workspace.docCollection.meta; + } + watchDocIds() { return yjsObserveByPath( this.workspaceService.workspace.rootYDoc.getMap('meta'), diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts index 1bb0ce4f1984a..2aa40c1fb588f 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts @@ -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 { - 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(); } diff --git a/packages/frontend/core/src/components/hooks/use-journal.ts b/packages/frontend/core/src/components/hooks/use-journal.ts index 7cb0f13306c93..75d52255e4777 100644 --- a/packages/frontend/core/src/components/hooks/use-journal.ts +++ b/packages/frontend/core/src/components/hooks/use-journal.ts @@ -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,43 +27,12 @@ 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 */ @@ -87,11 +50,9 @@ export const useJournalHelper = () => { */ 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( diff --git a/packages/frontend/core/src/desktop/dialogs/index.tsx b/packages/frontend/core/src/desktop/dialogs/index.tsx index c8b827a61efad..b97ce58e2ce34 100644 --- a/packages/frontend/core/src/desktop/dialogs/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/index.tsx @@ -15,6 +15,7 @@ import { ImportDialog } from './import'; import { ImportTemplateDialog } from './import-template'; import { ImportWorkspaceDialog } from './import-workspace'; import { CollectionSelectorDialog } from './selectors/collection'; +import { DateSelectorDialog } from './selectors/date'; import { DocSelectorDialog } from './selectors/doc'; import { TagSelectorDialog } from './selectors/tag'; import { SettingDialog } from './setting'; @@ -36,6 +37,7 @@ const WORKSPACE_DIALOGS = { 'tag-selector': TagSelectorDialog, 'doc-selector': DocSelectorDialog, 'collection-selector': CollectionSelectorDialog, + 'date-selector': DateSelectorDialog, import: ImportDialog, } satisfies { [key in keyof WORKSPACE_DIALOG_SCHEMA]?: React.FC< diff --git a/packages/frontend/core/src/desktop/dialogs/selectors/date.tsx b/packages/frontend/core/src/desktop/dialogs/selectors/date.tsx new file mode 100644 index 0000000000000..95035e72a449a --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/selectors/date.tsx @@ -0,0 +1,76 @@ +import { DatePicker, Menu } from '@affine/component'; +import type { DialogComponentProps } from '@affine/core/modules/dialogs'; +import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant'; +import { useI18n } from '@affine/i18n'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import { useCallback, useState } from 'react'; + +/** + * A global date selector popover, mainly used in blocksuite editor + */ +export const DateSelectorDialog = ({ + close, + position, + onSelect, +}: DialogComponentProps) => { + const [selectedDate, setSelectedDate] = useState(); + + const t = useI18n(); + + const onClose = useCallback( + (open: boolean) => { + if (!open) { + close(); + } + }, + [close] + ); + + const handleSelect = useCallback( + (date?: string) => { + setSelectedDate(date); + onSelect?.(date); + }, + [onSelect] + ); + + return ( + + } + > + {/* hack the menu positioning using the following fixed anchor */} +
+
+ ); +}; diff --git a/packages/frontend/core/src/modules/at-menu-config/index.ts b/packages/frontend/core/src/modules/at-menu-config/index.ts new file mode 100644 index 0000000000000..50bf34e197c40 --- /dev/null +++ b/packages/frontend/core/src/modules/at-menu-config/index.ts @@ -0,0 +1,24 @@ +import { + type Framework, + WorkspaceScope, + WorkspaceService, +} from '@toeverything/infra'; + +import { WorkspaceDialogService } from '../dialogs'; +import { DocDisplayMetaService } from '../doc-display-meta'; +import { JournalService } from '../journal'; +import { RecentDocsService } from '../quicksearch'; +import { AtMenuConfigService } from './services'; + +export function configAtMenuConfigModule(framework: Framework) { + framework + .scope(WorkspaceScope) + .service(AtMenuConfigService, [ + WorkspaceService, + JournalService, + DocDisplayMetaService, + WorkspaceDialogService, + RecentDocsService, + WorkspaceDialogService, + ]); +} diff --git a/packages/frontend/core/src/modules/at-menu-config/services/__tests__/suggest-date.spec.ts b/packages/frontend/core/src/modules/at-menu-config/services/__tests__/suggest-date.spec.ts new file mode 100644 index 0000000000000..f330d91128da0 --- /dev/null +++ b/packages/frontend/core/src/modules/at-menu-config/services/__tests__/suggest-date.spec.ts @@ -0,0 +1,166 @@ +/** + * @vitest-environment happy-dom + */ + +import { JOURNAL_DATE_FORMAT } from '@affine/core/modules/journal'; +import { I18n } from '@affine/i18n'; +import dayjs from 'dayjs'; +import { describe, expect, test } from 'vitest'; + +import { suggestJournalDate } from '../../services/index'; + +describe('suggestJournalDate', () => { + test('today', () => { + expect(suggestJournalDate('t')).toEqual({ + dateString: dayjs().format(JOURNAL_DATE_FORMAT), + alias: I18n.t('com.affine.today'), + }); + }); + + test('yesterday', () => { + expect(suggestJournalDate('y')).toEqual({ + dateString: dayjs().subtract(1, 'day').format(JOURNAL_DATE_FORMAT), + alias: I18n.t('com.affine.yesterday'), + }); + }); + + test('tomorrow', () => { + expect(suggestJournalDate('tm')).toEqual({ + dateString: dayjs().add(1, 'day').format(JOURNAL_DATE_FORMAT), + alias: I18n.t('com.affine.tomorrow'), + }); + }); + + test('last week - monday', () => { + expect(suggestJournalDate('lm')).toEqual({ + dateString: dayjs() + .subtract(1, 'week') + .startOf('week') + .add(1, 'day') + .format(JOURNAL_DATE_FORMAT), + alias: 'Last Monday', + }); + }); + + test('last week - tuesday', () => { + expect(suggestJournalDate('ltt')).toEqual({ + dateString: dayjs() + .subtract(1, 'week') + .startOf('week') + .add(2, 'day') + .format(JOURNAL_DATE_FORMAT), + alias: 'Last Tuesday', + }); + }); + + test('last week - wednesday', () => { + expect(suggestJournalDate('lw')).toEqual({ + dateString: dayjs() + .subtract(1, 'week') + .startOf('week') + .add(3, 'day') + .format(JOURNAL_DATE_FORMAT), + alias: 'Last Wednesday', + }); + }); + + test('last week - thursday', () => { + expect(suggestJournalDate('lth')).toEqual({ + dateString: dayjs() + .subtract(1, 'week') + .startOf('week') + .add(4, 'day') + .format(JOURNAL_DATE_FORMAT), + alias: 'Last Thursday', + }); + }); + + test('last week - friday', () => { + expect(suggestJournalDate('lf')).toEqual({ + dateString: dayjs() + .subtract(1, 'week') + .startOf('week') + .add(5, 'day') + .format(JOURNAL_DATE_FORMAT), + alias: 'Last Friday', + }); + }); + + test('next week - monday', () => { + expect(suggestJournalDate('nm')).toEqual({ + dateString: dayjs() + .add(1, 'week') + .startOf('week') + .add(1, 'day') + .format(JOURNAL_DATE_FORMAT), + alias: 'Next Monday', + }); + }); + + test('next week - tuesday', () => { + expect(suggestJournalDate('nxtus')).toEqual({ + dateString: dayjs() + .add(1, 'week') + .startOf('week') + .add(2, 'day') + .format(JOURNAL_DATE_FORMAT), + alias: 'Next Tuesday', + }); + }); + + test('next week - wednesday', () => { + expect(suggestJournalDate('nw')).toEqual({ + dateString: dayjs() + .add(1, 'week') + .startOf('week') + .add(3, 'day') + .format(JOURNAL_DATE_FORMAT), + alias: 'Next Wednesday', + }); + }); + + test('next week - thursday', () => { + expect(suggestJournalDate('nth')).toEqual({ + dateString: dayjs() + .add(1, 'week') + .startOf('week') + .add(4, 'day') + .format(JOURNAL_DATE_FORMAT), + alias: 'Next Thursday', + }); + }); + + test('next week - friday', () => { + expect(suggestJournalDate('nf')).toEqual({ + dateString: dayjs() + .add(1, 'week') + .startOf('week') + .add(5, 'day') + .format(JOURNAL_DATE_FORMAT), + alias: 'Next Friday', + }); + }); + + test('dec', () => { + const year = dayjs().year(); + const date = dayjs().date(); + expect(suggestJournalDate(`dec`)).toEqual({ + dateString: dayjs(`${year}-12-${date}`).format(JOURNAL_DATE_FORMAT), + }); + }); + + test('dec 1', () => { + const year = dayjs().year(); + expect(suggestJournalDate(`dec 10`)).toEqual({ + dateString: dayjs(`${year}-12-10`).format(JOURNAL_DATE_FORMAT), + }); + }); + + test('dec 33', () => { + const year = dayjs().year(); + const date = dayjs().date(); + expect(suggestJournalDate(`dec 33`)).toEqual({ + dateString: dayjs(`${year}-12-${date}`).format(JOURNAL_DATE_FORMAT), + }); + }); +}); diff --git a/packages/frontend/core/src/modules/at-menu-config/services/index.ts b/packages/frontend/core/src/modules/at-menu-config/services/index.ts new file mode 100644 index 0000000000000..6df2b4ffba441 --- /dev/null +++ b/packages/frontend/core/src/modules/at-menu-config/services/index.ts @@ -0,0 +1,473 @@ +import { I18n, i18nTime } from '@affine/i18n'; +import track from '@affine/track'; +import { + type AffineInlineEditor, + type LinkedMenuGroup, + type LinkedMenuItem, + type LinkedWidgetConfig, + LinkedWidgetUtils, +} from '@blocksuite/affine/blocks'; +import type { EditorHost } from '@blocksuite/block-std'; +import { DateTimeIcon } from '@blocksuite/icons/lit'; +import type { DocMeta } from '@blocksuite/store'; +import { signal } from '@preact/signals-core'; +import type { WorkspaceService } from '@toeverything/infra'; +import { Service } from '@toeverything/infra'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import dayjs from 'dayjs'; +import { html } from 'lit'; + +import type { WorkspaceDialogService } from '../../dialogs'; +import type { DocDisplayMetaService } from '../../doc-display-meta'; +import { JOURNAL_DATE_FORMAT, type JournalService } from '../../journal'; +import type { RecentDocsService } from '../../quicksearch'; + +const MAX_DOCS = 3; +const LOAD_CHUNK = 100; +export class AtMenuConfigService extends Service { + constructor( + private readonly workspaceService: WorkspaceService, + private readonly journalService: JournalService, + private readonly docDisplayMetaService: DocDisplayMetaService, + private readonly dialogService: WorkspaceDialogService, + private readonly recentDocsService: RecentDocsService, + private readonly workspaceDialogService: WorkspaceDialogService + ) { + super(); + } + + // todo(@peng17): maybe refactor the config using entity, so that each config + // can be reactive to the query, instead of recreating the whole config? + getConfig(): Partial { + return { + getMenus: this.getMenusFn(), + mobile: this.getMobileConfig(), + }; + } + + private insertDoc(inlineEditor: AffineInlineEditor, id: string) { + LinkedWidgetUtils.insertLinkedNode({ + inlineEditor, + docId: id, + }); + } + + private linkToDocGroup( + query: string, + close: () => void, + inlineEditor: AffineInlineEditor, + abortSignal: AbortSignal + ): LinkedMenuGroup { + const currentWorkspace = this.workspaceService.workspace; + const rawMetas = currentWorkspace.docCollection.meta.docMetas; + const isJournal = (d: DocMeta) => + !!this.journalService.getJournalDate(d.id); + const docItems = signal([]); + + // recent docs should be at the top + const recentDocs = this.recentDocsService.getRecentDocs(); + + const sortedRawMetas = + query.trim().length === 0 + ? rawMetas.toSorted((a, b) => { + const indexA = recentDocs.findIndex(doc => doc.id === a.id); + const indexB = recentDocs.findIndex(doc => doc.id === b.id); + + if (indexA > -1 && indexB < 0) { + return -1; + } else if (indexA < 0 && indexB > -1) { + return 1; + } else if (indexA > -1 && indexB > -1) { + return indexA - indexB; + } + + return Number.MAX_SAFE_INTEGER; + }) + : rawMetas; + + const docDisplayMetaService = this.docDisplayMetaService; + + const toDocItem = (meta: DocMeta): LinkedMenuItem | null => { + if (isJournal(meta) && !meta.updatedDate) { + return null; + } + + if (meta.trash) { + return null; + } + + const title = I18n.t( + docDisplayMetaService.title(meta, { + reference: true, + }) + ); + + if (!fuzzyMatch(title, query)) { + return null; + } + + return { + name: title, + key: meta.id, + icon: docDisplayMetaService.icon(meta.id, { + type: 'lit', + reference: true, + })(), + action: () => { + close(); + track.doc.editor.atMenu.linkDoc(); + this.insertDoc(inlineEditor, meta.id); + }, + }; + }; + + (async () => { + for (const [index, meta] of sortedRawMetas.entries()) { + if (abortSignal.aborted) { + return; + } + + const item = toDocItem(meta); + if (item) { + docItems.value = [...docItems.value, item]; + } + + if (index % LOAD_CHUNK === 0) { + // use scheduler.yield? + await new Promise(resolve => setTimeout(resolve, 0)); + } + } + })().catch(console.error); + + return { + name: I18n.t('com.affine.editor.at-menu.link-to-doc'), + items: docItems, + maxDisplay: MAX_DOCS, + get overflowText() { + const overflowCount = docItems.value.length - MAX_DOCS; + return I18n.t('com.affine.editor.at-menu.more-docs-hint', { + count: overflowCount > 100 ? '100+' : overflowCount, + }); + }, + }; + } + + private newDocMenuGroup( + query: string, + close: () => void, + editorHost: EditorHost, + inlineEditor: AffineInlineEditor + ): LinkedMenuGroup { + const originalNewDocMenuGroup = LinkedWidgetUtils.createNewDocMenuGroup( + query, + close, + editorHost, + inlineEditor + ); + + // Patch the import item, to use the custom import dialog. + const items = Array.isArray(originalNewDocMenuGroup.items) + ? originalNewDocMenuGroup.items + : originalNewDocMenuGroup.items.value; + + const newDocItem = items.find(item => item.key === 'create'); + const importItem = items.find(item => item.key === 'import'); + + // should have both new doc and import item + if (!newDocItem || !importItem) { + return originalNewDocMenuGroup; + } + + const customNewDocItem: LinkedMenuItem = { + ...newDocItem, + name: I18n.t('com.affine.editor.at-menu.create-doc', { + name: query || I18n.t('Untitled'), + }), + }; + + const customImportItem: LinkedMenuItem = { + ...importItem, + name: I18n.t('com.affine.editor.at-menu.import'), + action: () => { + close(); + track.doc.editor.atMenu.import(); + this.dialogService.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) { + this.insertDoc(inlineEditor, docId); + } + }); + }, + }; + + return { + ...originalNewDocMenuGroup, + name: I18n.t('com.affine.editor.at-menu.new-doc'), + items: [customNewDocItem, customImportItem], + }; + } + + private journalGroup( + query: string, + close: () => void, + inlineEditor: AffineInlineEditor + ): LinkedMenuGroup { + const suggestedDate = suggestJournalDate(query); + + const items: LinkedMenuItem[] = [ + { + icon: DateTimeIcon(), + key: 'date-picker', + name: I18n.t('com.affine.editor.at-menu.date-picker'), + action: () => { + close(); + const { x, y, width, height } = + inlineEditor.rootElement.getBoundingClientRect(); + const id = this.workspaceDialogService.open('date-selector', { + position: [x, y, width, height], + onSelect: date => { + if (date) { + onSelectDate(date); + this.workspaceDialogService.close(id); + } + }, + }); + }, + }, + ]; + + const onSelectDate = (date: string) => { + close(); + const doc = this.journalService.ensureJournalByDate(date); + this.insertDoc(inlineEditor, doc.id); + }; + + if (suggestedDate) { + const { dateString, alias } = suggestedDate; + const dateDisplay = i18nTime(dateString, { + absolute: { accuracy: 'day' }, + }); + + const icon = this.docDisplayMetaService.getJournalIcon(dateString, { + type: 'lit', + }); + + items.unshift({ + icon: icon(), + key: dateString, + name: alias + ? html`${alias}, + ${dateDisplay}` + : dateDisplay, + action: () => { + onSelectDate(dateString); + }, + }); + } + + return { + name: I18n.t('com.affine.editor.at-menu.journal'), + items, + }; + } + + private getMenusFn(): LinkedWidgetConfig['getMenus'] { + return (query, close, editorHost, inlineEditor, abortSignal) => { + return [ + this.journalGroup(query, close, inlineEditor), + this.linkToDocGroup(query, close, inlineEditor, abortSignal), + this.newDocMenuGroup(query, close, editorHost, inlineEditor), + ]; + }; + } + + private getMobileConfig(): Partial { + return { + 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 = fuzzyMatch(name, query); + * // isMatch: true + * ``` + * + * if initialMatch = true, the first char must match as well + */ +function fuzzyMatch(name: string, query: string, matchInitial?: boolean) { + 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' + ); + + if (matchInitial && query.length > 0 && !pureName.startsWith(query[0])) { + return false; + } + + return regex.test(pureName); +} + +function escapeRegExp(input: string) { + // escape regex characters in the input string to prevent regex format errors + return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +// todo: infer locale from user's locale? +const monthNames = Array.from({ length: 12 }, (_, index) => + new Intl.DateTimeFormat('en-US', { month: 'long' }).format( + new Date(2024, index) + ) +); + +// todo: infer locale from user's locale? +const weekDayNames = Array.from({ length: 7 }, (_, index) => + new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format( + new Date(2024, 0, index) + ) +); + +export function suggestJournalDate(query: string): { + dateString: string; + alias?: string; +} | null { + // given a query string, suggest a journal date + // if the query is empty or, starts with "t" AND matches today + // -> suggest today's date + // if the query starts with "y" AND matches "yesterday" + // -> suggest yesterday's date + // if the query starts with "l" AND matches last + // -> suggest last week's date + // if the query starts with "n" AND matches "next" + // -> suggest next week's date + // if the query starts with the first letter of a month and matches the month name + // -> if the trailing part matches a number + // -> suggest the date of the month + // -> otherwise, suggest the current day of the month + // otherwise, return null + query = query.trim().toLowerCase().split(' ').join(''); + + if (query === '' || fuzzyMatch('today', query, true)) { + return { + dateString: dayjs().format(JOURNAL_DATE_FORMAT), + alias: I18n.t('com.affine.today'), + }; + } + + if (fuzzyMatch('tomorrow', query, true)) { + return { + dateString: dayjs().add(1, 'day').format(JOURNAL_DATE_FORMAT), + alias: I18n.t('com.affine.tomorrow'), + }; + } + + if (fuzzyMatch('yesterday', query, true)) { + return { + dateString: dayjs().subtract(1, 'day').format(JOURNAL_DATE_FORMAT), + alias: I18n.t('com.affine.yesterday'), + }; + } + + // next week dates, start from monday + const nextWeekDates = Array.from({ length: 7 }, (_, index) => + dayjs() + .add(1, 'week') + .startOf('week') + .add(index, 'day') + .format(JOURNAL_DATE_FORMAT) + ).map(date => ({ + dateString: date, + alias: I18n.t('com.affine.next-week', { + weekday: weekDayNames[dayjs(date).day()], + }), + })); + + const lastWeekDates = Array.from({ length: 7 }, (_, index) => + dayjs() + .subtract(1, 'week') + .startOf('week') + .add(index, 'day') + .format(JOURNAL_DATE_FORMAT) + ).map(date => ({ + dateString: date, + alias: I18n.t('com.affine.last-week', { + weekday: weekDayNames[dayjs(date).day()], + }), + })); + + for (const date of [...nextWeekDates, ...lastWeekDates]) { + const matched = fuzzyMatch(date.alias, query, true); + if (matched) { + return date; + } + } + + // if query is a string that starts with alphabet letters and/or numbers + const regex = new RegExp(`^([a-z]+)(\\d*)$`, 'i'); + const matched = query.match(regex); + + if (matched) { + const [_, letters, numbers] = matched; + + for (const month of monthNames) { + const monthMatched = fuzzyMatch(month, letters, true); + if (monthMatched) { + let day = numbers ? parseInt(numbers) : dayjs().date(); + const invalidDay = day < 1 || day > 31; + if (invalidDay) { + // fallback to today's day + day = dayjs().date(); + } + const year = dayjs().year(); + return { + dateString: dayjs(`${year}-${month}-${day}`).format( + JOURNAL_DATE_FORMAT + ), + }; + } + } + } + + return null; +} diff --git a/packages/frontend/core/src/modules/dialogs/constant.ts b/packages/frontend/core/src/modules/dialogs/constant.ts index 9b1b04c9506ba..04fd38a5861c8 100644 --- a/packages/frontend/core/src/modules/dialogs/constant.ts +++ b/packages/frontend/core/src/modules/dialogs/constant.ts @@ -51,6 +51,10 @@ export type WORKSPACE_DIALOG_SCHEMA = { init: string[]; onBeforeConfirm?: (ids: string[], cb: () => void) => void; }) => string[]; + 'date-selector': (props: { + position: [number, number, number, number]; // [x, y, width, height] + onSelect?: (date?: string) => void; + }) => string; import: () => { docIds: string[]; entryId?: string; diff --git a/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts b/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts index 0f66163fa15a6..364141b19804c 100644 --- a/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts +++ b/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts @@ -1,5 +1,6 @@ import { extractEmojiIcon } from '@affine/core/utils'; import { i18nTime } from '@affine/i18n'; +import type { DocMeta } from '@blocksuite/affine/store'; import { BlockLinkIcon as LitBlockLinkIcon, EdgelessIcon as LitEdgelessIcon, @@ -90,10 +91,108 @@ export class DocDisplayMetaService extends Service { super(); } + getJournalIcon( + journalDate: string | Dayjs, + options?: DocDisplayIconOptions<'rc'> + ): typeof TodayIcon; + + getJournalIcon( + journalDate: string | Dayjs, + options?: DocDisplayIconOptions<'lit'> + ): typeof LitYesterdayIcon; + + getJournalIcon( + journalDate: string | Dayjs, + options?: DocDisplayIconOptions + ): T extends 'rc' ? typeof TodayIcon : typeof LitTodayIcon; + + getJournalIcon( + journalDate: string | Dayjs, + options?: DocDisplayIconOptions + ) { + const iconSet = icons[options?.type ?? 'rc']; + const day = dayjs(journalDate); + return day.isBefore(dayjs(), 'day') + ? iconSet.YesterdayIcon + : day.isAfter(dayjs(), 'day') + ? iconSet.TomorrowIcon + : iconSet.TodayIcon; + } + + icon(docId: string, options?: DocDisplayIconOptions<'rc'>): typeof TodayIcon; + + icon( + docId: string, + options?: DocDisplayIconOptions<'lit'> + ): typeof LitTodayIcon; + + icon( + docId: string, + options?: DocDisplayIconOptions + ) { + const get = (data$: LiveData) => data$.value; + + const iconSet = icons[options?.type ?? 'rc']; + + const doc = get(this.docsService.list.doc$(docId)); + const title = doc ? get(doc.title$) : ''; + const mode = doc ? get(doc.primaryMode$) : undefined; + const finalMode = options?.mode ?? mode ?? 'page'; + const referenceToNode = !!(options?.reference && options.referenceToNode); + + // increases block link priority + if (referenceToNode) { + return iconSet.BlockLinkIcon; + } + + // journal icon + const journalDate = this._toDayjs( + this.journalService.getJournalDate(docId) + ); + + if (journalDate) { + return this.getJournalIcon(journalDate, options); + } + + // reference icon + if (options?.reference) { + return finalMode === 'edgeless' + ? iconSet.LinkedEdgelessIcon + : iconSet.LinkedPageIcon; + } + + // emoji icon + const enableEmojiIcon = + get(this.featureFlagService.flags.enable_emoji_doc_icon.$) && + options?.enableEmojiIcon !== false; + if (enableEmojiIcon) { + const { emoji } = extractEmojiIcon(title); + if (emoji) return () => emoji; + } + + // default icon + return finalMode === 'edgeless' ? iconSet.EdgelessIcon : iconSet.PageIcon; + } + + icon$( + docId: string, + options?: DocDisplayIconOptions<'rc'> + ): LiveData; + + icon$( + docId: string, + options?: DocDisplayIconOptions<'lit'> + ): LiveData; + + icon$( + docId: string, + options?: DocDisplayIconOptions + ): LiveData; + icon$( docId: string, options?: DocDisplayIconOptions - ): LiveData { + ) { const iconSet = icons[options?.type ?? 'rc']; return LiveData.computed(get => { @@ -112,14 +211,9 @@ export class DocDisplayMetaService extends Service { const journalDate = this._toDayjs( this.journalService.journalDate$(docId).value ); + if (journalDate) { - if (!options?.compareDate) return iconSet.TodayIcon; - const compareDate = dayjs(options?.compareDate); - return journalDate.isBefore(compareDate, 'day') - ? iconSet.YesterdayIcon - : journalDate.isAfter(compareDate, 'day') - ? iconSet.TomorrowIcon - : iconSet.TodayIcon; + return this.getJournalIcon(journalDate, options); } // reference icon @@ -143,6 +237,41 @@ export class DocDisplayMetaService extends Service { }); } + title(idOrMeta: string | DocMeta, options?: DocDisplayTitleOptions) { + const docId = typeof idOrMeta === 'string' ? idOrMeta : idOrMeta.id; + const docTitle = + typeof idOrMeta === 'string' + ? this.docsService.list.doc(idOrMeta).title + : idOrMeta.title; + + const journalDateString = this.journalService.getJournalDate(docId); + + // journal + if (journalDateString) { + return i18nTime(journalDateString, { absolute: { accuracy: 'day' } }); + } + + if (options?.originalTitle) return options.originalTitle; + + // empty title + if (!docTitle) return { i18nKey: 'Untitled' } as const; + + // reference + if (options?.reference) return docTitle; + + // check emoji + const enableEmojiIcon = + this.featureFlagService.flags.enable_emoji_doc_icon.value && + options?.enableEmojiIcon !== false; + if (enableEmojiIcon) { + const { rest } = extractEmojiIcon(docTitle); + return rest; + } + + // default + return docTitle; + } + title$(docId: string, options?: DocDisplayTitleOptions) { return LiveData.computed(get => { const doc = get(this.docsService.list.doc$(docId)); diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts index 26b7181de52e0..213cc6a232253 100644 --- a/packages/frontend/core/src/modules/index.ts +++ b/packages/frontend/core/src/modules/index.ts @@ -2,6 +2,7 @@ import { configureQuotaModule } from '@affine/core/modules/quota'; import { configureInfraModules, type Framework } from '@toeverything/infra'; import { configureAppSidebarModule } from './app-sidebar'; +import { configAtMenuConfigModule } from './at-menu-config'; import { configureCloudModule } from './cloud'; import { configureCollectionModule } from './collection'; import { configureDialogModule } from './dialogs'; @@ -67,4 +68,5 @@ export function configureCommonModules(framework: Framework) { configureDialogModule(framework); configureDocInfoModule(framework); configureOpenInApp(framework); + configAtMenuConfigModule(framework); } diff --git a/packages/frontend/core/src/modules/journal/index.ts b/packages/frontend/core/src/modules/journal/index.ts index 8f97409ca570a..09ecb4a9ee378 100644 --- a/packages/frontend/core/src/modules/journal/index.ts +++ b/packages/frontend/core/src/modules/journal/index.ts @@ -6,17 +6,22 @@ import { WorkspaceScope, } from '@toeverything/infra'; +import { EditorSettingService } from '../editor-setting'; import { JournalService } from './services/journal'; import { JournalDocService } from './services/journal-doc'; import { JournalStore } from './store/journal'; -export { JournalService } from './services/journal'; +export { + JOURNAL_DATE_FORMAT, + JournalService, + type MaybeDate, +} from './services/journal'; export { JournalDocService } from './services/journal-doc'; export function configureJournalModule(framework: Framework) { framework .scope(WorkspaceScope) - .service(JournalService, [JournalStore]) + .service(JournalService, [JournalStore, DocsService, EditorSettingService]) .store(JournalStore, [DocsService]) .scope(DocScope) .service(JournalDocService, [DocService, JournalService]); diff --git a/packages/frontend/core/src/modules/journal/services/journal.ts b/packages/frontend/core/src/modules/journal/services/journal.ts index d78cf6123524d..b5c6d4060b4c4 100644 --- a/packages/frontend/core/src/modules/journal/services/journal.ts +++ b/packages/frontend/core/src/modules/journal/services/journal.ts @@ -1,10 +1,21 @@ -import { LiveData, Service } from '@toeverything/infra'; +import { Text } from '@blocksuite/affine/store'; +import type { DocProps, DocsService } from '@toeverything/infra'; +import { initDocFromProps, LiveData, Service } from '@toeverything/infra'; import dayjs from 'dayjs'; +import type { EditorSettingService } from '../../editor-setting'; import type { JournalStore } from '../store/journal'; +export type MaybeDate = Date | string | number; + +export const JOURNAL_DATE_FORMAT = 'YYYY-MM-DD'; + export class JournalService extends Service { - constructor(private readonly store: JournalStore) { + constructor( + private readonly store: JournalStore, + private readonly docsService: DocsService, + private readonly editorSettingService: EditorSettingService + ) { super(); } @@ -21,6 +32,10 @@ export class JournalService extends Service { }); } + getJournalDate(docId: string) { + return this.store.getDocJournalDate(docId); + } + setJournalDate(docId: string, date: string) { this.store.setDocJournalDate(docId, date); } @@ -32,7 +47,41 @@ export class JournalService extends Service { getJournalsByDate(date: string) { return this.store.getDocsByJournalDate(date); } + journalsByDate$(date: string) { return this.store.docsByJournalDate$(date); } + + private createJournal(maybeDate: MaybeDate) { + const day = dayjs(maybeDate); + const title = day.format(JOURNAL_DATE_FORMAT); + const docRecord = this.docsService.createDoc(); + const { doc, release } = this.docsService.open(docRecord.id); + this.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: this.editorSettingService.editorSetting.get('affine:note'), + }; + initDocFromProps(doc.blockSuiteDoc, docProps); + release(); + this.setJournalDate(docRecord.id, title); + return docRecord; + } + + ensureJournalByDate(maybeDate: MaybeDate) { + const day = dayjs(maybeDate); + const title = day.format(JOURNAL_DATE_FORMAT); + const docs = this.getJournalsByDate(title); + if (docs.length) return docs[0]; + return this.createJournal(maybeDate); + } } diff --git a/packages/frontend/core/src/modules/journal/store/journal.ts b/packages/frontend/core/src/modules/journal/store/journal.ts index 9b935c997b353..481b0617a2caf 100644 --- a/packages/frontend/core/src/modules/journal/store/journal.ts +++ b/packages/frontend/core/src/modules/journal/store/journal.ts @@ -37,6 +37,18 @@ export class JournalStore extends Store { }); } + getDocJournalDate(docId: string) { + const doc = this.docsService.list.doc(docId); + if (!doc) { + return undefined; + } + const journal = doc.properties.journal; + if (journal && !isJournalString(journal)) { + return undefined; + } + return journal ?? undefined; + } + setDocJournalDate(docId: string, date: string) { const doc = this.docsService.list.doc$(docId).value; if (!doc) { diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 3ce64b03417bd..47ed394c98389 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -7,7 +7,7 @@ "en": 100, "es-AR": 14, "es-CL": 15, - "es": 14, + "es": 13, "fr": 67, "hi": 2, "it-IT": 1, @@ -16,7 +16,7 @@ "ko": 79, "pl": 0, "pt-BR": 86, - "ru": 74, + "ru": 73, "sv-SE": 4, "ur": 3, "zh-Hans": 100, diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 00ea9cb885fc6..eafecf2d4f045 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1369,6 +1369,8 @@ "com.affine.toastMessage.successfullyDeleted": "Successfully deleted", "com.affine.today": "Today", "com.affine.tomorrow": "Tomorrow", + "com.affine.last-week": "Last {{weekday}}", + "com.affine.next-week": "Next {{weekday}}", "com.affine.top-tip.mobile": "Limited to view-only on mobile.", "com.affine.trashOperation.delete": "Delete", "com.affine.trashOperation.delete.description": "Once deleted, you can't undo this action. Do you confirm?", @@ -1467,5 +1469,12 @@ "com.affine.m.selector.journal-menu.conflicts": "Duplicate Entries in Today's Journal", "com.affine.attachment.preview.error.title": "Unable to preview this file", "com.affine.attachment.preview.error.subtitle": "file type not supported.", - "com.affine.pdf.page.render.error": "Failed to render page." + "com.affine.pdf.page.render.error": "Failed to render page.", + "com.affine.editor.at-menu.link-to-doc": "Link to Doc", + "com.affine.editor.at-menu.new-doc": "New Doc", + "com.affine.editor.at-menu.create-doc": "Create \"{{name}}\" Doc", + "com.affine.editor.at-menu.import": "Import", + "com.affine.editor.at-menu.more-docs-hint": "{{count}} more docs", + "com.affine.editor.at-menu.journal": "Journal", + "com.affine.editor.at-menu.date-picker": "Select a specific date" } diff --git a/tests/affine-local/e2e/links.spec.ts b/tests/affine-local/e2e/links.spec.ts index bd1a8c6959876..d35b30d937bcf 100644 --- a/tests/affine-local/e2e/links.spec.ts +++ b/tests/affine-local/e2e/links.spec.ts @@ -331,3 +331,90 @@ test('allow switching to embed view when linking to the other document with mode url2.searchParams.delete('refreshKey'); expect(url.toJSON()).toStrictEqual(url2.toJSON()); }); + +test('@ popover should show today menu item', async ({ page }) => { + await page.keyboard.press('Enter'); + await waitForEmptyEditor(page); + await page.keyboard.press('@'); + await expect(page.locator('.linked-doc-popover')).toBeVisible(); + const todayMenuItem = page.locator('.linked-doc-popover').getByText('Today'); + await expect(todayMenuItem).toBeVisible(); + + const textContent = await todayMenuItem.locator('span').textContent(); + await todayMenuItem.click(); + const date = textContent?.trim(); + + // a affine-reference should be created with name date + await expect( + page.locator('affine-reference:has-text("' + date + '")') + ).toBeVisible(); +}); + +test('@ popover with input=tmr', async ({ page }) => { + await page.keyboard.press('Enter'); + await waitForEmptyEditor(page); + await page.keyboard.press('@'); + await page.keyboard.type('tmr'); + await expect(page.locator('.linked-doc-popover')).toBeVisible(); + const tomorrowMenuItem = page + .locator('.linked-doc-popover') + .getByText('Tomorrow'); + await expect(tomorrowMenuItem).toBeVisible(); + + const textContent = await tomorrowMenuItem.locator('span').textContent(); + await tomorrowMenuItem.click(); + + // a affine-reference should be created with name date + await expect( + page.locator('affine-reference:has-text("' + textContent + '")') + ).toBeVisible(); +}); + +test('@ popover with input=dec should create a reference with a December date', async ({ + page, +}) => { + await page.keyboard.press('Enter'); + await waitForEmptyEditor(page); + await page.keyboard.press('@'); + await page.keyboard.type('dc'); + + const decemberMenuItem = page.locator( + '.linked-doc-popover icon-button:has-text("Dec")' + ); + await expect(decemberMenuItem).toBeVisible(); + + const textContent = await decemberMenuItem + .locator('.text-container') + .textContent(); + await decemberMenuItem.click(); + + // a affine-reference should be created with name date + await expect( + page.locator('affine-reference:has-text("' + textContent + '")') + ).toBeVisible(); +}); + +test('@ popover with click "select a specific date" should show a date picker', async ({ + page, +}) => { + await page.keyboard.press('Enter'); + await waitForEmptyEditor(page); + await page.keyboard.press('@'); + + const todayMenuItem = page.locator('.linked-doc-popover').getByText('Today'); + await expect(todayMenuItem).toBeVisible(); + + const textContent = await todayMenuItem.locator('span').textContent(); + const date = textContent?.trim(); + + await page.locator('icon-button:has-text("Select a specific date")').click(); + await expect( + page.locator('[data-is-date-cell][data-is-today=true]') + ).toBeVisible(); + await page.locator('[data-is-date-cell][data-is-today=true]').click(); + + // a affine-reference should be created with name date + await expect( + page.locator('affine-reference:has-text("' + date + '")') + ).toBeVisible(); +}); diff --git a/tools/cli/src/webpack/config.ts b/tools/cli/src/webpack/config.ts index 012d6b7cafd62..cdd9a99710070 100644 --- a/tools/cli/src/webpack/config.ts +++ b/tools/cli/src/webpack/config.ts @@ -149,6 +149,12 @@ export const createConfiguration: ( alias: { yjs: join(workspaceRoot, 'node_modules', 'yjs'), lit: join(workspaceRoot, 'node_modules', 'lit'), + '@preact/signals-core': join( + workspaceRoot, + 'node_modules', + '@preact', + 'signals-core' + ), }, },