fix(core): optimize at menu config loading 2 (#9366)

fix AF-2028

<div class='graphite__hidden'>
          <div>🎥 Video uploaded on Graphite:</div>
            <a href="https://app.graphite.dev/media/video/T2klNLEk0wxLh4NRDzhk/7931ddc8-3721-4b7d-b4cb-065f923f295c.mp4">
              <img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/T2klNLEk0wxLh4NRDzhk/7931ddc8-3721-4b7d-b4cb-065f923f295c.mp4">
            </a>
          </div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/T2klNLEk0wxLh4NRDzhk/7931ddc8-3721-4b7d-b4cb-065f923f295c.mp4">Recording at 2024-12-27 11.50.54.mp4</video>
This commit is contained in:
pengx17
2024-12-27 09:26:03 +00:00
parent 1fbb462389
commit 6977b0a948
3 changed files with 158 additions and 127 deletions

View File

@@ -77,8 +77,12 @@ export type LinkedMenuGroup = {
styles?: string; styles?: string;
// maximum quantity displayed by default // maximum quantity displayed by default
maxDisplay?: number; maxDisplay?: number;
// if the menu is loading
loading?: boolean | Signal<boolean>;
// copywriting when display quantity exceeds // copywriting when display quantity exceeds
overflowText?: string; overflowText?: string | Signal<string>;
// loading text
loadingText?: string | Signal<string>;
}; };
export type LinkedDocContext = { export type LinkedDocContext = {

View File

@@ -1,3 +1,4 @@
import { LoadingIcon } from '@blocksuite/affine-block-image';
import type { IconButton } from '@blocksuite/affine-components/icon-button'; import type { IconButton } from '@blocksuite/affine-components/icon-button';
import { MoreHorizontalIcon } from '@blocksuite/affine-components/icons'; import { MoreHorizontalIcon } from '@blocksuite/affine-components/icons';
import { import {
@@ -90,15 +91,26 @@ export class LinkedDocPopover extends SignalWatcher(
private _getActionItems(group: LinkedMenuGroup) { private _getActionItems(group: LinkedMenuGroup) {
const isExpanded = !!this._expanded.get(group.name); const isExpanded = !!this._expanded.get(group.name);
const items = resolveSignal(group.items); let items = resolveSignal(group.items);
if (isExpanded) {
return items;
}
const isOverflow = !!group.maxDisplay && items.length > group.maxDisplay; const isOverflow = !!group.maxDisplay && items.length > group.maxDisplay;
if (isOverflow) { const isLoading = resolveSignal(group.loading);
return items.slice(0, group.maxDisplay).concat({
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`, key: `${group.name} More`,
name: group.overflowText || 'more', name: resolveSignal(group.overflowText) || 'more',
icon: MoreHorizontalIcon, icon: MoreHorizontalIcon,
action: () => { action: () => {
this._expanded.set(group.name, true); this._expanded.set(group.name, true);
@@ -106,6 +118,7 @@ export class LinkedDocPopover extends SignalWatcher(
}, },
}); });
} }
return items; return items;
} }

View File

@@ -1,4 +1,3 @@
import { LoadingIcon } from '@affine/core/blocksuite/presets/blocks/_common/icon';
import { fuzzyMatch } from '@affine/core/utils/fuzzy-match'; import { fuzzyMatch } from '@affine/core/utils/fuzzy-match';
import { I18n, i18nTime } from '@affine/i18n'; import { I18n, i18nTime } from '@affine/i18n';
import track from '@affine/track'; import track from '@affine/track';
@@ -11,6 +10,7 @@ import {
LinkedWidgetUtils, LinkedWidgetUtils,
} from '@blocksuite/affine/blocks'; } from '@blocksuite/affine/blocks';
import { Text } from '@blocksuite/affine/store'; import { Text } from '@blocksuite/affine/store';
import { createSignalFromObservable } from '@blocksuite/affine-shared/utils';
import type { EditorHost } from '@blocksuite/block-std'; import type { EditorHost } from '@blocksuite/block-std';
import { import {
DateTimeIcon, DateTimeIcon,
@@ -18,11 +18,12 @@ import {
NewXxxPageIcon, NewXxxPageIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import type { DocMeta } from '@blocksuite/store'; import type { DocMeta } from '@blocksuite/store';
import { signal } from '@preact/signals-core'; import { computed } from '@preact/signals-core';
import { Service } from '@toeverything/infra'; import { Service } from '@toeverything/infra';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit'; import { html } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import { map } from 'rxjs';
import type { WorkspaceDialogService } from '../../dialogs'; import type { WorkspaceDialogService } from '../../dialogs';
import type { DocsService } from '../../doc'; import type { DocsService } from '../../doc';
@@ -34,7 +35,6 @@ import type { RecentDocsService } from '../../quicksearch';
import type { WorkspaceService } from '../../workspace'; import type { WorkspaceService } from '../../workspace';
const MAX_DOCS = 3; const MAX_DOCS = 3;
const LOAD_CHUNK = 100;
export class AtMenuConfigService extends Service { export class AtMenuConfigService extends Service {
constructor( constructor(
private readonly workspaceService: WorkspaceService, private readonly workspaceService: WorkspaceService,
@@ -75,7 +75,6 @@ export class AtMenuConfigService extends Service {
const rawMetas = currentWorkspace.docCollection.meta.docMetas; const rawMetas = currentWorkspace.docCollection.meta.docMetas;
const isJournal = (d: DocMeta) => const isJournal = (d: DocMeta) =>
!!this.journalService.journalDate$(d.id).value; !!this.journalService.journalDate$(d.id).value;
const docItems = signal<LinkedMenuItem[]>([]);
const docDisplayMetaService = this.docDisplayMetaService; const docDisplayMetaService = this.docDisplayMetaService;
@@ -123,36 +122,32 @@ export class AtMenuConfigService extends Service {
const showRecent = query.trim().length === 0; const showRecent = query.trim().length === 0;
(async () => { if (showRecent) {
const isIndexerLoading = const recentDocs = this.recentDocsService.getRecentDocs();
this.docsSearch.indexer.status$.value.remaining !== undefined && return {
this.docsSearch.indexer.status$.value.remaining > 0; name: I18n.t('com.affine.editor.at-menu.recent-docs'),
items: recentDocs
if (!showRecent && isIndexerLoading) { .map(doc => {
// add a loading item const meta = rawMetas.find(meta => meta.id === doc.id);
docItems.value = [ if (!meta) {
{ return null;
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 item = toDocItem({
const docMetas = ( ...meta,
showRecent highlights: undefined,
? this.recentDocsService.getRecentDocs() });
: await this.searchDocs(query) 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 => { .map(doc => {
const meta = rawMetas.find(meta => meta.id === doc.id); const meta = rawMetas.find(meta => meta.id === doc.id);
@@ -160,47 +155,62 @@ export class AtMenuConfigService extends Service {
return null; return null;
} }
const highlights = 'highlights' in doc ? doc.highlights : undefined; const highlights =
return { 'highlights' in doc ? doc.highlights : undefined;
const docItem = toDocItem({
...meta, ...meta,
highlights, highlights,
}; });
if (!docItem) {
return null;
}
return docItem;
}) })
.filter((m): m is DocMetaWithHighlights => !!m); .filter(m => !!m);
for (const [index, meta] of docMetas.entries()) { return docs;
if (abortSignal.aborted) { })
return; ),
} []
);
const item = toDocItem(meta); const { signal: isIndexerLoading, cleanup: cleanupIndexerLoading } =
if (item) { createSignalFromObservable(
docItems.value = [...docItems.value, item]; this.docsSearch.indexer.status$.pipe(
} map(
status => status.remaining !== undefined && status.remaining > 0
)
),
false
);
if (index % LOAD_CHUNK === 0) { const overflowText = computed(() => {
// use scheduler.yield? const overflowCount = docsSignal.value.length - MAX_DOCS;
await new Promise(resolve => setTimeout(resolve, 0));
}
}
})().catch(console.error);
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;
return I18n.t('com.affine.editor.at-menu.more-docs-hint', { return I18n.t('com.affine.editor.at-menu.more-docs-hint', {
count: overflowCount > 100 ? '100+' : overflowCount, 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( private newDocMenuGroup(
query: string, query: string,
@@ -418,8 +428,9 @@ export class AtMenuConfigService extends Service {
} }
// only search docs by title, excluding blocks // only search docs by title, excluding blocks
private async searchDocs(query: string) { private searchDocs$(query: string) {
const { buckets } = await this.docsSearch.indexer.blockIndex.aggregate( return this.docsSearch.indexer.blockIndex
.aggregate$(
{ {
type: 'boolean', type: 'boolean',
occur: 'must', occur: 'must',
@@ -458,14 +469,17 @@ export class AtMenuConfigService extends Service {
], ],
}, },
} }
); )
const result = buckets.map(bucket => { .pipe(
map(({ buckets }) =>
buckets.map(bucket => {
return { return {
id: bucket.key, id: bucket.key,
title: bucket.hits.nodes[0].fields.content, title: bucket.hits.nodes[0].fields.content,
highlights: bucket.hits.nodes[0].highlights.content[0], highlights: bucket.hits.nodes[0].highlights.content[0],
}; };
}); })
return result; )
);
} }
} }