diff --git a/packages/frontend/core/src/mobile/pages/workspace/search.tsx b/packages/frontend/core/src/mobile/pages/workspace/search.tsx
index 6911440c2..5ddc07907 100644
--- a/packages/frontend/core/src/mobile/pages/workspace/search.tsx
+++ b/packages/frontend/core/src/mobile/pages/workspace/search.tsx
@@ -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: ,
- })),
+ docList
+ .filter(item => item.id !== 'search-locally')
+ .map(item => ({
+ id: item.payload.docId,
+ icon: item.icon,
+ title: ,
+ })),
[docList]
);
@@ -121,6 +126,7 @@ const WithQueryList = () => {
docs={docs}
collections={collectionList}
tags={tagList}
+ error={error ? UserFriendlyError.fromAny(error).message : null}
/>
);
};
diff --git a/packages/frontend/core/src/mobile/views/search/search-results.tsx b/packages/frontend/core/src/mobile/views/search/search-results.tsx
index 498688686..d1215a92a 100644
--- a/packages/frontend/core/src/mobile/views/search/search-results.tsx
+++ b/packages/frontend/core/src/mobile/views/search/search-results.tsx
@@ -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 (
<>
{title}
+ {error && {error}
}
+
{!docs?.length && !collections?.length && !tags?.length ? (
) : null}
diff --git a/packages/frontend/core/src/mobile/views/search/style.css.ts b/packages/frontend/core/src/mobile/views/search/style.css.ts
index 60537be18..521bbaa44 100644
--- a/packages/frontend/core/src/mobile/views/search/style.css.ts
+++ b/packages/frontend/core/src/mobile/views/search/style.css.ts
@@ -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'),
+});
diff --git a/packages/frontend/core/src/modules/quicksearch/entities/quick-search.ts b/packages/frontend/core/src/modules/quicksearch/entities/quick-search.ts
index ea6b23fb7..f4959c0fc 100644
--- a/packages/frontend/core/src/modules/quicksearch/entities/quick-search.ts
+++ b/packages/frontend/core/src/modules/quicksearch/entities/quick-search.ts
@@ -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);
diff --git a/packages/frontend/core/src/modules/quicksearch/impls/docs.ts b/packages/frontend/core/src/modules/quicksearch/impls/docs.ts
index d240f8fd4..a54ce2999 100644
--- a/packages/frontend/core/src/modules/quicksearch/impls/docs.ts
+++ b/packages/frontend/core/src/modules/quicksearch/impls/docs.ts
@@ -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(null);
- query$ = new LiveData('');
+ lastQuery = '';
items$ = new LiveData[]>([]);
+ searchLocally = false;
+
query = effect(
- throttleTime(1000, undefined, {
+ tap(query => {
+ this.lastQuery = query;
+ }),
+ throttleTime(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();
}
diff --git a/packages/frontend/core/src/modules/quicksearch/index.ts b/packages/frontend/core/src/modules/quicksearch/index.ts
index be3f4f48a..4af1a6f5d 100644
--- a/packages/frontend/core/src/modules/quicksearch/index.ts
+++ b/packages/frontend/core/src/modules/quicksearch/index.ts
@@ -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,
diff --git a/packages/frontend/core/src/modules/quicksearch/providers/quick-search-provider.ts b/packages/frontend/core/src/modules/quicksearch/providers/quick-search-provider.ts
index 8d83df112..746aa2b9d 100644
--- a/packages/frontend/core/src/modules/quicksearch/providers/quick-search-provider.ts
+++ b/packages/frontend/core/src/modules/quicksearch/providers/quick-search-provider.ts
@@ -16,6 +16,7 @@ export interface QuickSearchSession {
query?: (query: string) => void;
loadMore?: () => void;
dispose?: () => void;
+ beforeSubmit?: (item: QuickSearchItem) => boolean;
}
export type QuickSearchSource =
diff --git a/packages/frontend/core/src/modules/quicksearch/services/cmdk.ts b/packages/frontend/core/src/modules/quicksearch/services/cmdk.ts
index c4439d7bc..e5b8d3a52 100644
--- a/packages/frontend/core/src/modules/quicksearch/services/cmdk.ts
+++ b/packages/frontend/core/src/modules/quicksearch/services/cmdk.ts
@@ -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];
}
diff --git a/packages/frontend/core/src/modules/quicksearch/types/item.ts b/packages/frontend/core/src/modules/quicksearch/types/item.ts
index c697da3b8..3555aee52 100644
--- a/packages/frontend/core/src/modules/quicksearch/types/item.ts
+++ b/packages/frontend/core/src/modules/quicksearch/types/item.ts
@@ -18,4 +18,5 @@ export type QuickSearchItem = {
keyBinding?: string;
timestamp?: number;
payload?: P;
+ beforeSubmit?: () => boolean;
} & (P extends NonNullable ? { payload: P } : unknown);
diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts
index c83b603c2..5043fe378 100644
--- a/packages/frontend/i18n/src/i18n.gen.ts
+++ b/packages/frontend/i18n/src/i18n.gen.ts
@@ -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`
*/
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index be3ba7ea1..42c640aed 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -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",