diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx index 98c5dc13d9..3e94a351c1 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx @@ -29,11 +29,11 @@ import { type RootService, } from '@blocksuite/blocks'; import { LinkIcon } from '@blocksuite/icons/rc'; -import type { - DocMode, - DocService, +import { + type DocMode, + type DocService, DocsService, - FrameworkProvider, + type FrameworkProvider, } from '@toeverything/infra'; import { type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators.js'; @@ -337,13 +337,27 @@ export function patchQuickSearchService( if (!query) { logger.error('No user input provided'); } else { - const searchedDoc = ( - await framework.get(DocsSearchService).search(query) - ).at(0); - if (searchedDoc) { + const resolvedDoc = resolveLinkToDoc(query); + if (resolvedDoc) { searchResult = { - docId: searchedDoc.docId, + docId: resolvedDoc.docId, }; + } else if ( + query.startsWith('http://') || + query.startsWith('https://') + ) { + searchResult = { + userInput: query, + }; + } else { + const searchedDoc = ( + await framework.get(DocsSearchService).search(query) + ).at(0); + if (searchedDoc) { + searchResult = { + docId: searchedDoc.docId, + }; + } } } } else { @@ -394,11 +408,15 @@ export function patchQuickSearchService( result.source === 'creation' && result.id === 'creation:create-page' ) { - throw new Error('Not implemented'); - // resolve({ - // docId: 'new-doc', - // isNewDoc: true, - // }); + const docsService = framework.get(DocsService); + const newDoc = docsService.createDoc({ + mode: 'page', + title: result.payload.title, + }); + resolve({ + docId: newDoc.id, + isNewDoc: true, + }); } }, { diff --git a/packages/frontend/core/src/modules/quicksearch/impls/docs.ts b/packages/frontend/core/src/modules/quicksearch/impls/docs.ts index a85af0f1df..0c7dd6326f 100644 --- a/packages/frontend/core/src/modules/quicksearch/impls/docs.ts +++ b/packages/frontend/core/src/modules/quicksearch/impls/docs.ts @@ -1,5 +1,4 @@ -import { EdgelessIcon, PageIcon, TodayIcon } from '@blocksuite/icons/rc'; -import type { DocsService } from '@toeverything/infra'; +import type { DocRecord, DocsService } from '@toeverything/infra'; import { effect, Entity, @@ -12,8 +11,8 @@ import { EMPTY, map, mergeMap, of, switchMap } from 'rxjs'; import type { DocsSearchService } from '../../docs-search'; import { resolveLinkToDoc } from '../../navigation'; -import type { WorkspacePropertiesAdapter } from '../../properties'; import type { QuickSearchSession } from '../providers/quick-search-provider'; +import type { DocDisplayMetaService } from '../services/doc-display-meta'; import type { QuickSearchItem } from '../types/item'; interface DocsPayload { @@ -30,7 +29,7 @@ export class DocsQuickSearchSession constructor( private readonly docsSearchService: DocsSearchService, private readonly docsService: DocsService, - private readonly propertiesAdapter: WorkspacePropertiesAdapter + private readonly docDisplayMetaService: DocDisplayMetaService ) { super(); } @@ -56,62 +55,40 @@ export class DocsQuickSearchSession if (!query) { out = of([] as QuickSearchItem<'docs', DocsPayload>[]); } else { - const maybeLink = resolveLinkToDoc(query); - const docRecord = maybeLink - ? this.docsService.list.doc$(maybeLink.docId).value - : null; - - if (docRecord) { - const docMode = docRecord?.mode$.value; - const icon = this.propertiesAdapter.getJournalPageDateString( - docRecord.id - ) /* is journal */ - ? TodayIcon - : docMode === 'edgeless' - ? EdgelessIcon - : PageIcon; - - out = of([ - { - id: 'doc:' + docRecord.id, - source: 'docs', - group: { - id: 'docs', - label: { - key: 'com.affine.quicksearch.group.searchfor', - options: { query: truncate(query) }, + out = this.docsSearchService.search$(query).pipe( + map(docs => { + const resolvedDoc = resolveLinkToDoc(query); + if ( + resolvedDoc && + !docs.some(doc => doc.docId === resolvedDoc.docId) + ) { + return [ + { + docId: resolvedDoc.docId, + score: 100, + blockId: resolvedDoc.blockId, + blockContent: '', }, - score: 5, - }, - label: { - title: docRecord.title$.value || { key: 'Untitled' }, - }, - score: 100, - icon, - timestamp: docRecord.meta$.value.updatedDate, - payload: { - docId: docRecord.id, - }, - }, - ] as QuickSearchItem<'docs', DocsPayload>[]); - } else { - out = this.docsSearchService.search$(query).pipe( - map(docs => - docs.map(doc => { + ...docs, + ]; + } else { + return docs; + } + }), + map(docs => + docs + .map(doc => { const docRecord = this.docsService.list.doc$(doc.docId).value; - const docMode = docRecord?.mode$.value; - const updatedTime = docRecord?.meta$.value.updatedDate; - - const icon = this.propertiesAdapter.getJournalPageDateString( - doc.docId - ) /* is journal */ - ? TodayIcon - : docMode === 'edgeless' - ? EdgelessIcon - : PageIcon; - + return [doc, docRecord] as const; + }) + .filter( + (props): props is [(typeof props)[0], DocRecord] => !!props[1] + ) + .map(([doc, docRecord]) => { + const { title, icon, updatedDate } = + this.docDisplayMetaService.getDocDisplayMeta(docRecord); return { - id: 'doc:' + doc.docId, + id: 'doc:' + docRecord.id, source: 'docs', group: { id: 'docs', @@ -122,18 +99,17 @@ export class DocsQuickSearchSession score: 5, }, label: { - title: doc.title || { key: 'Untitled' }, + title: title, subTitle: doc.blockContent, }, score: doc.score, icon, - timestamp: updatedTime, + timestamp: updatedDate, payload: doc, } as QuickSearchItem<'docs', DocsPayload>; }) - ) - ); - } + ) + ); } return out.pipe( mergeMap((items: QuickSearchItem<'docs', DocsPayload>[]) => { diff --git a/packages/frontend/core/src/modules/quicksearch/impls/recent-docs.ts b/packages/frontend/core/src/modules/quicksearch/impls/recent-docs.ts index bb8b3c9ec9..f50459c4d2 100644 --- a/packages/frontend/core/src/modules/quicksearch/impls/recent-docs.ts +++ b/packages/frontend/core/src/modules/quicksearch/impls/recent-docs.ts @@ -1,8 +1,7 @@ -import { EdgelessIcon, PageIcon, TodayIcon } from '@blocksuite/icons/rc'; import { Entity, LiveData } from '@toeverything/infra'; -import type { WorkspacePropertiesAdapter } from '../../properties'; import type { QuickSearchSession } from '../providers/quick-search-provider'; +import type { DocDisplayMetaService } from '../services/doc-display-meta'; import type { RecentDocsService } from '../services/recent-pages'; import type { QuickSearchGroup } from '../types/group'; import type { QuickSearchItem } from '../types/item'; @@ -21,7 +20,7 @@ export class RecentDocsQuickSearchSession { constructor( private readonly recentDocsService: RecentDocsService, - private readonly propertiesAdapter: WorkspacePropertiesAdapter + private readonly docDisplayMetaService: DocDisplayMetaService ) { super(); } @@ -40,20 +39,15 @@ export class RecentDocsQuickSearchSession return docRecords.map>( docRecord => { - const icon = this.propertiesAdapter.getJournalPageDateString( - docRecord.id - ) /* is journal */ - ? TodayIcon - : docRecord.mode$.value === 'edgeless' - ? EdgelessIcon - : PageIcon; + const { title, icon } = + this.docDisplayMetaService.getDocDisplayMeta(docRecord); return { id: 'recent-doc:' + docRecord.id, source: 'recent-doc', group: group, label: { - title: docRecord.meta$.value.title || { key: 'Untitled' }, + title: title, }, score: 0, icon, diff --git a/packages/frontend/core/src/modules/quicksearch/index.ts b/packages/frontend/core/src/modules/quicksearch/index.ts index 0408e15193..77feff2e2f 100644 --- a/packages/frontend/core/src/modules/quicksearch/index.ts +++ b/packages/frontend/core/src/modules/quicksearch/index.ts @@ -17,6 +17,7 @@ import { CreationQuickSearchSession } from './impls/creation'; import { DocsQuickSearchSession } from './impls/docs'; import { RecentDocsQuickSearchSession } from './impls/recent-docs'; import { CMDKQuickSearchService } from './services/cmdk'; +import { DocDisplayMetaService } from './services/doc-display-meta'; import { QuickSearchService } from './services/quick-search'; import { RecentDocsService } from './services/recent-pages'; @@ -40,17 +41,18 @@ export function configureQuickSearchModule(framework: Framework) { DocsService, ]) .service(RecentDocsService, [WorkspaceLocalState, DocsService]) + .service(DocDisplayMetaService, [WorkspacePropertiesAdapter]) .entity(QuickSearch) .entity(CommandsQuickSearchSession, [GlobalContextService]) .entity(DocsQuickSearchSession, [ DocsSearchService, DocsService, - WorkspacePropertiesAdapter, + DocDisplayMetaService, ]) .entity(CreationQuickSearchSession) .entity(CollectionsQuickSearchSession, [CollectionService]) .entity(RecentDocsQuickSearchSession, [ RecentDocsService, - WorkspacePropertiesAdapter, + DocDisplayMetaService, ]); } diff --git a/packages/frontend/core/src/modules/quicksearch/services/doc-display-meta.ts b/packages/frontend/core/src/modules/quicksearch/services/doc-display-meta.ts new file mode 100644 index 0000000000..1639555163 --- /dev/null +++ b/packages/frontend/core/src/modules/quicksearch/services/doc-display-meta.ts @@ -0,0 +1,36 @@ +import { i18nTime } from '@affine/i18n'; +import { EdgelessIcon, PageIcon, TodayIcon } from '@blocksuite/icons/rc'; +import type { DocRecord } from '@toeverything/infra'; +import { Service } from '@toeverything/infra'; + +import type { WorkspacePropertiesAdapter } from '../../properties'; + +export class DocDisplayMetaService extends Service { + constructor(private readonly propertiesAdapter: WorkspacePropertiesAdapter) { + super(); + } + + getDocDisplayMeta(docRecord: DocRecord) { + const journalDateString = this.propertiesAdapter.getJournalPageDateString( + docRecord.id + ); + const icon = journalDateString + ? TodayIcon + : docRecord.mode$.value === 'edgeless' + ? EdgelessIcon + : PageIcon; + + const title = journalDateString + ? i18nTime(journalDateString, { absolute: { accuracy: 'day' } }) + : docRecord.meta$.value.title || + ({ + key: 'Untitled', + } as const); + + return { + title: title, + icon: icon, + updatedDate: docRecord.meta$.value.updatedDate, + }; + } +} diff --git a/tests/affine-local/e2e/quick-search.spec.ts b/tests/affine-local/e2e/quick-search.spec.ts index 1c89dba5e8..89e67d4d26 100644 --- a/tests/affine-local/e2e/quick-search.spec.ts +++ b/tests/affine-local/e2e/quick-search.spec.ts @@ -507,3 +507,73 @@ test('can use @ to open quick search to search for doc and insert into canvas', await page.locator('affine-embed-linked-doc-block').dblclick({ force: true }); await expect(page.getByTestId('peek-view-modal')).toBeVisible(); }); + +test('can paste a doc link to create link reference', async ({ page }) => { + await openHomePage(page); + await waitForEditorLoad(page); + const url = page.url(); + await clickNewPageButton(page); + + // goto main content + await page.keyboard.press('Enter'); + + // paste the url + await page.evaluate( + async ([url]) => { + const clipData = { + 'text/plain': url, + }; + const e = new ClipboardEvent('paste', { + clipboardData: new DataTransfer(), + }); + Object.defineProperty(e, 'target', { + writable: false, + value: document, + }); + Object.entries(clipData).forEach(([key, value]) => { + e.clipboardData?.setData(key, value); + }); + document.dispatchEvent(e); + }, + [url] + ); + + // check the link reference + await page.waitForTimeout(500); + await expect( + page.locator('affine-reference:has-text("Write, Draw, Plan all at Once.")') + ).toBeVisible(); + + // can ctrl-z to revert to normal link + await page.keyboard.press('ControlOrMeta+z'); + + // check the normal link + await page.waitForTimeout(500); + await expect(page.locator(`affine-link:has-text("${url}")`)).toBeVisible(); +}); + +test('can use slash menu to insert a newly created doc card', async ({ + page, +}) => { + await openHomePage(page); + await clickNewPageButton(page); + + // goto main content + await page.keyboard.press('Enter'); + + // open slash menu + await page.keyboard.type('/linkedoc', { + delay: 50, + }); + await page.keyboard.press('Enter'); + await expect(page.getByTestId('cmdk-quick-search')).toBeVisible(); + + const testTitle = 'test title'; + await page.locator('[cmdk-input]').fill(testTitle); + await page.keyboard.press('Enter'); + + await expect(page.locator('affine-embed-linked-doc-block')).toBeVisible(); + await expect( + page.locator('.affine-embed-linked-doc-content-title') + ).toContainText(testTitle); +});