diff --git a/blocksuite/blocks/src/root-block/widgets/linked-doc/config.ts b/blocksuite/blocks/src/root-block/widgets/linked-doc/config.ts index 60f8083ba60f8..06e226ab7f5a0 100644 --- a/blocksuite/blocks/src/root-block/widgets/linked-doc/config.ts +++ b/blocksuite/blocks/src/root-block/widgets/linked-doc/config.ts @@ -77,8 +77,12 @@ export type LinkedMenuGroup = { styles?: string; // maximum quantity displayed by default maxDisplay?: number; + // if the menu is loading + loading?: boolean | Signal; // copywriting when display quantity exceeds - overflowText?: string; + overflowText?: string | Signal; + // loading text + loadingText?: string | Signal; }; export type LinkedDocContext = { diff --git a/blocksuite/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts b/blocksuite/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts index cb47a7b845438..d1b121bab2f9f 100644 --- a/blocksuite/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts +++ b/blocksuite/blocks/src/root-block/widgets/linked-doc/linked-doc-popover.ts @@ -1,3 +1,4 @@ +import { LoadingIcon } from '@blocksuite/affine-block-image'; import type { IconButton } from '@blocksuite/affine-components/icon-button'; import { MoreHorizontalIcon } from '@blocksuite/affine-components/icons'; import { @@ -90,15 +91,26 @@ export class LinkedDocPopover extends SignalWatcher( private _getActionItems(group: LinkedMenuGroup) { const isExpanded = !!this._expanded.get(group.name); - const items = resolveSignal(group.items); - if (isExpanded) { - return items; - } + let items = resolveSignal(group.items); + const isOverflow = !!group.maxDisplay && items.length > group.maxDisplay; - if (isOverflow) { - return items.slice(0, group.maxDisplay).concat({ + const isLoading = resolveSignal(group.loading); + + items = isExpanded ? items : items.slice(0, group.maxDisplay); + + if (isLoading) { + items = items.concat({ + key: 'loading', + name: resolveSignal(group.loadingText) || 'loading', + icon: LoadingIcon, + action: () => {}, + }); + } + + if (isOverflow && !isExpanded && group.maxDisplay) { + items = items.concat({ key: `${group.name} More`, - name: group.overflowText || 'more', + name: resolveSignal(group.overflowText) || 'more', icon: MoreHorizontalIcon, action: () => { this._expanded.set(group.name, true); @@ -106,6 +118,7 @@ export class LinkedDocPopover extends SignalWatcher( }, }); } + return items; } 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 index bdabefec7b700..7b2314e439c66 100644 --- a/packages/frontend/core/src/modules/at-menu-config/services/index.ts +++ b/packages/frontend/core/src/modules/at-menu-config/services/index.ts @@ -1,4 +1,3 @@ -import { LoadingIcon } from '@affine/core/blocksuite/presets/blocks/_common/icon'; import { fuzzyMatch } from '@affine/core/utils/fuzzy-match'; import { I18n, i18nTime } from '@affine/i18n'; import track from '@affine/track'; @@ -11,6 +10,7 @@ import { LinkedWidgetUtils, } from '@blocksuite/affine/blocks'; import { Text } from '@blocksuite/affine/store'; +import { createSignalFromObservable } from '@blocksuite/affine-shared/utils'; import type { EditorHost } from '@blocksuite/block-std'; import { DateTimeIcon, @@ -18,11 +18,12 @@ import { NewXxxPageIcon, } from '@blocksuite/icons/lit'; import type { DocMeta } from '@blocksuite/store'; -import { signal } from '@preact/signals-core'; +import { computed } from '@preact/signals-core'; import { Service } from '@toeverything/infra'; import { cssVarV2 } from '@toeverything/theme/v2'; import { html } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { map } from 'rxjs'; import type { WorkspaceDialogService } from '../../dialogs'; import type { DocsService } from '../../doc'; @@ -34,7 +35,6 @@ import type { RecentDocsService } from '../../quicksearch'; import type { WorkspaceService } from '../../workspace'; const MAX_DOCS = 3; -const LOAD_CHUNK = 100; export class AtMenuConfigService extends Service { constructor( private readonly workspaceService: WorkspaceService, @@ -75,7 +75,6 @@ export class AtMenuConfigService extends Service { const rawMetas = currentWorkspace.docCollection.meta.docMetas; const isJournal = (d: DocMeta) => !!this.journalService.journalDate$(d.id).value; - const docItems = signal([]); const docDisplayMetaService = this.docDisplayMetaService; @@ -123,83 +122,94 @@ export class AtMenuConfigService extends Service { const showRecent = query.trim().length === 0; - (async () => { - const isIndexerLoading = - this.docsSearch.indexer.status$.value.remaining !== undefined && - this.docsSearch.indexer.status$.value.remaining > 0; - - if (!showRecent && isIndexerLoading) { - // add a loading item - docItems.value = [ - { - key: 'loading', - name: I18n.t('com.affine.editor.at-menu.loading'), - icon: LoadingIcon, - action: () => { - // no action - }, - }, - ]; - // wait for indexer to finish - await this.docsSearch.indexer.status$.waitFor( - status => status.remaining === 0 - ); - // remove the loading item - docItems.value = []; - } - - const docMetas = ( - showRecent - ? this.recentDocsService.getRecentDocs() - : await this.searchDocs(query) - ) - .map(doc => { - const meta = rawMetas.find(meta => meta.id === doc.id); - - if (!meta) { - return null; - } - - const highlights = 'highlights' in doc ? doc.highlights : undefined; - return { - ...meta, - highlights, - }; - }) - .filter((m): m is DocMetaWithHighlights => !!m); + if (showRecent) { + const recentDocs = this.recentDocsService.getRecentDocs(); + return { + name: I18n.t('com.affine.editor.at-menu.recent-docs'), + items: recentDocs + .map(doc => { + const meta = rawMetas.find(meta => meta.id === doc.id); + if (!meta) { + return null; + } + const item = toDocItem({ + ...meta, + highlights: undefined, + }); + if (!item) { + return null; + } + return item; + }) + .filter(item => !!item), + }; + } else { + const { signal: docsSignal, cleanup } = createSignalFromObservable( + this.searchDocs$(query).pipe( + map(result => { + const docs = result + .map(doc => { + const meta = rawMetas.find(meta => meta.id === doc.id); + + if (!meta) { + return null; + } + + const highlights = + 'highlights' in doc ? doc.highlights : undefined; + + const docItem = toDocItem({ + ...meta, + highlights, + }); - for (const [index, meta] of docMetas.entries()) { - if (abortSignal.aborted) { - return; - } + if (!docItem) { + return null; + } - const item = toDocItem(meta); - if (item) { - docItems.value = [...docItems.value, item]; - } + return docItem; + }) + .filter(m => !!m); - if (index % LOAD_CHUNK === 0) { - // use scheduler.yield? - await new Promise(resolve => setTimeout(resolve, 0)); - } - } - })().catch(console.error); + return docs; + }) + ), + [] + ); + + const { signal: isIndexerLoading, cleanup: cleanupIndexerLoading } = + createSignalFromObservable( + this.docsSearch.indexer.status$.pipe( + map( + status => status.remaining !== undefined && status.remaining > 0 + ) + ), + false + ); - return { - name: showRecent - ? I18n.t('com.affine.editor.at-menu.recent-docs') - : I18n.t('com.affine.editor.at-menu.link-to-doc', { - query, - }), - items: docItems, - maxDisplay: MAX_DOCS, - get overflowText() { - const overflowCount = docItems.value.length - MAX_DOCS; + const overflowText = computed(() => { + const overflowCount = docsSignal.value.length - MAX_DOCS; return I18n.t('com.affine.editor.at-menu.more-docs-hint', { count: overflowCount > 100 ? '100+' : overflowCount, }); - }, - }; + }); + + abortSignal.addEventListener('abort', () => { + cleanup(); + cleanupIndexerLoading(); + }); + + return { + name: I18n.t('com.affine.editor.at-menu.link-to-doc', { + query, + }), + loading: isIndexerLoading, + loadingText: I18n.t('com.affine.editor.at-menu.loading'), + items: docsSignal, + maxDisplay: MAX_DOCS, + overflowText, + }; + } } private newDocMenuGroup( @@ -418,54 +428,58 @@ export class AtMenuConfigService extends Service { } // only search docs by title, excluding blocks - private async searchDocs(query: string) { - const { buckets } = await this.docsSearch.indexer.blockIndex.aggregate( - { - type: 'boolean', - occur: 'must', - queries: [ - { - type: 'match', - field: 'content', - match: query, - }, - { - type: 'boolean', - occur: 'should', - queries: [ - { - type: 'match', - field: 'flavour', - match: 'affine:page', - }, - ], - }, - ], - }, - 'docId', - { - hits: { - fields: ['docId', 'content'], - pagination: { - limit: 1, - }, - highlights: [ + private searchDocs$(query: string) { + return this.docsSearch.indexer.blockIndex + .aggregate$( + { + type: 'boolean', + occur: 'must', + queries: [ { + type: 'match', field: 'content', - before: ``, - end: '', + match: query, + }, + { + type: 'boolean', + occur: 'should', + queries: [ + { + type: 'match', + field: 'flavour', + match: 'affine:page', + }, + ], }, ], }, - } - ); - const result = buckets.map(bucket => { - return { - id: bucket.key, - title: bucket.hits.nodes[0].fields.content, - highlights: bucket.hits.nodes[0].highlights.content[0], - }; - }); - return result; + 'docId', + { + hits: { + fields: ['docId', 'content'], + pagination: { + limit: 1, + }, + highlights: [ + { + field: 'content', + before: ``, + end: '', + }, + ], + }, + } + ) + .pipe( + map(({ buckets }) => + buckets.map(bucket => { + return { + id: bucket.key, + title: bucket.hits.nodes[0].fields.content, + highlights: bucket.hits.nodes[0].highlights.content[0], + }; + }) + ) + ); } }