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