mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +00:00
feat(core): quick search support search locally (#12987)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a "search locally" option in the docs quick search, allowing users to perform searches on their local device when supported. * Added new quick search group labels and options for local search, with dynamic UI updates based on search mode. * **Improvements** * Enhanced search responsiveness by reducing input throttling delay. * Added a pre-submission check to improve search item handling. * Improved stability by handling cases where document IDs may be missing during search result processing. * **Localization** * Added English language support for new local search options and labels. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
QuickSearchTagIcon,
|
||||
} from '@affine/core/modules/quicksearch';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { sleep } from '@blocksuite/affine/global/utils';
|
||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||
@@ -105,13 +106,17 @@ const WithQueryList = () => {
|
||||
const docList = useLiveData(searchService.docs.items$);
|
||||
const tagList = useLiveData(searchService.tags.items$);
|
||||
|
||||
const error = useLiveData(searchService.docs.error$);
|
||||
|
||||
const docs = useMemo(
|
||||
() =>
|
||||
docList.map(item => ({
|
||||
id: item.payload.docId,
|
||||
icon: item.icon,
|
||||
title: <SearchResLabel item={item} />,
|
||||
})),
|
||||
docList
|
||||
.filter(item => item.id !== 'search-locally')
|
||||
.map(item => ({
|
||||
id: item.payload.docId,
|
||||
icon: item.icon,
|
||||
title: <SearchResLabel item={item} />,
|
||||
})),
|
||||
[docList]
|
||||
);
|
||||
|
||||
@@ -121,6 +126,7 @@ const WithQueryList = () => {
|
||||
docs={docs}
|
||||
collections={collectionList}
|
||||
tags={tagList}
|
||||
error={error ? UserFriendlyError.fromAny(error).message : null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface SearchResultsProps {
|
||||
docs?: DocCardProps['meta'][];
|
||||
collections?: UniversalSearchResultItemProps['item'][];
|
||||
tags?: UniversalSearchResultItemProps['item'][];
|
||||
error?: any;
|
||||
}
|
||||
|
||||
const Empty = () => {
|
||||
@@ -26,11 +27,14 @@ export const SearchResults = ({
|
||||
docs,
|
||||
collections,
|
||||
tags,
|
||||
error,
|
||||
}: SearchResultsProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.resTitle}>{title}</div>
|
||||
|
||||
{error && <p className={styles.errorMessage}>{error}</p>}
|
||||
|
||||
{!docs?.length && !collections?.length && !tags?.length ? (
|
||||
<Empty />
|
||||
) : null}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import {
|
||||
bodyEmphasized,
|
||||
footnoteRegular,
|
||||
@@ -84,3 +85,9 @@ export const empty = style([
|
||||
color: cssVarV2('text/primary'),
|
||||
},
|
||||
]);
|
||||
|
||||
export const errorMessage = style({
|
||||
padding: '0px 16px 16px',
|
||||
fontSize: cssVar('fontSm'),
|
||||
color: cssVarV2('status/error'),
|
||||
});
|
||||
|
||||
@@ -110,6 +110,9 @@ export class QuickSearch extends Entity {
|
||||
}
|
||||
|
||||
submit(result: QuickSearchItem | null) {
|
||||
if (result && result.beforeSubmit && !result.beforeSubmit?.()) {
|
||||
return;
|
||||
}
|
||||
if (this.state$.value?.callback) {
|
||||
this.state$.value.sessions.forEach(session => session.dispose?.());
|
||||
this.state$.value.callback(result);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ServerFeature } from '@affine/graphql';
|
||||
import { SearchIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
effect,
|
||||
Entity,
|
||||
@@ -8,6 +10,7 @@ import {
|
||||
import { truncate } from 'lodash-es';
|
||||
import { catchError, EMPTY, map, of, switchMap, tap, throttleTime } from 'rxjs';
|
||||
|
||||
import type { WorkspaceServerService } from '../../cloud';
|
||||
import type { DocRecord, DocsService } from '../../doc';
|
||||
import type { DocDisplayMetaService } from '../../doc-display-meta';
|
||||
import type { DocsSearchService } from '../../docs-search';
|
||||
@@ -28,6 +31,7 @@ export class DocsQuickSearchSession
|
||||
{
|
||||
constructor(
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly workspaceServerService: WorkspaceServerService,
|
||||
private readonly docsSearchService: DocsSearchService,
|
||||
private readonly docsService: DocsService,
|
||||
private readonly docDisplayMetaService: DocDisplayMetaService
|
||||
@@ -35,6 +39,11 @@ export class DocsQuickSearchSession
|
||||
super();
|
||||
}
|
||||
|
||||
private readonly isSupportServerIndexer = () =>
|
||||
this.workspaceServerService.server?.config$.value.features.includes(
|
||||
ServerFeature.Indexer
|
||||
) ?? false;
|
||||
|
||||
private readonly isIndexerLoading$ = this.docsSearchService.indexerState$.map(
|
||||
({ completed }) => {
|
||||
return !completed;
|
||||
@@ -45,6 +54,26 @@ export class DocsQuickSearchSession
|
||||
|
||||
isCloudWorkspace = this.workspaceService.workspace.flavour !== 'local';
|
||||
|
||||
searchLocallyItem = {
|
||||
id: 'search-locally',
|
||||
source: 'docs',
|
||||
label: {
|
||||
title: {
|
||||
i18nKey: 'com.affine.quicksearch.search-locally',
|
||||
},
|
||||
},
|
||||
score: 1000,
|
||||
icon: SearchIcon,
|
||||
payload: {
|
||||
docId: '',
|
||||
},
|
||||
beforeSubmit: () => {
|
||||
this.searchLocally = true;
|
||||
this.query(this.lastQuery);
|
||||
return false;
|
||||
},
|
||||
} as QuickSearchItem<'docs', DocsPayload>;
|
||||
|
||||
isLoading$ = LiveData.computed(get => {
|
||||
return (
|
||||
(this.isCloudWorkspace ? false : get(this.isIndexerLoading$)) ||
|
||||
@@ -54,12 +83,17 @@ export class DocsQuickSearchSession
|
||||
|
||||
error$ = new LiveData<any>(null);
|
||||
|
||||
query$ = new LiveData('');
|
||||
lastQuery = '';
|
||||
|
||||
items$ = new LiveData<QuickSearchItem<'docs', DocsPayload>[]>([]);
|
||||
|
||||
searchLocally = false;
|
||||
|
||||
query = effect(
|
||||
throttleTime<string>(1000, undefined, {
|
||||
tap(query => {
|
||||
this.lastQuery = query;
|
||||
}),
|
||||
throttleTime<string>(500, undefined, {
|
||||
leading: false,
|
||||
trailing: true,
|
||||
}),
|
||||
@@ -87,7 +121,9 @@ export class DocsQuickSearchSession
|
||||
group: {
|
||||
id: 'docs',
|
||||
label: {
|
||||
i18nKey: 'com.affine.quicksearch.group.searchfor',
|
||||
i18nKey: this.searchLocally
|
||||
? 'com.affine.quicksearch.group.searchfor-locally'
|
||||
: 'com.affine.quicksearch.group.searchfor',
|
||||
options: { query: truncate(query) },
|
||||
},
|
||||
score: 5,
|
||||
@@ -107,17 +143,29 @@ export class DocsQuickSearchSession
|
||||
}
|
||||
return out.pipe(
|
||||
tap((items: QuickSearchItem<'docs', DocsPayload>[]) => {
|
||||
this.items$.next(items);
|
||||
this.items$.next(
|
||||
this.isSupportServerIndexer() && !this.searchLocally
|
||||
? [...items, this.searchLocallyItem]
|
||||
: items
|
||||
);
|
||||
this.isQueryLoading$.next(false);
|
||||
}),
|
||||
onStart(() => {
|
||||
this.error$.next(null);
|
||||
this.items$.next([]);
|
||||
this.items$.next(
|
||||
this.isSupportServerIndexer() && !this.searchLocally
|
||||
? [this.searchLocallyItem]
|
||||
: []
|
||||
);
|
||||
this.isQueryLoading$.next(true);
|
||||
}),
|
||||
catchError(err => {
|
||||
this.error$.next(err instanceof Error ? err.message : err);
|
||||
this.items$.next([]);
|
||||
this.items$.next(
|
||||
this.isSupportServerIndexer() && !this.searchLocally
|
||||
? [this.searchLocallyItem]
|
||||
: []
|
||||
);
|
||||
this.isQueryLoading$.next(false);
|
||||
return EMPTY;
|
||||
}),
|
||||
@@ -128,10 +176,6 @@ export class DocsQuickSearchSession
|
||||
|
||||
// TODO(@EYHN): load more
|
||||
|
||||
setQuery(query: string) {
|
||||
this.query$.next(query);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.query.unsubscribe();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type Framework } from '@toeverything/infra';
|
||||
|
||||
import { WorkspaceServerService } from '../cloud';
|
||||
import { CollectionService } from '../collection';
|
||||
import { WorkspaceDialogService } from '../dialogs';
|
||||
import { DocsService } from '../doc';
|
||||
@@ -56,6 +57,7 @@ export function configureQuickSearchModule(framework: Framework) {
|
||||
.entity(CommandsQuickSearchSession, [GlobalContextService])
|
||||
.entity(DocsQuickSearchSession, [
|
||||
WorkspaceService,
|
||||
WorkspaceServerService,
|
||||
DocsSearchService,
|
||||
DocsService,
|
||||
DocDisplayMetaService,
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface QuickSearchSession<S, P> {
|
||||
query?: (query: string) => void;
|
||||
loadMore?: () => void;
|
||||
dispose?: () => void;
|
||||
beforeSubmit?: (item: QuickSearchItem<S, P>) => boolean;
|
||||
}
|
||||
|
||||
export type QuickSearchSource<S, P> =
|
||||
|
||||
@@ -60,10 +60,14 @@ export class CMDKQuickSearchService extends Service {
|
||||
|
||||
if (result.source === 'recent-doc' || result.source === 'docs') {
|
||||
const doc: {
|
||||
docId: string;
|
||||
docId?: string;
|
||||
blockId?: string;
|
||||
} = result.payload;
|
||||
|
||||
if (!doc.docId) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.source === 'recent-doc' && track.$.cmdk.recent.recentDocs();
|
||||
result.source === 'docs' &&
|
||||
track.$.cmdk.results.searchResultsDocs();
|
||||
@@ -71,6 +75,7 @@ export class CMDKQuickSearchService extends Service {
|
||||
const options: { docId: string; blockIds?: string[] } = {
|
||||
docId: doc.docId,
|
||||
};
|
||||
|
||||
if (doc.blockId) {
|
||||
options.blockIds = [doc.blockId];
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ export type QuickSearchItem<S = any, P = any> = {
|
||||
keyBinding?: string;
|
||||
timestamp?: number;
|
||||
payload?: P;
|
||||
beforeSubmit?: () => boolean;
|
||||
} & (P extends NonNullable<unknown> ? { payload: P } : unknown);
|
||||
|
||||
@@ -4519,12 +4519,22 @@ export function useAFFiNEI18N(): {
|
||||
* `New`
|
||||
*/
|
||||
["com.affine.quicksearch.group.creation"](): string;
|
||||
/**
|
||||
* `Search locally`
|
||||
*/
|
||||
["com.affine.quicksearch.search-locally"](): string;
|
||||
/**
|
||||
* `Search for "{{query}}"`
|
||||
*/
|
||||
["com.affine.quicksearch.group.searchfor"](options: {
|
||||
readonly query: string;
|
||||
}): string;
|
||||
/**
|
||||
* `Search for "{{query}}" (locally)`
|
||||
*/
|
||||
["com.affine.quicksearch.group.searchfor-locally"](options: {
|
||||
readonly query: string;
|
||||
}): string;
|
||||
/**
|
||||
* `Reset sync`
|
||||
*/
|
||||
|
||||
@@ -1121,7 +1121,9 @@
|
||||
"com.affine.split-view-folder-warning.description": "Split view does not support folders.",
|
||||
"do-not-show-this-again": "Do not show this again",
|
||||
"com.affine.quicksearch.group.creation": "New",
|
||||
"com.affine.quicksearch.search-locally": "Search locally",
|
||||
"com.affine.quicksearch.group.searchfor": "Search for \"{{query}}\"",
|
||||
"com.affine.quicksearch.group.searchfor-locally": "Search for \"{{query}}\" (locally)",
|
||||
"com.affine.resetSyncStatus.button": "Reset sync",
|
||||
"com.affine.resetSyncStatus.description": "This operation may fix some synchronization issues.",
|
||||
"com.affine.rootAppSidebar.collections": "Collections",
|
||||
|
||||
Reference in New Issue
Block a user