diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart index 03a35836e9..a423217fae 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart @@ -20,7 +20,6 @@ class CommandPaletteBloc CommandPaletteBloc() : super(CommandPaletteState.initial()) { _searchListener.start( onResultsChanged: _onResultsChanged, - onResultsClosed: _onResultsClosed, ); _initTrash(); @@ -35,6 +34,7 @@ class CommandPaletteBloc final TrashListener _trashListener = TrashListener(); String? _oldQuery; String? _workspaceId; + int _messagesReceived = 0; @override Future close() { @@ -75,18 +75,22 @@ class CommandPaletteBloc emit(state.copyWith(query: null, isLoading: false, results: [])); } }, - resultsChanged: (results, didClose) { + resultsChanged: (results, max) { if (state.query != _oldQuery) { emit(state.copyWith(results: [])); + _oldQuery = state.query; + _messagesReceived = 0; } + _messagesReceived++; + final searchResults = _filterDuplicates(results.items); searchResults.sort((a, b) => b.score.compareTo(a.score)); emit( state.copyWith( results: searchResults, - isLoading: !didClose, + isLoading: _messagesReceived != max, ), ); }, @@ -94,6 +98,9 @@ class CommandPaletteBloc _workspaceId = workspaceId; emit(state.copyWith(results: [], query: '', isLoading: false)); }, + clearSearch: () { + emit(state.copyWith(results: [], query: '', isLoading: false)); + }, ); }); } @@ -125,6 +132,10 @@ class CommandPaletteBloc final res = [...results]; for (final item in results) { + if (item.data.trim().isEmpty) { + continue; + } + final duplicateIndex = currentItems.indexWhere((a) => a.id == item.id); if (duplicateIndex == -1) { continue; @@ -145,10 +156,7 @@ class CommandPaletteBloc add(CommandPaletteEvent.performSearch(search: value)); void _onResultsChanged(RepeatedSearchResultPB results) => - add(CommandPaletteEvent.resultsChanged(results: results)); - - void _onResultsClosed(RepeatedSearchResultPB results) => - add(CommandPaletteEvent.resultsChanged(results: results, didClose: true)); + add(CommandPaletteEvent.resultsChanged(results: results, max: 2)); } @freezed @@ -161,7 +169,7 @@ class CommandPaletteEvent with _$CommandPaletteEvent { const factory CommandPaletteEvent.resultsChanged({ required RepeatedSearchResultPB results, - @Default(false) bool didClose, + @Default(1) int max, }) = _ResultsChanged; const factory CommandPaletteEvent.trashChanged({ @@ -171,6 +179,8 @@ class CommandPaletteEvent with _$CommandPaletteEvent { const factory CommandPaletteEvent.workspaceChanged({ @Default(null) String? workspaceId, }) = _WorkspaceChanged; + + const factory CommandPaletteEvent.clearSearch() = _ClearSearch; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart index e68c18bd3e..ef7e59e695 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart @@ -59,13 +59,6 @@ class SearchListener { (err) => Log.error(err), ); break; - case SearchNotification.DidCloseResults: - result.fold( - (payload) => _updateDidCloseNotifier?.value = - RepeatedSearchResultPB.fromBuffer(payload), - (err) => Log.error(err), - ); - break; default: break; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart index 187cbaf544..ae61f9d7a2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart @@ -133,8 +133,7 @@ class CommandPaletteModal extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ SearchField(query: state.query, isLoading: state.isLoading), - if ((state.query?.isEmpty ?? true) || - state.isLoading && state.results.isEmpty) ...[ + if (state.query?.isEmpty ?? true) ...[ const Divider(height: 0), Flexible( child: RecentViewsList( @@ -150,6 +149,9 @@ class CommandPaletteModal extends StatelessWidget { results: state.results, ), ), + ] else if ((state.query?.isNotEmpty ?? false) && + !state.isLoading) ...[ + const _NoResultsHint(), ], _CommandPaletteFooter( shouldShow: state.results.isNotEmpty && @@ -163,6 +165,27 @@ class CommandPaletteModal extends StatelessWidget { } } +class _NoResultsHint extends StatelessWidget { + const _NoResultsHint(); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(height: 0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: FlowyText.regular( + LocaleKeys.commandPalette_noResultsHint.tr(), + textAlign: TextAlign.left, + ), + ), + ], + ); + } +} + class _CommandPaletteFooter extends StatelessWidget { const _CommandPaletteFooter({required this.shouldShow}); @@ -177,6 +200,7 @@ class _CommandPaletteFooter extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( + color: Theme.of(context).cardColor, border: Border(top: BorderSide(color: Theme.of(context).dividerColor)), ), child: Row( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart index 713fe5bd14..7ac439dcba 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart @@ -26,12 +26,12 @@ class RecentViewTile extends StatelessWidget { title: Row( children: [ icon, - const HSpace(4), + const HSpace(6), FlowyText(view.name), ], ), - focusColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), - hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), + focusColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), + hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), onTap: () { onSelected(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart index d8c257e897..8e13462130 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; @@ -6,7 +8,6 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class RecentViewsList extends StatelessWidget { @@ -51,7 +52,7 @@ class RecentViewsList extends StatelessWidget { : FlowySvg(view.iconData, size: const Size.square(20)); return RecentViewTile( - icon: icon, + icon: SizedBox(width: 24, child: icon), view: view, onSelected: onSelected, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart index 545905d918..022be101c2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -23,12 +24,24 @@ class SearchField extends StatefulWidget { } class _SearchFieldState extends State { - final focusNode = FocusNode(); + late final FocusNode focusNode; late final controller = TextEditingController(text: widget.query); @override void initState() { super.initState(); + focusNode = FocusNode( + onKeyEvent: (node, event) { + if (node.hasFocus && + event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowDown) { + node.nextFocus(); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + ); focusNode.requestFocus(); controller.selection = TextSelection( baseOffset: 0, @@ -75,34 +88,77 @@ class _SearchFieldState extends State { .textTheme .bodySmall! .copyWith(color: Theme.of(context).colorScheme.error), - // TODO(Mathias): Remove beta when support document/database search - suffix: FlowyTooltip( - message: LocaleKeys.commandPalette_betaTooltip.tr(), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 5, - vertical: 1, + suffix: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedOpacity( + opacity: controller.text.trim().isNotEmpty ? 1 : 0, + duration: const Duration(milliseconds: 200), + child: Builder( + builder: (context) { + final icon = Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AFThemeExtension.of(context).lightGreyHover, + ), + child: const FlowySvg( + FlowySvgs.close_s, + size: Size.square(16), + ), + ); + if (controller.text.isEmpty) { + return icon; + } + + return FlowyTooltip( + message: + LocaleKeys.commandPalette_clearSearchTooltip.tr(), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: controller.text.trim().isNotEmpty + ? _clearSearch + : null, + child: icon, + ), + ), + ); + }, + ), ), - decoration: BoxDecoration( - color: AFThemeExtension.of(context).lightGreyHover, - borderRadius: BorderRadius.circular(4), + const HSpace(8), + // TODO(Mathias): Remove beta when support database search + FlowyTooltip( + message: LocaleKeys.commandPalette_betaTooltip.tr(), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 2, + ), + decoration: BoxDecoration( + color: AFThemeExtension.of(context).lightGreyHover, + borderRadius: BorderRadius.circular(4), + ), + child: FlowyText.semibold( + LocaleKeys.commandPalette_betaLabel.tr(), + fontSize: 11, + lineHeight: 1.2, + ), + ), ), - child: FlowyText.semibold( - LocaleKeys.commandPalette_betaLabel.tr(), - fontSize: 10, - ), - ), + ], ), counterText: "", focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), borderRadius: Corners.s8Border, + borderSide: BorderSide(color: Colors.transparent), ), errorBorder: OutlineInputBorder( + borderRadius: Corners.s8Border, borderSide: BorderSide( color: Theme.of(context).colorScheme.error, ), - borderRadius: Corners.s8Border, ), ), onChanged: (value) => context @@ -111,7 +167,6 @@ class _SearchFieldState extends State { ), ), if (widget.isLoading) ...[ - const HSpace(12), FlowyTooltip( message: LocaleKeys.commandPalette_loadingTooltip.tr(), child: const SizedBox( @@ -125,4 +180,11 @@ class _SearchFieldState extends State { ], ); } + + void _clearSearch() { + controller.clear(); + context + .read() + .add(const CommandPaletteEvent.clearSearch()); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart index a21d1823b3..770f37beee 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; @@ -8,10 +9,11 @@ import 'package:appflowy/workspace/application/command_palette/search_result_ext import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -class SearchResultTile extends StatelessWidget { +class SearchResultTile extends StatefulWidget { const SearchResultTile({ super.key, required this.result, @@ -24,40 +26,123 @@ class SearchResultTile extends StatelessWidget { final bool isTrashed; @override - Widget build(BuildContext context) { - final icon = result.getIcon(); + State createState() => _SearchResultTileState(); +} - return ListTile( - dense: true, - title: Row( - children: [ - if (icon != null) ...[icon, const HSpace(6)], - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isTrashed) ...[ - FlowyText( - LocaleKeys.commandPalette_fromTrashHint.tr(), - color: AFThemeExtension.of(context).textColor.withAlpha(175), - fontSize: 10, - ), - ], - FlowyText(result.data), - ], - ), - ], - ), - focusColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), - hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), +class _SearchResultTileState extends State { + bool _hasFocus = false; + + final focusNode = FocusNode(); + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final icon = widget.result.getIcon(); + final cleanedPreview = _cleanPreview(widget.result.preview); + + return GestureDetector( + behavior: HitTestBehavior.opaque, onTap: () { - onSelected(); + widget.onSelected(); getIt().add( ActionNavigationEvent.performAction( - action: NavigationAction(objectId: result.viewId), + action: NavigationAction(objectId: widget.result.viewId), ), ); }, + child: Focus( + onKeyEvent: (node, event) { + if (event is! KeyDownEvent) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.enter) { + widget.onSelected(); + + getIt().add( + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: widget.result.viewId), + ), + ); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + onFocusChange: (hasFocus) => setState(() => _hasFocus = hasFocus), + child: FlowyHover( + isSelected: () => _hasFocus, + style: HoverStyle( + hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), + foregroundColorOnHover: AFThemeExtension.of(context).textColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (icon != null) ...[ + SizedBox(width: 24, child: icon), + const HSpace(6), + ], + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.isTrashed) ...[ + FlowyText( + LocaleKeys.commandPalette_fromTrashHint.tr(), + color: AFThemeExtension.of(context) + .textColor + .withAlpha(175), + fontSize: 10, + ), + ], + FlowyText(widget.result.data), + ], + ), + ], + ), + if (cleanedPreview.isNotEmpty) ...[ + const VSpace(4), + _DocumentPreview(preview: cleanedPreview), + ], + ], + ), + ), + ), + ), + ); + } + + String _cleanPreview(String preview) { + return preview.replaceAll('\n', ' ').trim(); + } +} + +class _DocumentPreview extends StatelessWidget { + const _DocumentPreview({required this.preview}); + + final String preview; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16) + + const EdgeInsets.only(left: 14), + child: FlowyText.regular( + preview, + color: Theme.of(context).hintColor, + fontSize: 12, + maxLines: 3, + ), ); } } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 56acca07bc..60900d1c78 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -1330,7 +1330,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -1927,6 +1927,7 @@ dependencies = [ "flowy-folder", "flowy-folder-pub", "flowy-search", + "flowy-search-pub", "flowy-server", "flowy-server-pub", "flowy-sqlite", @@ -2216,6 +2217,7 @@ dependencies = [ "flowy-user", "futures", "lib-dispatch", + "lib-infra", "protobuf", "serde", "serde_json", @@ -2232,9 +2234,12 @@ dependencies = [ name = "flowy-search-pub" version = "0.1.0" dependencies = [ + "client-api", "collab", "collab-folder", "flowy-error", + "futures", + "lib-infra", ] [[package]] @@ -2256,6 +2261,7 @@ dependencies = [ "flowy-encrypt", "flowy-error", "flowy-folder-pub", + "flowy-search-pub", "flowy-server-pub", "flowy-storage", "flowy-user-pub", @@ -4808,7 +4814,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.11.0", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -4829,7 +4835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.47", diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index 5f9068878d..8cc7c3e749 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -1519,9 +1519,12 @@ dependencies = [ name = "flowy-search-pub" version = "0.1.0" dependencies = [ + "client-api", "collab", "collab-folder", "flowy-error", + "futures", + "lib-infra", ] [[package]] @@ -1543,6 +1546,7 @@ dependencies = [ "flowy-encrypt", "flowy-error", "flowy-folder-pub", + "flowy-search-pub", "flowy-server-pub", "flowy-storage", "flowy-user-pub", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 3253cd85fa..844f7d57c2 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -1964,6 +1964,7 @@ dependencies = [ "flowy-folder", "flowy-folder-pub", "flowy-search", + "flowy-search-pub", "flowy-server", "flowy-server-pub", "flowy-sqlite", @@ -2253,6 +2254,7 @@ dependencies = [ "flowy-user", "futures", "lib-dispatch", + "lib-infra", "protobuf", "serde", "serde_json", @@ -2269,9 +2271,12 @@ dependencies = [ name = "flowy-search-pub" version = "0.1.0" dependencies = [ + "client-api", "collab", "collab-folder", "flowy-error", + "futures", + "lib-infra", ] [[package]] @@ -2293,6 +2298,7 @@ dependencies = [ "flowy-encrypt", "flowy-error", "flowy-folder-pub", + "flowy-search-pub", "flowy-server-pub", "flowy-storage", "flowy-user-pub", diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 4305c83d32..83c03c87ec 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1876,13 +1876,15 @@ "image": "Image" }, "commandPalette": { - "placeholder": "Type to search for views...", + "placeholder": "Type to search...", "bestMatches": "Best matches", "recentHistory": "Recent history", "navigateHint": "to navigate", "loadingTooltip": "We are looking for results...", "betaLabel": "BETA", - "betaTooltip": "We currently only support searching for pages", - "fromTrashHint": "From trash" + "betaTooltip": "We currently only support searching for pages and content in documents", + "fromTrashHint": "From trash", + "noResultsHint": "We didn't find what you're looking for, try searching for another term.", + "clearSearchTooltip": "Clear search field" } } \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index d90ee2a7b3..17bd8e1646 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1761,6 +1761,7 @@ dependencies = [ "flowy-folder", "flowy-folder-pub", "flowy-search", + "flowy-search-pub", "flowy-server", "flowy-server-pub", "flowy-sqlite", @@ -2048,12 +2049,14 @@ dependencies = [ "flowy-codegen", "flowy-derive", "flowy-error", + "flowy-folder", "flowy-notification", "flowy-search-pub", "flowy-sqlite", "flowy-user", "futures", "lib-dispatch", + "lib-infra", "protobuf", "serde", "serde_json", @@ -2070,9 +2073,12 @@ dependencies = [ name = "flowy-search-pub" version = "0.1.0" dependencies = [ + "client-api", "collab", "collab-folder", "flowy-error", + "futures", + "lib-infra", ] [[package]] @@ -2097,6 +2103,7 @@ dependencies = [ "flowy-encrypt", "flowy-error", "flowy-folder-pub", + "flowy-search-pub", "flowy-server-pub", "flowy-storage", "flowy-user-pub", diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 0e5687ce2e..6cdcb0f5bd 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -24,6 +24,7 @@ flowy-config = { workspace = true } flowy-date = { workspace = true } collab-integrate = { workspace = true } flowy-search = { workspace = true } +flowy-search-pub = { workspace = true } collab-entity = { workspace = true } collab-plugins = { workspace = true } collab = { workspace = true } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs index 23e6af0b51..b31853a803 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs @@ -1,12 +1,20 @@ +use flowy_folder::manager::FolderManager; +use flowy_search::document::handler::DocumentSearchHandler; use flowy_search::folder::handler::FolderSearchHandler; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; +use flowy_search_pub::cloud::SearchCloudService; use std::sync::Arc; pub struct SearchDepsResolver(); impl SearchDepsResolver { - pub async fn resolve(folder_indexer: Arc) -> Arc { + pub async fn resolve( + folder_indexer: Arc, + cloud_service: Arc, + folder_manager: Arc, + ) -> Arc { let folder_handler = Arc::new(FolderSearchHandler::new(folder_indexer)); - Arc::new(SearchManager::new(vec![folder_handler])) + let document_handler = Arc::new(DocumentSearchHandler::new(cloud_service, folder_manager)); + Arc::new(SearchManager::new(vec![folder_handler, document_handler])) } } diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index adbdda6a64..1853fd12d3 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -1,3 +1,5 @@ +use client_api::entity::search_dto::SearchDocumentResponseItem; +use flowy_search_pub::cloud::SearchCloudService; use flowy_storage::{ObjectIdentity, ObjectStorageService}; use std::sync::Arc; @@ -601,3 +603,18 @@ impl ChatCloudService for ServerProvider { }) } } + +#[async_trait] +impl SearchCloudService for ServerProvider { + async fn document_search( + &self, + workspace_id: &str, + query: String, + ) -> Result, FlowyError> { + let server = self.get_server()?; + match server.search_service() { + Some(search_service) => search_service.document_search(workspace_id, query).await, + None => Err(FlowyError::internal().with_context("SearchCloudService not found")), + } + } +} diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index c0cf2b9491..818fdfe356 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -200,7 +200,12 @@ impl AppFlowyCore { ) .await; - let search_manager = SearchDepsResolver::resolve(folder_indexer).await; + let search_manager = SearchDepsResolver::resolve( + folder_indexer, + server_provider.clone(), + folder_manager.clone(), + ) + .await; ( user_manager, diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 968289b5f0..96e157ac23 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -525,8 +525,8 @@ impl DatabaseManager { .into_iter() .map(|value| { value - .into_iter() - .map(|(_k, v)| v.to_string()) + .into_values() + .map(|v| v.to_string()) .collect::>() .join(", ") }) diff --git a/frontend/rust-lib/flowy-search-pub/Cargo.toml b/frontend/rust-lib/flowy-search-pub/Cargo.toml index 1a534d16b3..631f2d2c83 100644 --- a/frontend/rust-lib/flowy-search-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-search-pub/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +lib-infra = { workspace = true } collab = { workspace = true } collab-folder = { workspace = true } - flowy-error = { workspace = true } +client-api = { workspace = true } +futures = { workspace = true } diff --git a/frontend/rust-lib/flowy-search-pub/src/cloud.rs b/frontend/rust-lib/flowy-search-pub/src/cloud.rs new file mode 100644 index 0000000000..f2ffb3c439 --- /dev/null +++ b/frontend/rust-lib/flowy-search-pub/src/cloud.rs @@ -0,0 +1,12 @@ +use client_api::entity::search_dto::SearchDocumentResponseItem; +use flowy_error::FlowyError; +use lib_infra::async_trait::async_trait; + +#[async_trait] +pub trait SearchCloudService: Send + Sync + 'static { + async fn document_search( + &self, + workspace_id: &str, + query: String, + ) -> Result, FlowyError>; +} diff --git a/frontend/rust-lib/flowy-search-pub/src/lib.rs b/frontend/rust-lib/flowy-search-pub/src/lib.rs index 0b8f0b5a5a..ee0ade69c4 100644 --- a/frontend/rust-lib/flowy-search-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-search-pub/src/lib.rs @@ -1 +1,2 @@ +pub mod cloud; pub mod entities; diff --git a/frontend/rust-lib/flowy-search/Cargo.toml b/frontend/rust-lib/flowy-search/Cargo.toml index ac015fb324..dbd2b3ecf1 100644 --- a/frontend/rust-lib/flowy-search/Cargo.toml +++ b/frontend/rust-lib/flowy-search/Cargo.toml @@ -21,10 +21,12 @@ flowy-notification.workspace = true flowy-sqlite.workspace = true flowy-user.workspace = true flowy-search-pub.workspace = true +flowy-folder = { workspace = true } bytes.workspace = true futures.workspace = true lib-dispatch.workspace = true +lib-infra = { workspace = true } protobuf.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/frontend/rust-lib/flowy-search/src/document/handler.rs b/frontend/rust-lib/flowy-search/src/document/handler.rs new file mode 100644 index 0000000000..ffdafb8cc7 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/document/handler.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; + +use flowy_error::FlowyResult; +use flowy_folder::{manager::FolderManager, ViewLayout}; +use flowy_search_pub::cloud::SearchCloudService; +use lib_infra::async_trait::async_trait; + +use crate::{ + entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResultPB}, + services::manager::{SearchHandler, SearchType}, +}; + +pub struct DocumentSearchHandler { + pub cloud_service: Arc, + pub folder_manager: Arc, +} + +impl DocumentSearchHandler { + pub fn new( + cloud_service: Arc, + folder_manager: Arc, + ) -> Self { + Self { + cloud_service, + folder_manager, + } + } +} + +#[async_trait] +impl SearchHandler for DocumentSearchHandler { + fn search_type(&self) -> SearchType { + SearchType::Document + } + + async fn perform_search( + &self, + query: String, + filter: Option, + ) -> FlowyResult> { + let filter = match filter { + Some(filter) => filter, + None => return Ok(vec![]), + }; + + let workspace_id = match filter.workspace_id { + Some(workspace_id) => workspace_id, + None => return Ok(vec![]), + }; + + let results = self + .cloud_service + .document_search(&workspace_id, query) + .await?; + + // Grab all views from folder cache + // Notice that `get_all_view_pb` returns Views that don't include trashed and private views + let mut views = self.folder_manager.get_all_views_pb().await?.into_iter(); + let mut search_results: Vec = vec![]; + + for result in results { + if let Some(view) = views.find(|v| v.id == result.object_id) { + // If there is no View for the result, we don't add it to the results + // If possible we will extract the icon to display for the result + let icon: Option = match view.icon.clone() { + Some(view_icon) => Some(ResultIconPB::from(view_icon)), + None => { + let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into(); + Some(ResultIconPB { + ty: ResultIconTypePB::Icon, + value: view_layout_ty.to_string(), + }) + }, + }; + + search_results.push(SearchResultPB { + index_type: IndexTypePB::Document, + view_id: result.object_id.clone(), + id: result.object_id.clone(), + data: view.name.clone(), + icon, + // We reverse the score, the cloud search score is based on + // 1 being the worst result, and closer to 0 being good result, that is + // the opposite of local search. + score: 1.0 - result.score, + workspace_id: result.workspace_id, + preview: result.preview, + }); + } + } + + Ok(search_results) + } + + /// Ignore for [DocumentSearchHandler] + fn index_count(&self) -> u64 { + 0 + } +} diff --git a/frontend/rust-lib/flowy-search/src/document/mod.rs b/frontend/rust-lib/flowy-search/src/document/mod.rs new file mode 100644 index 0000000000..062ae9d9be --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/document/mod.rs @@ -0,0 +1 @@ +pub mod handler; diff --git a/frontend/rust-lib/flowy-search/src/entities/index_type.rs b/frontend/rust-lib/flowy-search/src/entities/index_type.rs index 0f7a7de2e5..77adc76a97 100644 --- a/frontend/rust-lib/flowy-search/src/entities/index_type.rs +++ b/frontend/rust-lib/flowy-search/src/entities/index_type.rs @@ -3,8 +3,9 @@ use flowy_derive::ProtoBuf_Enum; #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] pub enum IndexTypePB { View = 0, - DocumentBlock = 1, - DatabaseRow = 2, + Document = 1, + DocumentBlock = 2, + DatabaseRow = 3, } impl Default for IndexTypePB { diff --git a/frontend/rust-lib/flowy-search/src/entities/notification.rs b/frontend/rust-lib/flowy-search/src/entities/notification.rs index 64b9872f93..e05f46dd09 100644 --- a/frontend/rust-lib/flowy-search/src/entities/notification.rs +++ b/frontend/rust-lib/flowy-search/src/entities/notification.rs @@ -8,7 +8,7 @@ pub struct SearchResultNotificationPB { pub items: Vec, #[pb(index = 2)] - pub closed: bool, + pub sends: u64, #[pb(index = 3, one_of)] pub channel: Option, @@ -19,7 +19,6 @@ pub enum SearchNotification { #[default] Unknown = 0, DidUpdateResults = 1, - DidCloseResults = 2, } impl std::convert::From for i32 { @@ -32,7 +31,6 @@ impl std::convert::From for SearchNotification { fn from(notification: i32) -> Self { match notification { 1 => SearchNotification::DidUpdateResults, - 2 => SearchNotification::DidCloseResults, _ => SearchNotification::Unknown, } } diff --git a/frontend/rust-lib/flowy-search/src/entities/result.rs b/frontend/rust-lib/flowy-search/src/entities/result.rs index 4830057ee9..0f5ea4dc23 100644 --- a/frontend/rust-lib/flowy-search/src/entities/result.rs +++ b/frontend/rust-lib/flowy-search/src/entities/result.rs @@ -1,5 +1,6 @@ use collab_folder::{IconType, ViewIcon}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_folder::entities::ViewIconPB; use super::IndexTypePB; @@ -31,6 +32,9 @@ pub struct SearchResultPB { #[pb(index = 7)] pub workspace_id: String, + + #[pb(index = 8, one_of)] + pub preview: Option, } impl SearchResultPB { @@ -43,6 +47,7 @@ impl SearchResultPB { icon: self.icon.clone(), score, workspace_id: self.workspace_id.clone(), + preview: self.preview.clone(), } } } @@ -122,3 +127,12 @@ impl From for ResultIconPB { } } } + +impl From for ResultIconPB { + fn from(val: ViewIconPB) -> Self { + ResultIconPB { + ty: IconType::from(val.ty).into(), + value: val.value, + } + } +} diff --git a/frontend/rust-lib/flowy-search/src/folder/entities.rs b/frontend/rust-lib/flowy-search/src/folder/entities.rs index 15123bcbd9..b3837668b8 100644 --- a/frontend/rust-lib/flowy-search/src/folder/entities.rs +++ b/frontend/rust-lib/flowy-search/src/folder/entities.rs @@ -30,6 +30,7 @@ impl From for SearchResultPB { score: 0.0, icon, workspace_id: data.workspace_id, + preview: None, } } } diff --git a/frontend/rust-lib/flowy-search/src/folder/handler.rs b/frontend/rust-lib/flowy-search/src/folder/handler.rs index c97f258105..f92e17cda1 100644 --- a/frontend/rust-lib/flowy-search/src/folder/handler.rs +++ b/frontend/rust-lib/flowy-search/src/folder/handler.rs @@ -3,6 +3,7 @@ use crate::{ services::manager::{SearchHandler, SearchType}, }; use flowy_error::FlowyResult; +use lib_infra::async_trait::async_trait; use std::sync::Arc; use super::indexer::FolderIndexManagerImpl; @@ -17,12 +18,13 @@ impl FolderSearchHandler { } } +#[async_trait] impl SearchHandler for FolderSearchHandler { fn search_type(&self) -> SearchType { SearchType::Folder } - fn perform_search( + async fn perform_search( &self, query: String, filter: Option, diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 146da46a49..5831e0871a 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -298,12 +298,9 @@ impl IndexManager for FolderIndexManagerImpl { let wid = workspace_id.clone(); af_spawn(async move { while let Ok(msg) = rx.recv().await { - tracing::warn!("[Indexer] Message received: {:?}", msg); match msg { IndexContent::Create(value) => match serde_json::from_value::(value) { Ok(view) => { - tracing::warn!("[Indexer] CREATE: {:?}", view); - let _ = indexer.add_index(IndexableData { id: view.id, data: view.name, @@ -316,7 +313,6 @@ impl IndexManager for FolderIndexManagerImpl { }, IndexContent::Update(value) => match serde_json::from_value::(value) { Ok(view) => { - tracing::warn!("[Indexer] UPDATE: {:?}", view); let _ = indexer.update_index(IndexableData { id: view.id, data: view.name, @@ -328,7 +324,6 @@ impl IndexManager for FolderIndexManagerImpl { Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), }, IndexContent::Delete(ids) => { - tracing::warn!("[Indexer] DELETE: {:?}", ids); if let Err(e) = indexer.remove_indices(ids) { tracing::error!("FolderIndexManager error deserialize: {:?}", e); } @@ -459,7 +454,6 @@ impl FolderIndexManager for FolderIndexManagerImpl { } }, FolderViewChange::Deleted { view_ids } => { - tracing::warn!("[Indexer] ViewChange Reached Deleted: {:?}", view_ids); let _ = self.remove_indices(view_ids); }, }; diff --git a/frontend/rust-lib/flowy-search/src/lib.rs b/frontend/rust-lib/flowy-search/src/lib.rs index 9b2ea272d8..820a6d9cb3 100644 --- a/frontend/rust-lib/flowy-search/src/lib.rs +++ b/frontend/rust-lib/flowy-search/src/lib.rs @@ -1,3 +1,4 @@ +pub mod document; pub mod entities; pub mod event_handler; pub mod event_map; diff --git a/frontend/rust-lib/flowy-search/src/services/manager.rs b/frontend/rust-lib/flowy-search/src/services/manager.rs index 487c589d05..aec33a5b60 100644 --- a/frontend/rust-lib/flowy-search/src/services/manager.rs +++ b/frontend/rust-lib/flowy-search/src/services/manager.rs @@ -5,21 +5,27 @@ use super::notifier::{SearchNotifier, SearchResultChanged, SearchResultReceiverR use crate::entities::{SearchFilterPB, SearchResultNotificationPB, SearchResultPB}; use flowy_error::FlowyResult; use lib_dispatch::prelude::af_spawn; -use tokio::{sync::broadcast, task::spawn_blocking}; +use lib_infra::async_trait::async_trait; +use tokio::sync::broadcast; + #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum SearchType { Folder, + Document, } +#[async_trait] pub trait SearchHandler: Send + Sync + 'static { /// returns the type of search this handler is responsible for fn search_type(&self) -> SearchType; + /// performs a search and returns the results - fn perform_search( + async fn perform_search( &self, query: String, filter: Option, ) -> FlowyResult>; + /// returns the number of indexed objects fn index_count(&self) -> u64; } @@ -57,25 +63,22 @@ impl SearchManager { filter: Option, channel: Option, ) { - let mut sends: usize = 0; let max: usize = self.handlers.len(); let handlers = self.handlers.clone(); - for (_, handler) in handlers { let q = query.clone(); let f = filter.clone(); let ch = channel.clone(); let notifier = self.notifier.clone(); - spawn_blocking(move || { - let res = handler.perform_search(q, f); - sends += 1; + af_spawn(async move { + let res = handler.perform_search(q, f).await; - let close = sends == max; let items = res.unwrap_or_default(); + let notification = SearchResultNotificationPB { items, - closed: close, + sends: max as u64, channel: ch, }; diff --git a/frontend/rust-lib/flowy-search/src/services/notifier.rs b/frontend/rust-lib/flowy-search/src/services/notifier.rs index 83ec113daf..abbf5d4b0c 100644 --- a/frontend/rust-lib/flowy-search/src/services/notifier.rs +++ b/frontend/rust-lib/flowy-search/src/services/notifier.rs @@ -31,15 +31,13 @@ impl SearchResultReceiverRunner { .for_each(|changed| async { match changed { SearchResultChanged::SearchResultUpdate(notification) => { - let ty = if notification.closed { - SearchNotification::DidCloseResults - } else { - SearchNotification::DidUpdateResults - }; - - send_notification(SEARCH_ID, ty, notification.channel.clone()) - .payload(notification) - .send(); + send_notification( + SEARCH_ID, + SearchNotification::DidUpdateResults, + notification.channel.clone(), + ) + .payload(notification) + .send(); }, } }) diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index bb16b35cdb..1f15d85088 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -40,6 +40,7 @@ flowy-document-pub = { workspace = true } appflowy-cloud-billing-client = { workspace = true } flowy-error = { workspace = true, features = ["impl_from_serde", "impl_from_reqwest", "impl_from_url", "impl_from_appflowy_cloud"] } flowy-server-pub = { workspace = true } +flowy-search-pub = { workspace = true } flowy-encrypt = { workspace = true } flowy-storage = { workspace = true } flowy-chat-pub = { workspace = true } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/mod.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/mod.rs index 37ad00781c..105129f131 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/mod.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/mod.rs @@ -3,6 +3,7 @@ pub(crate) use database::*; pub(crate) use document::*; pub(crate) use file_storage::*; pub(crate) use folder::*; +pub(crate) use search::*; pub(crate) use user::*; mod chat; @@ -10,5 +11,6 @@ mod database; mod document; mod file_storage; mod folder; +mod search; mod user; mod util; diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs new file mode 100644 index 0000000000..c0bf6e2dea --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs @@ -0,0 +1,40 @@ +use client_api::entity::search_dto::SearchDocumentResponseItem; +use flowy_error::FlowyError; +use flowy_search_pub::cloud::SearchCloudService; +use lib_infra::async_trait::async_trait; + +use crate::af_cloud::AFServer; + +pub(crate) struct AFCloudSearchCloudServiceImpl { + pub inner: T, +} + +// The limit of what the score should be for results, used to +// filter out irrelevant results. +const SCORE_LIMIT: f64 = 0.8; +const DEFAULT_PREVIEW: u32 = 80; + +#[async_trait] +impl SearchCloudService for AFCloudSearchCloudServiceImpl +where + T: AFServer, +{ + async fn document_search( + &self, + workspace_id: &str, + query: String, + ) -> Result, FlowyError> { + let client = self.inner.try_get_client()?; + let result = client + .search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW) + .await?; + + // Filter out irrelevant results + let result = result + .into_iter() + .filter(|r| r.score < SCORE_LIMIT) + .collect(); + + Ok(result) + } +} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index e5fb800039..6e50eb3b59 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -11,6 +11,7 @@ use client_api::ws::{ }; use client_api::{Client, ClientConfiguration}; use flowy_chat_pub::cloud::ChatCloudService; +use flowy_search_pub::cloud::SearchCloudService; use flowy_storage::ObjectStorageService; use rand::Rng; use semver::Version; @@ -38,6 +39,8 @@ use crate::af_cloud::impls::{ use crate::AppFlowyServer; +use super::impls::AFCloudSearchCloudServiceImpl; + pub(crate) type AFCloudClient = Client; pub struct AppFlowyCloudServer { @@ -255,6 +258,14 @@ impl AppFlowyServer for AppFlowyCloudServer { }; Some(Arc::new(AFCloudFileStorageServiceImpl::new(client))) } + + fn search_service(&self) -> Option> { + let server = AFServerImpl { + client: self.get_client(), + }; + + Some(Arc::new(AFCloudSearchCloudServiceImpl { inner: server })) + } } /// Spawns a new asynchronous task to handle WebSocket connections based on token state. diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index 12c2f47916..b2c17c900d 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,3 +1,4 @@ +use flowy_search_pub::cloud::SearchCloudService; use flowy_storage::ObjectStorageService; use std::sync::Arc; @@ -70,4 +71,8 @@ impl AppFlowyServer for LocalServer { fn file_storage(&self) -> Option> { None } + + fn search_service(&self) -> Option> { + None + } } diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index 70a8e4b9a8..bebfc81bcb 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -1,6 +1,7 @@ use client_api::ws::ConnectState; use client_api::ws::WSConnectStateReceiver; use client_api::ws::WebSocketChannel; +use flowy_search_pub::cloud::SearchCloudService; use flowy_storage::ObjectStorageService; use std::sync::Arc; @@ -100,6 +101,10 @@ pub trait AppFlowyServer: Send + Sync + 'static { Arc::new(DefaultChatCloudServiceImpl) } + /// Bridge for the Cloud AI Search features + /// + fn search_service(&self) -> Option>; + /// Manages collaborative objects within a remote storage system. This includes operations such as /// checking storage status, retrieving updates and snapshots, and dispatching updates. The service /// also provides subscription capabilities for real-time updates. diff --git a/frontend/rust-lib/flowy-server/src/supabase/server.rs b/frontend/rust-lib/flowy-server/src/supabase/server.rs index a9846966a8..fa59931fa2 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/server.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/server.rs @@ -1,3 +1,4 @@ +use flowy_search_pub::cloud::SearchCloudService; use flowy_storage::ObjectStorageService; use std::collections::HashMap; use std::sync::{Arc, Weak}; @@ -194,4 +195,8 @@ impl AppFlowyServer for SupabaseServer { .clone() .map(|s| s as Arc) } + + fn search_service(&self) -> Option> { + None + } }