mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-07-26 03:23:01 +00:00
chore: local and server result
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@ -27,11 +27,12 @@ void main() {
|
||||
expect(find.byType(RecentViewsList), findsOneWidget);
|
||||
|
||||
// Expect three recent history items
|
||||
expect(find.byType(RecentViewTile), findsNWidgets(3));
|
||||
expect(find.byType(SearchRecentViewCell), findsNWidgets(3));
|
||||
|
||||
// Expect the first item to be the last viewed document
|
||||
final firstDocumentWidget =
|
||||
tester.widget(find.byType(RecentViewTile).first) as RecentViewTile;
|
||||
tester.widget(find.byType(SearchRecentViewCell).first)
|
||||
as SearchRecentViewCell;
|
||||
expect(firstDocumentWidget.view.name, secondDocument);
|
||||
});
|
||||
});
|
||||
|
@ -11,10 +11,25 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'command_palette_bloc.freezed.dart';
|
||||
|
||||
class Debouncer {
|
||||
Debouncer({required this.delay});
|
||||
|
||||
final Duration delay;
|
||||
Timer? _timer;
|
||||
|
||||
void run(void Function() action) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(delay, action);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
class CommandPaletteBloc
|
||||
extends Bloc<CommandPaletteEvent, CommandPaletteState> {
|
||||
CommandPaletteBloc() : super(CommandPaletteState.initial()) {
|
||||
// Register event handlers
|
||||
on<_SearchChanged>(_onSearchChanged);
|
||||
on<_PerformSearch>(_onPerformSearch);
|
||||
on<_NewSearchStream>(_onNewSearchStream);
|
||||
@ -26,38 +41,35 @@ class CommandPaletteBloc
|
||||
_initTrash();
|
||||
}
|
||||
|
||||
Timer? _debounceOnChanged;
|
||||
final Debouncer _searchDebouncer = Debouncer(
|
||||
delay: const Duration(milliseconds: 300),
|
||||
);
|
||||
final TrashService _trashService = TrashService();
|
||||
final TrashListener _trashListener = TrashListener();
|
||||
String? _oldQuery;
|
||||
String? _activeQuery;
|
||||
String? _workspaceId;
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_trashListener.close();
|
||||
_debounceOnChanged?.cancel();
|
||||
_searchDebouncer.dispose();
|
||||
state.searchResponseStream?.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _initTrash() async {
|
||||
// Start listening for trash updates
|
||||
_trashListener.start(
|
||||
trashUpdated: (trashOrFailed) {
|
||||
add(
|
||||
CommandPaletteEvent.trashChanged(
|
||||
trash: trashOrFailed.toNullable(),
|
||||
),
|
||||
);
|
||||
},
|
||||
trashUpdated: (trashOrFailed) => add(
|
||||
CommandPaletteEvent.trashChanged(
|
||||
trash: trashOrFailed.toNullable(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Read initial trash state and forward results
|
||||
final trashOrFailure = await _trashService.readTrash();
|
||||
add(
|
||||
CommandPaletteEvent.trashChanged(
|
||||
trash: trashOrFailure.toNullable()?.items,
|
||||
),
|
||||
trashOrFailure.fold(
|
||||
(trash) => add(CommandPaletteEvent.trashChanged(trash: trash.items)),
|
||||
(error) => debugPrint('Failed to load trash: $error'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -65,9 +77,7 @@ class CommandPaletteBloc
|
||||
_SearchChanged event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) {
|
||||
_debounceOnChanged?.cancel();
|
||||
_debounceOnChanged = Timer(
|
||||
const Duration(milliseconds: 300),
|
||||
_searchDebouncer.run(
|
||||
() {
|
||||
if (!isClosed) {
|
||||
add(CommandPaletteEvent.performSearch(search: event.search));
|
||||
@ -80,31 +90,44 @@ class CommandPaletteBloc
|
||||
_PerformSearch event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) async {
|
||||
if (event.search.isNotEmpty && event.search != state.query) {
|
||||
_oldQuery = state.query;
|
||||
if (event.search.isEmpty && event.search != state.query) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
query: null,
|
||||
isLoading: false,
|
||||
serverResponseItems: [],
|
||||
localResponseItems: [],
|
||||
combinedResponseItems: {},
|
||||
resultSummaries: [],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWith(query: event.search, isLoading: true));
|
||||
_activeQuery = event.search;
|
||||
|
||||
// Fire off search asynchronously (fire and forget)
|
||||
unawaited(
|
||||
SearchBackendService.performSearch(
|
||||
event.search,
|
||||
workspaceId: _workspaceId,
|
||||
).then(
|
||||
(result) => result.onSuccess((stream) {
|
||||
if (!isClosed) {
|
||||
add(CommandPaletteEvent.newSearchStream(stream: stream));
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Clear state if search is empty or unchanged
|
||||
emit(
|
||||
state.copyWith(
|
||||
query: null,
|
||||
isLoading: false,
|
||||
resultItems: [],
|
||||
resultSummaries: [],
|
||||
(result) => result.fold(
|
||||
(stream) {
|
||||
if (!isClosed && _activeQuery == event.search) {
|
||||
add(CommandPaletteEvent.newSearchStream(stream: stream));
|
||||
}
|
||||
},
|
||||
(error) {
|
||||
debugPrint('Search error: $error');
|
||||
if (!isClosed) {
|
||||
add(
|
||||
CommandPaletteEvent.resultsChanged(
|
||||
searchId: '',
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -123,83 +146,88 @@ class CommandPaletteBloc
|
||||
);
|
||||
|
||||
event.stream.listen(
|
||||
onItems: (
|
||||
List<SearchResponseItemPB> items,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
) {
|
||||
if (_isActiveSearch(searchId)) {
|
||||
add(
|
||||
CommandPaletteEvent.resultsChanged(
|
||||
items: items,
|
||||
searchId: searchId,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onSummaries: (
|
||||
List<SearchSummaryPB> summaries,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
) {
|
||||
if (_isActiveSearch(searchId)) {
|
||||
add(
|
||||
CommandPaletteEvent.resultsChanged(
|
||||
summaries: summaries,
|
||||
searchId: searchId,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onFinished: (String searchId) {
|
||||
if (_isActiveSearch(searchId)) {
|
||||
add(
|
||||
CommandPaletteEvent.resultsChanged(
|
||||
searchId: searchId,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onLocalItems: (items, searchId) => _handleResultsUpdate(
|
||||
searchId: searchId,
|
||||
localItems: items,
|
||||
),
|
||||
onServerItems: (items, searchId, isLoading) => _handleResultsUpdate(
|
||||
searchId: searchId,
|
||||
serverItems: items,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
onSummaries: (summaries, searchId, isLoading) => _handleResultsUpdate(
|
||||
searchId: searchId,
|
||||
summaries: summaries,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
onFinished: (searchId) => _handleResultsUpdate(
|
||||
searchId: searchId,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleResultsUpdate({
|
||||
required String searchId,
|
||||
List<SearchResponseItemPB>? serverItems,
|
||||
List<LocalSearchResponseItemPB>? localItems,
|
||||
List<SearchSummaryPB>? summaries,
|
||||
bool isLoading = true,
|
||||
}) {
|
||||
if (_isActiveSearch(searchId)) {
|
||||
add(
|
||||
CommandPaletteEvent.resultsChanged(
|
||||
searchId: searchId,
|
||||
serverItems: serverItems,
|
||||
localItems: localItems,
|
||||
summaries: summaries,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onResultsChanged(
|
||||
_ResultsChanged event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) async {
|
||||
// If query was updated since last emission, clear previous results.
|
||||
if (state.query != _oldQuery) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
resultItems: [],
|
||||
resultSummaries: [],
|
||||
isLoading: event.isLoading,
|
||||
),
|
||||
);
|
||||
_oldQuery = state.query;
|
||||
}
|
||||
|
||||
// Check for outdated search streams
|
||||
if (state.searchId != event.searchId) return;
|
||||
|
||||
final updatedItems =
|
||||
event.items ?? List<SearchResponseItemPB>.from(state.resultItems);
|
||||
final updatedSummaries =
|
||||
event.summaries ?? List<SearchSummaryPB>.from(state.resultSummaries);
|
||||
final combinedItems = <String, SearchResultItem>{};
|
||||
for (final item in event.serverItems ?? state.serverResponseItems) {
|
||||
combinedItems[item.id] = SearchResultItem(
|
||||
id: item.id,
|
||||
icon: item.icon,
|
||||
displayName: item.displayName,
|
||||
content: item.content,
|
||||
workspaceId: item.workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
for (final item in event.localItems ?? state.localResponseItems) {
|
||||
combinedItems.putIfAbsent(
|
||||
item.id,
|
||||
() => SearchResultItem(
|
||||
id: item.id,
|
||||
icon: item.icon,
|
||||
displayName: item.displayName,
|
||||
content: '',
|
||||
workspaceId: item.workspaceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
resultItems: updatedItems,
|
||||
resultSummaries: updatedSummaries,
|
||||
serverResponseItems: event.serverItems ?? state.serverResponseItems,
|
||||
localResponseItems: event.localItems ?? state.localResponseItems,
|
||||
resultSummaries: event.summaries ?? state.resultSummaries,
|
||||
combinedResponseItems: combinedItems,
|
||||
isLoading: event.isLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Update trash state and, in case of null, retry reading trash from the service
|
||||
FutureOr<void> _onTrashChanged(
|
||||
_TrashChanged event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
@ -216,7 +244,6 @@ class CommandPaletteBloc
|
||||
}
|
||||
}
|
||||
|
||||
// Update the workspace and clear current search results and query
|
||||
FutureOr<void> _onWorkspaceChanged(
|
||||
_WorkspaceChanged event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
@ -225,27 +252,20 @@ class CommandPaletteBloc
|
||||
emit(
|
||||
state.copyWith(
|
||||
query: '',
|
||||
resultItems: [],
|
||||
serverResponseItems: [],
|
||||
localResponseItems: [],
|
||||
combinedResponseItems: {},
|
||||
resultSummaries: [],
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Clear search state
|
||||
FutureOr<void> _onClearSearch(
|
||||
_ClearSearch event,
|
||||
Emitter<CommandPaletteState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
query: '',
|
||||
resultItems: [],
|
||||
resultSummaries: [],
|
||||
isLoading: false,
|
||||
searchId: null,
|
||||
),
|
||||
);
|
||||
emit(CommandPaletteState.initial().copyWith(trash: state.trash));
|
||||
}
|
||||
|
||||
bool _isActiveSearch(String searchId) =>
|
||||
@ -264,7 +284,8 @@ class CommandPaletteEvent with _$CommandPaletteEvent {
|
||||
const factory CommandPaletteEvent.resultsChanged({
|
||||
required String searchId,
|
||||
required bool isLoading,
|
||||
List<SearchResponseItemPB>? items,
|
||||
List<SearchResponseItemPB>? serverItems,
|
||||
List<LocalSearchResponseItemPB>? localItems,
|
||||
List<SearchSummaryPB>? summaries,
|
||||
}) = _ResultsChanged;
|
||||
|
||||
@ -277,12 +298,30 @@ class CommandPaletteEvent with _$CommandPaletteEvent {
|
||||
const factory CommandPaletteEvent.clearSearch() = _ClearSearch;
|
||||
}
|
||||
|
||||
class SearchResultItem {
|
||||
const SearchResultItem({
|
||||
required this.id,
|
||||
required this.icon,
|
||||
required this.content,
|
||||
required this.displayName,
|
||||
this.workspaceId,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String content;
|
||||
final ResultIconPB icon;
|
||||
final String displayName;
|
||||
final String? workspaceId;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CommandPaletteState with _$CommandPaletteState {
|
||||
const CommandPaletteState._();
|
||||
const factory CommandPaletteState({
|
||||
@Default(null) String? query,
|
||||
@Default([]) List<SearchResponseItemPB> resultItems,
|
||||
@Default([]) List<SearchResponseItemPB> serverResponseItems,
|
||||
@Default([]) List<LocalSearchResponseItemPB> localResponseItems,
|
||||
@Default({}) Map<String, SearchResultItem> combinedResponseItems,
|
||||
@Default([]) List<SearchSummaryPB> resultSummaries,
|
||||
@Default(null) SearchResponseStream? searchResponseStream,
|
||||
required bool isLoading,
|
||||
@ -290,6 +329,7 @@ class CommandPaletteState with _$CommandPaletteState {
|
||||
@Default(null) String? searchId,
|
||||
}) = _CommandPaletteState;
|
||||
|
||||
factory CommandPaletteState.initial() =>
|
||||
const CommandPaletteState(isLoading: false);
|
||||
factory CommandPaletteState.initial() => const CommandPaletteState(
|
||||
isLoading: false,
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -13,6 +14,7 @@ class SearchResultListBloc
|
||||
// Register event handlers
|
||||
on<_OnHoverSummary>(_onHoverSummary);
|
||||
on<_OnHoverResult>(_onHoverResult);
|
||||
on<_OpenPage>(_onOpenPage);
|
||||
}
|
||||
|
||||
FutureOr<void> _onHoverSummary(
|
||||
@ -23,6 +25,7 @@ class SearchResultListBloc
|
||||
state.copyWith(
|
||||
hoveredSummary: event.summary,
|
||||
hoveredResult: null,
|
||||
openPageId: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -35,9 +38,17 @@ class SearchResultListBloc
|
||||
state.copyWith(
|
||||
hoveredSummary: null,
|
||||
hoveredResult: event.item,
|
||||
openPageId: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
FutureOr<void> _onOpenPage(
|
||||
_OpenPage event,
|
||||
Emitter<SearchResultListState> emit,
|
||||
) {
|
||||
emit(state.copyWith(openPageId: event.pageId));
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -46,8 +57,12 @@ class SearchResultListEvent with _$SearchResultListEvent {
|
||||
required SearchSummaryPB summary,
|
||||
}) = _OnHoverSummary;
|
||||
const factory SearchResultListEvent.onHoverResult({
|
||||
required SearchResponseItemPB item,
|
||||
required SearchResultItem item,
|
||||
}) = _OnHoverResult;
|
||||
|
||||
const factory SearchResultListEvent.openPage({
|
||||
required String pageId,
|
||||
}) = _OpenPage;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -55,7 +70,8 @@ class SearchResultListState with _$SearchResultListState {
|
||||
const SearchResultListState._();
|
||||
const factory SearchResultListState({
|
||||
@Default(null) SearchSummaryPB? hoveredSummary,
|
||||
@Default(null) SearchResponseItemPB? hoveredResult,
|
||||
@Default(null) SearchResultItem? hoveredResult,
|
||||
@Default(null) String? openPageId,
|
||||
}) = _SearchResultListState;
|
||||
|
||||
factory SearchResultListState.initial() => const SearchResultListState();
|
||||
|
@ -50,12 +50,18 @@ class SearchResponseStream {
|
||||
List<SearchResponseItemPB> items,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
)? _onItems;
|
||||
)? _onServerItems;
|
||||
void Function(
|
||||
List<SearchSummaryPB> summaries,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
)? _onSummaries;
|
||||
|
||||
void Function(
|
||||
List<LocalSearchResponseItemPB> items,
|
||||
String searchId,
|
||||
)? _onLocalItems;
|
||||
|
||||
void Function(String searchId)? _onFinished;
|
||||
int get nativePort => _port.sendPort.nativePort;
|
||||
|
||||
@ -65,21 +71,28 @@ class SearchResponseStream {
|
||||
}
|
||||
|
||||
void _onResultsChanged(Uint8List data) {
|
||||
final response = SearchResponsePB.fromBuffer(data);
|
||||
final searchState = SearchStatePB.fromBuffer(data);
|
||||
|
||||
if (response.hasResult()) {
|
||||
if (response.result.hasSearchResult()) {
|
||||
_onItems?.call(
|
||||
response.result.searchResult.items,
|
||||
if (searchState.hasResponse()) {
|
||||
if (searchState.response.hasSearchResult()) {
|
||||
_onServerItems?.call(
|
||||
searchState.response.searchResult.items,
|
||||
searchId,
|
||||
response.isLoading,
|
||||
searchState.isLoading,
|
||||
);
|
||||
}
|
||||
if (response.result.hasSearchSummary()) {
|
||||
if (searchState.response.hasSearchSummary()) {
|
||||
_onSummaries?.call(
|
||||
response.result.searchSummary.items,
|
||||
searchState.response.searchSummary.items,
|
||||
searchId,
|
||||
searchState.isLoading,
|
||||
);
|
||||
}
|
||||
|
||||
if (searchState.response.hasLocalSearchResult()) {
|
||||
_onLocalItems?.call(
|
||||
searchState.response.localSearchResult.items,
|
||||
searchId,
|
||||
response.isLoading,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@ -92,16 +105,21 @@ class SearchResponseStream {
|
||||
List<SearchResponseItemPB> items,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
)? onItems,
|
||||
)? onServerItems,
|
||||
required void Function(
|
||||
List<SearchSummaryPB> summaries,
|
||||
String searchId,
|
||||
bool isLoading,
|
||||
)? onSummaries,
|
||||
required void Function(
|
||||
List<LocalSearchResponseItemPB> items,
|
||||
String searchId,
|
||||
)? onLocalItems,
|
||||
required void Function(String searchId)? onFinished,
|
||||
}) {
|
||||
_onItems = onItems;
|
||||
_onServerItems = onServerItems;
|
||||
_onSummaries = onSummaries;
|
||||
_onLocalItems = onLocalItems;
|
||||
_onFinished = onFinished;
|
||||
}
|
||||
}
|
||||
|
@ -153,13 +153,13 @@ class CommandPaletteModal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (state.resultItems.isNotEmpty &&
|
||||
if (state.combinedResponseItems.isNotEmpty &&
|
||||
(state.query?.isNotEmpty ?? false)) ...[
|
||||
const Divider(height: 0),
|
||||
Flexible(
|
||||
child: SearchResultList(
|
||||
trash: state.trash,
|
||||
resultItems: state.resultItems,
|
||||
resultItems: state.combinedResponseItems.values.toList(),
|
||||
resultSummaries: state.resultSummaries,
|
||||
),
|
||||
),
|
||||
|
@ -4,7 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';
|
||||
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';
|
||||
@ -52,7 +52,7 @@ class RecentViewsList extends StatelessWidget {
|
||||
)
|
||||
: FlowySvg(view.iconData, size: const Size.square(20));
|
||||
|
||||
return RecentViewTile(
|
||||
return SearchRecentViewCell(
|
||||
icon: SizedBox(width: 24, child: icon),
|
||||
view: view,
|
||||
onSelected: onSelected,
|
||||
|
@ -7,8 +7,8 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RecentViewTile extends StatelessWidget {
|
||||
const RecentViewTile({
|
||||
class SearchRecentViewCell extends StatelessWidget {
|
||||
const SearchRecentViewCell({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.view,
|
@ -1,11 +1,8 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart';
|
||||
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';
|
||||
@ -19,12 +16,10 @@ class SearchResultCell extends StatefulWidget {
|
||||
const SearchResultCell({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.onSelected,
|
||||
this.isTrashed = false,
|
||||
});
|
||||
|
||||
final SearchResponseItemPB item;
|
||||
final VoidCallback onSelected;
|
||||
final SearchResultItem item;
|
||||
final bool isTrashed;
|
||||
|
||||
@override
|
||||
@ -43,12 +38,9 @@ class _SearchResultCellState extends State<SearchResultCell> {
|
||||
|
||||
/// Helper to handle the selection action.
|
||||
void _handleSelection() {
|
||||
widget.onSelected();
|
||||
getIt<ActionNavigationBloc>().add(
|
||||
ActionNavigationEvent.performAction(
|
||||
action: NavigationAction(objectId: widget.item.id),
|
||||
),
|
||||
);
|
||||
context.read<SearchResultListBloc>().add(
|
||||
SearchResultListEvent.openPage(pageId: widget.item.id),
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper to clean up preview text.
|
||||
@ -62,7 +54,7 @@ class _SearchResultCellState extends State<SearchResultCell> {
|
||||
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
);
|
||||
final icon = widget.item.icon.getIcon();
|
||||
final cleanedPreview = _cleanPreview(widget.item.preview);
|
||||
final cleanedPreview = _cleanPreview(widget.item.content);
|
||||
final hasPreview = cleanedPreview.isNotEmpty;
|
||||
final trashHintText =
|
||||
widget.isTrashed ? LocaleKeys.commandPalette_fromTrashHint.tr() : null;
|
||||
@ -208,7 +200,7 @@ class SearchResultPreview extends StatelessWidget {
|
||||
required this.data,
|
||||
});
|
||||
|
||||
final SearchResponseItemPB data;
|
||||
final SearchResultItem data;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -1,3 +1,7 @@
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -20,9 +24,8 @@ class SearchResultList extends StatelessWidget {
|
||||
});
|
||||
|
||||
final List<TrashPB> trash;
|
||||
final List<SearchResponseItemPB> resultItems;
|
||||
final List<SearchResultItem> resultItems;
|
||||
final List<SearchSummaryPB> resultSummaries;
|
||||
|
||||
Widget _buildSectionHeader(String title) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8) +
|
||||
const EdgeInsets.only(left: 8),
|
||||
@ -65,7 +68,6 @@ class SearchResultList extends StatelessWidget {
|
||||
final item = resultItems[index];
|
||||
return SearchResultCell(
|
||||
item: item,
|
||||
onSelected: () => FlowyOverlay.pop(context),
|
||||
isTrashed: trash.any((t) => t.id == item.id),
|
||||
);
|
||||
},
|
||||
@ -80,33 +82,46 @@ class SearchResultList extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: BlocProvider(
|
||||
create: (context) => SearchResultListBloc(),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 7,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
if (resultSummaries.isNotEmpty) _buildSummariesSection(),
|
||||
const VSpace(10),
|
||||
if (resultItems.isNotEmpty) _buildResultsSection(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
const HSpace(10),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 16,
|
||||
child: BlocListener<SearchResultListBloc, SearchResultListState>(
|
||||
listener: (context, state) {
|
||||
if (state.openPageId != null) {
|
||||
FlowyOverlay.pop(context);
|
||||
getIt<ActionNavigationBloc>().add(
|
||||
ActionNavigationEvent.performAction(
|
||||
action: NavigationAction(objectId: state.openPageId!),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 7,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
children: [
|
||||
if (resultSummaries.isNotEmpty) _buildSummariesSection(),
|
||||
const VSpace(10),
|
||||
if (resultItems.isNotEmpty) _buildResultsSection(context),
|
||||
],
|
||||
),
|
||||
child: const SearchCellPreview(),
|
||||
),
|
||||
),
|
||||
],
|
||||
const HSpace(10),
|
||||
if (resultItems.any((item) => item.content.isNotEmpty))
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 16,
|
||||
),
|
||||
child: const SearchCellPreview(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -36,7 +36,7 @@ class SearchSummaryCell extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: FlowyText(
|
||||
summary.content,
|
||||
maxLines: 3,
|
||||
maxLines: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -78,14 +78,19 @@ class SearchSummarySource extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = source.icon.getIcon();
|
||||
return Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
SizedBox(width: 24, child: icon),
|
||||
const HSpace(6),
|
||||
],
|
||||
FlowyText(source.displayName),
|
||||
],
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
child: FlowyButton(
|
||||
leftIcon: icon,
|
||||
hoverColor:
|
||||
Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
|
||||
text: FlowyText(source.displayName),
|
||||
onTap: () {
|
||||
context.read<SearchResultListBloc>().add(
|
||||
SearchResultListEvent.openPage(pageId: source.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2694,8 +2694,8 @@
|
||||
"placeholder": "Search or ask a question...",
|
||||
"bestMatches": "Best matches",
|
||||
"aiOverview": "AI overview",
|
||||
"aiOverviewSource": "Sources",
|
||||
"pagePreview": "Preview",
|
||||
"aiOverviewSource": "Reference sources",
|
||||
"pagePreview": "Content preview",
|
||||
"recentHistory": "Recent history",
|
||||
"navigateHint": "to navigate",
|
||||
"loadingTooltip": "We are looking for results...",
|
||||
|
@ -71,10 +71,10 @@ impl LocalAIResourceController {
|
||||
) -> Self {
|
||||
let (resource_notify, _) = tokio::sync::broadcast::channel(1);
|
||||
let (app_state_sender, _) = tokio::sync::broadcast::channel(1);
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
let mut offline_app_disk_watch: Option<WatchContext> = None;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
{
|
||||
match watch_offline_app() {
|
||||
Ok((new_watcher, mut rx)) => {
|
||||
|
@ -1,9 +1,9 @@
|
||||
use crate::entities::{
|
||||
CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, SearchResultPB,
|
||||
SearchSourcePB, SearchSummaryPB,
|
||||
CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB,
|
||||
SearchResponsePB, SearchSourcePB, SearchSummaryPB,
|
||||
};
|
||||
use crate::{
|
||||
entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB},
|
||||
entities::{ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB},
|
||||
services::manager::{SearchHandler, SearchType},
|
||||
};
|
||||
use async_stream::stream;
|
||||
@ -45,7 +45,7 @@ impl SearchHandler for DocumentSearchHandler {
|
||||
&self,
|
||||
query: String,
|
||||
filter: Option<SearchFilterPB>,
|
||||
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>> {
|
||||
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResponsePB>> + Send + 'static>> {
|
||||
let cloud_service = self.cloud_service.clone();
|
||||
let folder_manager = self.folder_manager.clone();
|
||||
|
||||
@ -99,13 +99,10 @@ impl SearchHandler for DocumentSearchHandler {
|
||||
for item in &result_items {
|
||||
if let Some(view) = views.iter().find(|v| v.id == item.object_id.to_string()) {
|
||||
items.push(SearchResponseItemPB {
|
||||
index_type: IndexTypePB::Document,
|
||||
id: item.object_id.to_string(),
|
||||
display_name: view.name.clone(),
|
||||
icon: extract_icon(view),
|
||||
score: item.score,
|
||||
workspace_id: item.workspace_id.to_string(),
|
||||
preview: item.preview.clone(),
|
||||
content: item.content.clone()}
|
||||
);
|
||||
} else {
|
||||
@ -133,15 +130,11 @@ impl SearchHandler for DocumentSearchHandler {
|
||||
let sources: Vec<SearchSourcePB> = v.sources
|
||||
.iter()
|
||||
.flat_map(|id| {
|
||||
if let Some(view) = views.iter().find(|v| v.id == id.to_string()) {
|
||||
Some(SearchSourcePB {
|
||||
views.iter().find(|v| v.id == id.to_string()).map(|view| SearchSourcePB {
|
||||
id: id.to_string(),
|
||||
display_name: view.name.clone(),
|
||||
icon: extract_icon(view),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -1,31 +0,0 @@
|
||||
use flowy_derive::ProtoBuf_Enum;
|
||||
|
||||
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
|
||||
pub enum IndexTypePB {
|
||||
View = 0,
|
||||
Document = 1,
|
||||
DocumentBlock = 2,
|
||||
DatabaseRow = 3,
|
||||
}
|
||||
|
||||
impl Default for IndexTypePB {
|
||||
fn default() -> Self {
|
||||
Self::View
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<IndexTypePB> for i32 {
|
||||
fn from(notification: IndexTypePB) -> Self {
|
||||
notification as i32
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<i32> for IndexTypePB {
|
||||
fn from(notification: i32) -> Self {
|
||||
match notification {
|
||||
1 => IndexTypePB::View,
|
||||
2 => IndexTypePB::DocumentBlock,
|
||||
_ => IndexTypePB::DatabaseRow,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
mod index_type;
|
||||
mod notification;
|
||||
mod query;
|
||||
mod result;
|
||||
mod search_filter;
|
||||
|
||||
pub use index_type::*;
|
||||
pub use notification::*;
|
||||
pub use query::*;
|
||||
pub use result::*;
|
||||
|
@ -1,10 +1,10 @@
|
||||
use super::SearchResultPB;
|
||||
use super::SearchResponsePB;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct SearchResponsePB {
|
||||
pub struct SearchStatePB {
|
||||
#[pb(index = 1, one_of)]
|
||||
pub result: Option<SearchResultPB>,
|
||||
pub response: Option<SearchResponsePB>,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub search_id: String,
|
||||
|
@ -1,4 +1,3 @@
|
||||
use super::IndexTypePB;
|
||||
use collab_folder::{IconType, ViewIcon};
|
||||
use derive_builder::Builder;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
@ -7,7 +6,7 @@ use flowy_folder::entities::ViewIconPB;
|
||||
#[derive(Debug, Default, ProtoBuf, Builder, Clone)]
|
||||
#[builder(name = "CreateSearchResultPBArgs")]
|
||||
#[builder(pattern = "mutable")]
|
||||
pub struct SearchResultPB {
|
||||
pub struct SearchResponsePB {
|
||||
#[pb(index = 1, one_of)]
|
||||
#[builder(default)]
|
||||
pub search_result: Option<RepeatedSearchResponseItemPB>,
|
||||
@ -15,6 +14,10 @@ pub struct SearchResultPB {
|
||||
#[pb(index = 2, one_of)]
|
||||
#[builder(default)]
|
||||
pub search_summary: Option<RepeatedSearchSummaryPB>,
|
||||
|
||||
#[pb(index = 3, one_of)]
|
||||
#[builder(default)]
|
||||
pub local_search_result: Option<RepeatedLocalSearchResponseItemPB>,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
@ -53,43 +56,40 @@ pub struct RepeatedSearchResponseItemPB {
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct SearchResponseItemPB {
|
||||
#[pb(index = 1)]
|
||||
pub index_type: IndexTypePB,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub id: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
#[pb(index = 2)]
|
||||
pub display_name: String,
|
||||
|
||||
#[pb(index = 4, one_of)]
|
||||
#[pb(index = 3, one_of)]
|
||||
pub icon: Option<ResultIconPB>,
|
||||
|
||||
#[pb(index = 5)]
|
||||
pub score: f64,
|
||||
|
||||
#[pb(index = 6)]
|
||||
#[pb(index = 4)]
|
||||
pub workspace_id: String,
|
||||
|
||||
#[pb(index = 7, one_of)]
|
||||
pub preview: Option<String>,
|
||||
|
||||
#[pb(index = 8)]
|
||||
#[pb(index = 5)]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl SearchResponseItemPB {
|
||||
pub fn with_score(&self, score: f64) -> Self {
|
||||
SearchResponseItemPB {
|
||||
index_type: self.index_type.clone(),
|
||||
id: self.id.clone(),
|
||||
display_name: self.display_name.clone(),
|
||||
icon: self.icon.clone(),
|
||||
score,
|
||||
workspace_id: self.workspace_id.clone(),
|
||||
preview: self.preview.clone(),
|
||||
content: self.content.clone(),
|
||||
}
|
||||
}
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct RepeatedLocalSearchResponseItemPB {
|
||||
#[pb(index = 1)]
|
||||
pub items: Vec<LocalSearchResponseItemPB>,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf, Default, Debug, Clone)]
|
||||
pub struct LocalSearchResponseItemPB {
|
||||
#[pb(index = 1)]
|
||||
pub id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub display_name: String,
|
||||
|
||||
#[pb(index = 3, one_of)]
|
||||
pub icon: Option<ResultIconPB>,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub workspace_id: String,
|
||||
}
|
||||
|
||||
#[derive(ProtoBuf_Enum, Clone, Debug, PartialEq, Eq, Default)]
|
||||
|
@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::entities::{IndexTypePB, ResultIconPB, SearchResponseItemPB};
|
||||
use crate::entities::{LocalSearchResponseItemPB, ResultIconPB};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FolderIndexData {
|
||||
@ -11,7 +11,7 @@ pub struct FolderIndexData {
|
||||
pub workspace_id: String,
|
||||
}
|
||||
|
||||
impl From<FolderIndexData> for SearchResponseItemPB {
|
||||
impl From<FolderIndexData> for LocalSearchResponseItemPB {
|
||||
fn from(data: FolderIndexData) -> Self {
|
||||
let icon = if data.icon.is_empty() {
|
||||
None
|
||||
@ -23,14 +23,10 @@ impl From<FolderIndexData> for SearchResponseItemPB {
|
||||
};
|
||||
|
||||
Self {
|
||||
index_type: IndexTypePB::View,
|
||||
id: data.id,
|
||||
display_name: data.title,
|
||||
score: 0.0,
|
||||
icon,
|
||||
workspace_id: data.workspace_id,
|
||||
preview: None,
|
||||
content: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::indexer::FolderIndexManagerImpl;
|
||||
use crate::entities::{
|
||||
CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, SearchFilterPB, SearchResultPB,
|
||||
CreateSearchResultPBArgs, RepeatedLocalSearchResponseItemPB, SearchFilterPB, SearchResponsePB,
|
||||
};
|
||||
use crate::services::manager::{SearchHandler, SearchType};
|
||||
use async_stream::stream;
|
||||
@ -30,7 +30,7 @@ impl SearchHandler for FolderSearchHandler {
|
||||
&self,
|
||||
query: String,
|
||||
filter: Option<SearchFilterPB>,
|
||||
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>> {
|
||||
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResponsePB>> + Send + 'static>> {
|
||||
let index_manager = self.index_manager.clone();
|
||||
|
||||
Box::pin(stream! {
|
||||
@ -48,8 +48,8 @@ impl SearchHandler for FolderSearchHandler {
|
||||
}
|
||||
|
||||
// Build the search result.
|
||||
let search_result = RepeatedSearchResponseItemPB {items};
|
||||
yield Ok(CreateSearchResultPBArgs::default().search_result(Some(search_result)).build().unwrap())
|
||||
let search_result = RepeatedLocalSearchResponseItemPB {items};
|
||||
yield Ok(CreateSearchResultPBArgs::default().local_search_result(Some(search_result)).build().unwrap())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
use crate::{
|
||||
entities::SearchResponseItemPB,
|
||||
folder::schema::{
|
||||
FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME,
|
||||
FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME,
|
||||
},
|
||||
use crate::folder::schema::{
|
||||
FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME,
|
||||
FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME,
|
||||
};
|
||||
use collab::core::collab::{IndexContent, IndexContentReceiver};
|
||||
use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewIndexContent, ViewLayout};
|
||||
@ -14,9 +11,8 @@ use std::sync::{Arc, Weak};
|
||||
use std::{collections::HashMap, fs};
|
||||
|
||||
use super::entities::FolderIndexData;
|
||||
use crate::entities::ResultIconTypePB;
|
||||
use crate::entities::{LocalSearchResponseItemPB, ResultIconTypePB};
|
||||
use lib_infra::async_trait::async_trait;
|
||||
use strsim::levenshtein;
|
||||
use tantivy::{
|
||||
collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document,
|
||||
Index, IndexReader, IndexWriter, TantivyDocument, Term,
|
||||
@ -113,11 +109,6 @@ impl FolderIndexManagerImpl {
|
||||
(icon, icon_ty)
|
||||
}
|
||||
|
||||
fn score_result(&self, query: &str, term: &str) -> f64 {
|
||||
let distance = levenshtein(query, term) as f64;
|
||||
1.0 / (distance + 1.0)
|
||||
}
|
||||
|
||||
/// Simple implementation to index all given data by spawning async tasks.
|
||||
fn index_all(&self, data_vec: Vec<IndexableData>) -> Result<(), FlowyError> {
|
||||
for data in data_vec {
|
||||
@ -130,7 +121,7 @@ impl FolderIndexManagerImpl {
|
||||
}
|
||||
|
||||
/// Searches the index using the given query string.
|
||||
pub async fn search(&self, query: String) -> Result<Vec<SearchResponseItemPB>, FlowyError> {
|
||||
pub async fn search(&self, query: String) -> Result<Vec<LocalSearchResponseItemPB>, FlowyError> {
|
||||
let lock = self.state.read().await;
|
||||
let state = lock
|
||||
.as_ref()
|
||||
@ -157,8 +148,8 @@ impl FolderIndexManagerImpl {
|
||||
}
|
||||
if !content.is_empty() {
|
||||
let s = serde_json::to_string(&content)?;
|
||||
let result: SearchResponseItemPB = serde_json::from_str::<FolderIndexData>(&s)?.into();
|
||||
results.push(result.with_score(self.score_result(&query, &result.display_name)));
|
||||
let result: LocalSearchResponseItemPB = serde_json::from_str::<FolderIndexData>(&s)?.into();
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,11 +191,11 @@ impl IndexManager for FolderIndexManagerImpl {
|
||||
})
|
||||
.await;
|
||||
},
|
||||
Err(err) => tracing::error!("FolderIndexManager error deserialize (update): {:?}", err),
|
||||
Err(err) => error!("FolderIndexManager error deserialize (update): {:?}", err),
|
||||
},
|
||||
IndexContent::Delete(ids) => {
|
||||
if let Err(e) = indexer.remove_indices(ids).await {
|
||||
tracing::error!("FolderIndexManager error (delete): {:?}", e);
|
||||
error!("FolderIndexManager error (delete): {:?}", e);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::entities::{SearchFilterPB, SearchResponsePB, SearchResultPB};
|
||||
use crate::entities::{SearchFilterPB, SearchResponsePB, SearchStatePB};
|
||||
use allo_isolate::Isolate;
|
||||
use flowy_error::FlowyResult;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
@ -25,7 +25,7 @@ pub trait SearchHandler: Send + Sync + 'static {
|
||||
&self,
|
||||
query: String,
|
||||
filter: Option<SearchFilterPB>,
|
||||
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>>;
|
||||
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResponsePB>> + Send + 'static>>;
|
||||
}
|
||||
|
||||
/// The [SearchManager] is used to inject multiple [SearchHandler]'s
|
||||
@ -34,7 +34,7 @@ pub trait SearchHandler: Send + Sync + 'static {
|
||||
///
|
||||
pub struct SearchManager {
|
||||
pub handlers: HashMap<SearchType, Arc<dyn SearchHandler>>,
|
||||
current_search: Arc<tokio::sync::Mutex<Option<String>>>, // Track current search
|
||||
current_search: Arc<tokio::sync::Mutex<Option<String>>>,
|
||||
}
|
||||
|
||||
impl SearchManager {
|
||||
@ -84,23 +84,21 @@ impl SearchManager {
|
||||
}
|
||||
|
||||
let mut stream = handler.perform_search(query.clone(), filter).await;
|
||||
while let Some(result) = stream.next().await {
|
||||
while let Some(Ok(search_result)) = stream.next().await {
|
||||
if !is_current_search(¤t_search, &search_id).await {
|
||||
trace!("[Search] discard search stream: {}", query);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Ok(result) = result {
|
||||
let resp = SearchResponsePB {
|
||||
result: Some(result),
|
||||
search_id: search_id.clone(),
|
||||
is_loading: true,
|
||||
};
|
||||
if let Ok::<Vec<u8>, _>(data) = resp.try_into() {
|
||||
if let Err(err) = clone_sink.send(data).await {
|
||||
error!("Failed to send search result: {}", err);
|
||||
break;
|
||||
}
|
||||
let resp = SearchStatePB {
|
||||
response: Some(search_result),
|
||||
search_id: search_id.clone(),
|
||||
is_loading: true,
|
||||
};
|
||||
if let Ok::<Vec<u8>, _>(data) = resp.try_into() {
|
||||
if let Err(err) = clone_sink.send(data).await {
|
||||
error!("Failed to send search result: {}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -110,8 +108,8 @@ impl SearchManager {
|
||||
return;
|
||||
}
|
||||
|
||||
let resp = SearchResponsePB {
|
||||
result: None,
|
||||
let resp = SearchStatePB {
|
||||
response: None,
|
||||
search_id: search_id.clone(),
|
||||
is_loading: true,
|
||||
};
|
||||
|
Reference in New Issue
Block a user