diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index 4b30b0043a..dea856331c 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -25,21 +25,21 @@ jobs: test-on-ubuntu: runs-on: ubuntu-latest steps: -# - name: Maximize build space -# uses: easimon/maximize-build-space@master -# with: -# root-reserve-mb: 2048 -# swap-size-mb: 1024 -# remove-dotnet: 'true' -# -# # the following step is required to avoid running out of space -# - name: Maximize build space -# run: | -# sudo rm -rf /usr/share/dotnet -# sudo rm -rf /opt/ghc -# sudo rm -rf "/usr/local/share/boost" -# sudo rm -rf "$AGENT_TOOLSDIRECTORY" -# sudo docker image prune --all --force + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + root-reserve-mb: 2048 + swap-size-mb: 1024 + remove-dotnet: 'true' + + # the following step is required to avoid running out of space + - name: Maximize build space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo docker image prune --all --force - name: Checkout source code uses: actions/checkout@v4 diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test.dart new file mode 100644 index 0000000000..4274980b3a --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test.dart @@ -0,0 +1,22 @@ +import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Command Palette', () { + testWidgets('Toggle command palette', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.toggleCommandPalette(); + expect(find.byType(CommandPaletteModal), findsOneWidget); + + await tester.toggleCommandPalette(); + expect(find.byType(CommandPaletteModal), findsNothing); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test_runner.dart new file mode 100644 index 0000000000..b1e990361a --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/command_palette/command_palette_test_runner.dart @@ -0,0 +1,14 @@ +import 'package:integration_test/integration_test.dart'; + +import 'command_palette_test.dart' as command_palette_test; +import 'folder_search_test.dart' as folder_search_test; +import 'recent_history_test.dart' as recent_history_test; + +void startTesting() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Command Palette integration tests + command_palette_test.main(); + folder_search_test.main(); + recent_history_test.main(); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart new file mode 100644 index 0000000000..d26f80387a --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart @@ -0,0 +1,54 @@ +import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/util.dart'; + + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Folder Search', () { + testWidgets('Search for views', (tester) async { + const firstDocument = "ViewOne"; + const secondDocument = "ViewOna"; + + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.createNewPageWithNameUnderParent(name: firstDocument); + await tester.createNewPageWithNameUnderParent(name: secondDocument); + + await tester.toggleCommandPalette(); + expect(find.byType(CommandPaletteModal), findsOneWidget); + + final searchFieldFinder = find.descendant( + of: find.byType(SearchField), + matching: find.byType(FlowyTextField), + ); + + await tester.enterText(searchFieldFinder, secondDocument); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + // Expect two search results "ViewOna" and "ViewOne" (Distance 1 to ViewOna) + expect(find.byType(SearchResultTile), findsNWidgets(2)); + + // The score should be higher for "ViewOna" thus it should be shown first + final secondDocumentWidget = tester + .widget(find.byType(SearchResultTile).first) as SearchResultTile; + expect(secondDocumentWidget.result.data, secondDocument); + + // Change search to "ViewOne" + await tester.enterText(searchFieldFinder, firstDocument); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // The score should be higher for "ViewOne" thus it should be shown first + final firstDocumentWidget = tester + .widget(find.byType(SearchResultTile).first) as SearchResultTile; + expect(firstDocumentWidget.result.data, firstDocument); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart new file mode 100644 index 0000000000..892ed5dad0 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart @@ -0,0 +1,38 @@ +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/recent_views_list.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Recent History', () { + testWidgets('Search for views', (tester) async { + const firstDocument = "First"; + const secondDocument = "Second"; + + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.createNewPageWithNameUnderParent(name: firstDocument); + await tester.createNewPageWithNameUnderParent(name: secondDocument); + + await tester.toggleCommandPalette(); + expect(find.byType(CommandPaletteModal), findsOneWidget); + + // Expect history list + expect(find.byType(RecentViewsList), findsOneWidget); + + // Expect three recent history items + expect(find.byType(RecentViewTile), findsNWidgets(3)); + + // Expect the first item to be the last viewed document + final firstDocumentWidget = + tester.widget(find.byType(RecentViewTile).first) as RecentViewTile; + expect(firstDocumentWidget.view.name, secondDocument); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index abfcb324f6..90d1581aba 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -1,5 +1,9 @@ import 'dart:io'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -26,9 +30,6 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'emoji.dart'; @@ -520,6 +521,16 @@ extension CommonOperations on WidgetTester { } } + Future toggleCommandPalette() async { + // Press CMD+P or CTRL+P to open the command palette + await simulateKeyEvent( + LogicalKeyboardKey.keyP, + isControlPressed: !Platform.isMacOS, + isMetaPressed: Platform.isMacOS, + ); + await pumpAndSettle(); + } + Future openCollaborativeWorkspaceMenu() async { if (!FeatureFlag.collaborativeWorkspace.isOn) { throw UnsupportedError('Collaborative workspace is not enabled'); diff --git a/frontend/appflowy_flutter/lib/core/notification/search_notification.dart b/frontend/appflowy_flutter/lib/core/notification/search_notification.dart new file mode 100644 index 0000000000..71d8167c42 --- /dev/null +++ b/frontend/appflowy_flutter/lib/core/notification/search_notification.dart @@ -0,0 +1,50 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/entities.pbenum.dart'; +import 'package:appflowy_backend/rust_stream.dart'; +import 'package:appflowy_result/appflowy_result.dart'; + +import 'notification_helper.dart'; + +typedef SearchNotificationCallback = void Function( + SearchNotification, + FlowyResult, +); + +class SearchNotificationParser + extends NotificationParser { + SearchNotificationParser({ + super.id, + required super.callback, + }) : super( + tyParser: (ty) => SearchNotification.valueOf(ty), + errorParser: (bytes) => FlowyError.fromBuffer(bytes), + ); +} + +typedef SearchNotificationHandler = Function( + SearchNotification ty, + FlowyResult result, +); + +class SearchNotificationListener { + SearchNotificationListener({ + required String objectId, + required SearchNotificationHandler handler, + }) : _parser = SearchNotificationParser(id: objectId, callback: handler) { + _subscription = + RustStreamReceiver.listen((observable) => _parser?.parse(observable)); + } + + StreamSubscription? _subscription; + SearchNotificationParser? _parser; + + Future stop() async { + _parser = null; + await _subscription?.cancel(); + _subscription = null; + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart index 8813afc3b2..8e77f362f6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart @@ -1,13 +1,12 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'widgets/widgets.dart'; class NotificationsSettingGroup extends StatefulWidget { - const NotificationsSettingGroup({ - super.key, - }); + const NotificationsSettingGroup({super.key}); @override State createState() => @@ -15,7 +14,6 @@ class NotificationsSettingGroup extends StatefulWidget { } class _NotificationsSettingGroupState extends State { - // TODO:remove this after notification page is implemented bool isPushNotificationOn = false; @override diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart index 9cccd3ab51..a77b4b2f27 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -1,11 +1,26 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/icon.pbenum.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; + +extension ToProto on FlowyIconType { + ViewIconTypePB toProto() { + switch (this) { + case FlowyIconType.emoji: + return ViewIconTypePB.Emoji; + case FlowyIconType.icon: + return ViewIconTypePB.Icon; + case FlowyIconType.custom: + return ViewIconTypePB.Url; + } + } +} enum FlowyIconType { emoji, @@ -14,6 +29,12 @@ enum FlowyIconType { } class EmojiPickerResult { + factory EmojiPickerResult.none() => + const EmojiPickerResult(FlowyIconType.icon, ''); + + factory EmojiPickerResult.emoji(String emoji) => + EmojiPickerResult(FlowyIconType.emoji, emoji); + const EmojiPickerResult( this.type, this.emoji, @@ -23,7 +44,7 @@ class EmojiPickerResult { final String emoji; } -class FlowyIconPicker extends StatefulWidget { +class FlowyIconPicker extends StatelessWidget { const FlowyIconPicker({ super.key, required this.onSelected, @@ -31,17 +52,6 @@ class FlowyIconPicker extends StatefulWidget { final void Function(EmojiPickerResult result) onSelected; - @override - State createState() => _FlowyIconPickerState(); -} - -class _FlowyIconPickerState extends State - with SingleTickerProviderStateMixin { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { // ONLY supports emoji picker for now @@ -55,33 +65,18 @@ class _FlowyIconPickerState extends State _buildTabs(context), const Spacer(), _RemoveIconButton( - onTap: () { - widget.onSelected( - const EmojiPickerResult( - FlowyIconType.icon, - '', - ), - ); - }, + onTap: () => onSelected(EmojiPickerResult.none()), ), ], ), - const Divider( - height: 2, - ), + const Divider(height: 2), Expanded( child: TabBarView( children: [ FlowyEmojiPicker( - emojiPerLine: _getEmojiPerLine(), - onEmojiSelected: (_, emoji) { - widget.onSelected( - EmojiPickerResult( - FlowyIconType.emoji, - emoji, - ), - ); - }, + emojiPerLine: _getEmojiPerLine(context), + onEmojiSelected: (_, emoji) => + onSelected(EmojiPickerResult.emoji(emoji)), ), ], ), @@ -109,9 +104,7 @@ class _FlowyIconPickerState extends State horizontal: 12.0, vertical: 8.0, ), - child: FlowyText( - LocaleKeys.emoji_emojiTab.tr(), - ), + child: FlowyText(LocaleKeys.emoji_emojiTab.tr()), ), ), ], @@ -119,7 +112,7 @@ class _FlowyIconPickerState extends State ); } - int _getEmojiPerLine() { + int _getEmojiPerLine(BuildContext context) { if (PlatformExtension.isDesktopOrWeb) { return 9; } @@ -129,11 +122,10 @@ class _FlowyIconPickerState extends State } class _RemoveIconButton extends StatelessWidget { - const _RemoveIconButton({ - required this.onTap, - }); + const _RemoveIconButton({required this.onTap}); final VoidCallback onTap; + @override Widget build(BuildContext context) { return SizedBox( diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart index a41037a47e..b63442f4e8 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; class IconPickerPage extends StatelessWidget { const IconPickerPage({ @@ -21,9 +22,7 @@ class IconPickerPage extends StatelessWidget { titleText: title ?? LocaleKeys.titleBar_pageIcon.tr(), ), body: SafeArea( - child: FlowyIconPicker( - onSelected: onSelected, - ), + child: FlowyIconPicker(onSelected: onSelected), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 1c23939771..44ff2147f9 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -6,8 +6,8 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action_bloc.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_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:collection/collection.dart'; @@ -123,7 +123,7 @@ class _GridPageState extends State { view: widget.view, databaseController: widget.databaseController, )..add(const GridEvent.initial()), - child: BlocListener( + child: BlocListener( listener: (context, state) { final action = state.action; if (action?.type == ActionType.openRow && diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart index 1172f81ac9..234192cc1d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart @@ -10,7 +10,7 @@ import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/shortcuts.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; +import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; @@ -87,8 +87,8 @@ class _MobileGridPageState extends State { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider.value( - value: getIt(), + BlocProvider.value( + value: getIt(), ), BlocProvider( create: (context) => GridBloc( diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 98ca75ac45..04bcf6150b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; @@ -6,15 +8,14 @@ import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action_bloc.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/view/prelude.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DocumentPage extends StatefulWidget { @@ -57,7 +58,7 @@ class _DocumentPageState extends State { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider.value(value: getIt()), + BlocProvider.value(value: getIt()), BlocProvider( create: (_) => DocumentBloc(view: widget.view) ..add(const DocumentEvent.initial()), @@ -85,9 +86,9 @@ class _DocumentPageState extends State { return const SizedBox.shrink(); } - return BlocListener( - listener: _onNotificationAction, + return BlocListener( listenWhen: (_, curr) => curr.action != null, + listener: _onNotificationAction, child: _buildEditorPage(context, state), ); }, @@ -161,7 +162,7 @@ class _DocumentPageState extends State { void _onNotificationAction( BuildContext context, - NotificationActionState state, + ActionNavigationState state, ) { if (state.action != null && state.action!.type == ActionType.jumpToBlock) { final path = state.action?.arguments?[ActionArgumentKeys.nodePath]; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index 9ca7f9ec8c..eafba22524 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -19,7 +21,6 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:string_validator/string_validator.dart'; diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index c2759ab2c8..39659f080b 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -18,9 +18,9 @@ import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/user/presentation/router.dart'; +import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/application/settings/appearance/desktop_appearance.dart'; import 'package:appflowy/workspace/application/settings/appearance/mobile_appearance.dart'; @@ -193,7 +193,7 @@ void _resolveHomeDeps(GetIt getIt) { (view, _) => DocShareBloc(view: view), ); - getIt.registerSingleton(NotificationActionBloc()); + getIt.registerSingleton(ActionNavigationBloc()); getIt.registerLazySingleton(() => TabsBloc()); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 4b59782976..7f2a9086d8 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -1,25 +1,27 @@ import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; -import 'package:appflowy/workspace/application/notifications/notification_service.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/notification/notification_service.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -150,12 +152,15 @@ class _ApplicationWidgetState extends State { create: (_) => DocumentAppearanceCubit()..fetch(), ), BlocProvider.value(value: getIt()), - BlocProvider.value(value: getIt()), + BlocProvider.value( + value: getIt() + ..add(const ActionNavigationEvent.initialize()), + ), BlocProvider.value( value: getIt()..add(const ReminderEvent.started()), ), ], - child: BlocListener( + child: BlocListener( listenWhen: (_, curr) => curr.action != null, listener: (context, state) { final action = state.action; @@ -189,7 +194,13 @@ class _ApplicationWidgetState extends State { data: MediaQuery.of(context).copyWith( textScaler: TextScaler.linear(state.textScaleFactor), ), - child: overlayManagerBuilder(context, child), + child: overlayManagerBuilder( + context, + CommandPalette( + toggleNotifier: ValueNotifier(false), + child: child, + ), + ), ), debugShowCheckedModeBanner: false, theme: state.lightTheme, diff --git a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart index a7fb47b405..d50c6fc795 100644 --- a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart @@ -6,9 +6,9 @@ import 'package:appflowy/user/application/reminder/reminder_extension.dart'; import 'package:appflowy/user/application/reminder/reminder_service.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/util/int64_extension.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; -import 'package:appflowy/workspace/application/notifications/notification_service.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/notification/notification_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; @@ -22,14 +22,14 @@ part 'reminder_bloc.freezed.dart'; class ReminderBloc extends Bloc { ReminderBloc() : super(ReminderState()) { - _actionBloc = getIt(); + _actionBloc = getIt(); _reminderService = const ReminderService(); timer = _periodicCheck(); _dispatch(); } - late final NotificationActionBloc _actionBloc; + late final ActionNavigationBloc _actionBloc; late final ReminderService _reminderService; late final Timer timer; @@ -147,7 +147,7 @@ class ReminderBloc extends Bloc { rowId = reminder.meta[ReminderMetaKeys.rowId]; } - final action = NotificationAction( + final action = NavigationAction( objectId: reminder.objectId, arguments: { ActionArgumentKeys.view: view, @@ -158,7 +158,7 @@ class ReminderBloc extends Bloc { if (!isClosed) { _actionBloc.add( - NotificationActionEvent.performAction( + ActionNavigationEvent.performAction( action: action, nextActions: [ action.copyWith( @@ -198,8 +198,8 @@ class ReminderBloc extends Bloc { title: LocaleKeys.reminderNotification_title.tr(), body: LocaleKeys.reminderNotification_message.tr(), onClick: () => _actionBloc.add( - NotificationActionEvent.performAction( - action: NotificationAction(objectId: reminder.objectId), + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: reminder.objectId), ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/action_navigation/action_navigation_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/action_navigation/action_navigation_bloc.dart new file mode 100644 index 0000000000..04c4bfaa9f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/action_navigation/action_navigation_bloc.dart @@ -0,0 +1,134 @@ +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy/workspace/application/workspace/workspace_listener.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'action_navigation_bloc.freezed.dart'; + +class ActionNavigationBloc + extends Bloc { + ActionNavigationBloc() : super(const ActionNavigationState.initial()) { + on((event, emit) async { + await event.when( + initialize: () async { + final views = await ViewBackendService().fetchViews(); + emit(state.copyWith(views: views)); + await initializeListeners(); + }, + viewsChanged: (views) { + emit(state.copyWith(views: views)); + }, + performAction: (action, nextActions) { + emit(state.copyWith(action: action, nextActions: nextActions)); + + if (nextActions.isNotEmpty) { + final newActions = [...nextActions]; + final next = newActions.removeAt(0); + + add( + ActionNavigationEvent.performAction( + action: next, + nextActions: newActions, + ), + ); + } else { + emit(state.setNoAction()); + } + }, + ); + }); + } + + WorkspaceListener? _workspaceListener; + + @override + Future close() async { + await _workspaceListener?.stop(); + return super.close(); + } + + Future initializeListeners() async { + if (_workspaceListener != null) { + return; + } + + final userOrFailure = await getIt().getUser(); + final user = userOrFailure.fold((s) => s, (f) => null); + if (user == null) { + _workspaceListener = null; + return; + } + + final workspaceSettingsOrFailure = + await FolderEventGetCurrentWorkspaceSetting().send(); + final workspaceId = workspaceSettingsOrFailure.fold( + (s) => s.workspaceId, + (f) => null, + ); + if (workspaceId == null) { + _workspaceListener = null; + return; + } + + _workspaceListener = WorkspaceListener( + user: user, + workspaceId: workspaceId, + ); + + _workspaceListener?.start( + appsChanged: (_) async { + final views = await ViewBackendService().fetchViews(); + add(ActionNavigationEvent.viewsChanged(views)); + }, + ); + } +} + +@freezed +class ActionNavigationEvent with _$ActionNavigationEvent { + const factory ActionNavigationEvent.initialize() = _Initialize; + + const factory ActionNavigationEvent.performAction({ + required NavigationAction action, + @Default([]) List nextActions, + }) = _PerformAction; + + const factory ActionNavigationEvent.viewsChanged(List views) = + _ViewsChanged; +} + +class ActionNavigationState { + const ActionNavigationState.initial() + : action = null, + nextActions = const [], + views = const []; + + const ActionNavigationState({ + required this.action, + this.nextActions = const [], + this.views = const [], + }); + + final NavigationAction? action; + final List nextActions; + final List views; + + ActionNavigationState copyWith({ + NavigationAction? action, + List? nextActions, + List? views, + }) => + ActionNavigationState( + action: action ?? this.action, + nextActions: nextActions ?? this.nextActions, + views: views ?? this.views, + ); + + ActionNavigationState setNoAction() => + ActionNavigationState(action: null, nextActions: [], views: views); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action.dart b/frontend/appflowy_flutter/lib/workspace/application/action_navigation/navigation_action.dart similarity index 75% rename from frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action.dart rename to frontend/appflowy_flutter/lib/workspace/application/action_navigation/navigation_action.dart index cb05bc88ce..ee68ea7c0d 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/action_navigation/navigation_action.dart @@ -10,13 +10,13 @@ class ActionArgumentKeys { static String rowId = "row_id"; } -/// A [NotificationAction] is used to communicate with the -/// [NotificationActionBloc] to perform actions based on an event +/// A [NavigationAction] is used to communicate with the +/// [ActionNavigationBloc] to perform actions based on an event /// triggered by pressing a notification, such as opening a specific /// view and jumping to a specific block. /// -class NotificationAction { - const NotificationAction({ +class NavigationAction { + const NavigationAction({ this.type = ActionType.openView, this.arguments, required this.objectId, @@ -27,12 +27,12 @@ class NotificationAction { final String objectId; final Map? arguments; - NotificationAction copyWith({ + NavigationAction copyWith({ ActionType? type, String? objectId, Map? arguments, }) => - NotificationAction( + NavigationAction( type: type ?? this.type, objectId: objectId ?? this.objectId, arguments: arguments ?? this.arguments, diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart new file mode 100644 index 0000000000..f2e0d3cf02 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart @@ -0,0 +1,181 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'package:appflowy/plugins/trash/application/trash_listener.dart'; +import 'package:appflowy/plugins/trash/application/trash_service.dart'; +import 'package:appflowy/workspace/application/command_palette/search_listener.dart'; +import 'package:appflowy/workspace/application/command_palette/search_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/entities.pb.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'command_palette_bloc.freezed.dart'; + +class CommandPaletteBloc + extends Bloc { + CommandPaletteBloc() : super(CommandPaletteState.initial()) { + _searchListener.start( + onResultsChanged: _onResultsChanged, + onResultsClosed: _onResultsClosed, + ); + + _initTrash(); + + _dispatch(); + } + + Timer? _debounceOnChanged; + final TrashService _trashService = TrashService(); + final SearchListener _searchListener = SearchListener(); + final TrashListener _trashListener = TrashListener(); + String? _oldQuery; + + @override + Future close() { + _trashListener.close(); + _searchListener.stop(); + return super.close(); + } + + void _dispatch() { + on((event, emit) async { + event.when( + searchChanged: _debounceOnSearchChanged, + trashChanged: (trash) async { + if (trash != null) { + emit(state.copyWith(trash: trash)); + return; + } + + final trashOrFailure = await _trashService.readTrash(); + final trashRes = trashOrFailure.fold( + (trash) => trash, + (error) => null, + ); + + if (trashRes != null) { + emit(state.copyWith(trash: trashRes.items)); + } + }, + performSearch: (search) async { + if (search.isNotEmpty) { + _oldQuery = state.query; + emit(state.copyWith(query: search, isLoading: true)); + await SearchBackendService.performSearch(search); + } else { + emit(state.copyWith(query: null, isLoading: false, results: [])); + } + }, + resultsChanged: (results, didClose) { + if (state.query != _oldQuery) { + emit(state.copyWith(results: [])); + } + + final searchResults = _filterDuplicates(results.items); + searchResults.sort((a, b) => b.score.compareTo(a.score)); + + emit( + state.copyWith( + results: searchResults, + isLoading: !didClose, + ), + ); + }, + ); + }); + } + + Future _initTrash() async { + _trashListener.start( + trashUpdated: (trashOrFailed) { + final trash = trashOrFailed.fold( + (trash) => trash, + (error) => null, + ); + + add(CommandPaletteEvent.trashChanged(trash: trash)); + }, + ); + + final trashOrFailure = await _trashService.readTrash(); + final trashRes = trashOrFailure.fold( + (trash) => trash, + (error) => null, + ); + + add(CommandPaletteEvent.trashChanged(trash: trashRes?.items)); + } + + void _debounceOnSearchChanged(String value) { + _debounceOnChanged?.cancel(); + _debounceOnChanged = Timer( + const Duration(milliseconds: 300), + () => _performSearch(value), + ); + } + + List _filterDuplicates(List results) { + final currentItems = [...state.results]; + final res = [...results]; + + for (final item in results) { + final duplicateIndex = currentItems.indexWhere((a) => a.id == item.id); + if (duplicateIndex == -1) { + continue; + } + + final duplicate = currentItems[duplicateIndex]; + if (item.score < duplicate.score) { + res.remove(item); + } else { + currentItems.remove(duplicate); + } + } + + return res..addAll(currentItems); + } + + void _performSearch(String value) => + add(CommandPaletteEvent.performSearch(search: value)); + + void _onResultsChanged(RepeatedSearchResultPB results) => + add(CommandPaletteEvent.resultsChanged(results: results)); + + void _onResultsClosed(RepeatedSearchResultPB results) => + add(CommandPaletteEvent.resultsChanged(results: results, didClose: true)); +} + +@freezed +class CommandPaletteEvent with _$CommandPaletteEvent { + const factory CommandPaletteEvent.searchChanged({required String search}) = + _SearchChanged; + + const factory CommandPaletteEvent.performSearch({required String search}) = + _PerformSearch; + + const factory CommandPaletteEvent.resultsChanged({ + required RepeatedSearchResultPB results, + @Default(false) bool didClose, + }) = _ResultsChanged; + + const factory CommandPaletteEvent.trashChanged({ + @Default(null) List? trash, + }) = _TrashChanged; +} + +@freezed +class CommandPaletteState with _$CommandPaletteState { + const CommandPaletteState._(); + + const factory CommandPaletteState({ + @Default(null) String? query, + required List results, + required bool isLoading, + @Default([]) List trash, + }) = _CommandPaletteState; + + factory CommandPaletteState.initial() => + const CommandPaletteState(results: [], isLoading: false); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart new file mode 100644 index 0000000000..9c169a5175 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart @@ -0,0 +1,65 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/search_notification.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/entities.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flowy_infra/notifier.dart'; + +// Do not modify! +const _searchObjectId = "SEARCH_IDENTIFIER"; + +class SearchListener { + SearchListener(); + + PublishNotifier? _updateNotifier = PublishNotifier(); + PublishNotifier? _updateDidCloseNotifier = + PublishNotifier(); + SearchNotificationListener? _listener; + + void start({ + required void Function(RepeatedSearchResultPB) onResultsChanged, + required void Function(RepeatedSearchResultPB) onResultsClosed, + }) { + _updateNotifier?.addPublishListener(onResultsChanged); + _updateDidCloseNotifier?.addPublishListener(onResultsClosed); + _listener = SearchNotificationListener( + objectId: _searchObjectId, + handler: _handler, + ); + } + + void _handler( + SearchNotification ty, + FlowyResult result, + ) { + switch (ty) { + case SearchNotification.DidUpdateResults: + result.fold( + (payload) => _updateNotifier?.value = + RepeatedSearchResultPB.fromBuffer(payload), + (err) => Log.error(err), + ); + break; + case SearchNotification.DidCloseResults: + result.fold( + (payload) => _updateDidCloseNotifier?.value = + RepeatedSearchResultPB.fromBuffer(payload), + (err) => Log.error(err), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _updateNotifier?.dispose(); + _updateNotifier = null; + _updateDidCloseNotifier?.dispose(); + _updateDidCloseNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart new file mode 100644 index 0000000000..798e174be6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/entities.pb.dart'; + +extension GetIcon on SearchResultPB { + Widget? getIcon() { + if (icon.ty == ResultIconTypePB.Emoji) { + return icon.value.isNotEmpty + ? Text( + icon.value, + style: const TextStyle(fontSize: 18.0), + ) + : null; + } else if (icon.ty == ResultIconTypePB.Icon) { + return FlowySvg(icon.getViewSvg(), size: const Size.square(20)); + } + + return null; + } +} + +extension _ToViewIcon on ResultIconPB { + FlowySvgData getViewSvg() => switch (value) { + "0" => FlowySvgs.document_s, + "1" => FlowySvgs.grid_s, + "2" => FlowySvgs.board_s, + "3" => FlowySvgs.date_s, + _ => FlowySvgs.document_s, + }; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart new file mode 100644 index 0000000000..2c0ff1c38d --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart @@ -0,0 +1,14 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/entities.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; + +class SearchBackendService { + static Future> performSearch( + String keyword, + ) async { + final request = SearchQueryPB(search: keyword); + + return SearchEventSearch(request).send(); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/notifications/notification_service.dart b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart similarity index 92% rename from frontend/appflowy_flutter/lib/workspace/application/notifications/notification_service.dart rename to frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart index 13d0820314..5418eb2b1c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/notifications/notification_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; + import 'package:local_notifier/local_notifier.dart'; const _appName = "AppFlowy"; @@ -12,9 +13,7 @@ const _appName = "AppFlowy"; /// class NotificationService { static Future initialize() async { - await localNotifier.setup( - appName: _appName, - ); + await localNotifier.setup(appName: _appName); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action_bloc.dart deleted file mode 100644 index 831c2ed83f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/notifications/notification_action_bloc.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:appflowy/workspace/application/notifications/notification_action.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'notification_action_bloc.freezed.dart'; - -class NotificationActionBloc - extends Bloc { - NotificationActionBloc() : super(const NotificationActionState.initial()) { - on((event, emit) async { - event.when( - performAction: (action, nextActions) { - emit(state.copyWith(action: action, nextActions: nextActions)); - - if (nextActions.isNotEmpty) { - final newActions = [...nextActions]; - final next = newActions.removeAt(0); - - add( - NotificationActionEvent.performAction( - action: next, - nextActions: newActions, - ), - ); - } - }, - ); - }); - } -} - -@freezed -class NotificationActionEvent with _$NotificationActionEvent { - const factory NotificationActionEvent.performAction({ - required NotificationAction action, - @Default([]) List nextActions, - }) = _PerformAction; -} - -class NotificationActionState { - const NotificationActionState.initial() - : action = null, - nextActions = const []; - - const NotificationActionState({ - required this.action, - this.nextActions = const [], - }); - - final NotificationAction? action; - final List nextActions; - - NotificationActionState copyWith({ - NotificationAction? action, - List? nextActions, - }) => - NotificationActionState( - action: action ?? this.action, - nextActions: nextActions ?? this.nextActions, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart index a454952016..a0c16cd4b3 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/recent/recent_views_bloc.dart @@ -29,9 +29,7 @@ class RecentViewsBloc extends Bloc { await event.map( initial: (e) async { _listener.start( - recentViewsUpdated: (result) => _onRecentViewsUpdated( - result, - ), + recentViewsUpdated: (result) => _onRecentViewsUpdated(result), ); add(const RecentViewsEvent.fetchRecentViews()); }, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index a8ffc0516e..ca01acd058 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -167,9 +167,10 @@ class ViewBackendService { static Future> updateViewIcon({ required String viewId, required String viewIcon, + ViewIconTypePB iconType = ViewIconTypePB.Emoji, }) { final icon = ViewIconPB() - ..ty = ViewIconTypePB.Emoji + ..ty = iconType ..value = viewIcon; final payload = UpdateViewIconPayloadPB.create() ..viewId = viewId diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart new file mode 100644 index 0000000000..9a7dd29044 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart @@ -0,0 +1,243 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_results_list.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CommandPalette extends InheritedWidget { + CommandPalette({ + super.key, + required Widget? child, + required ValueNotifier toggleNotifier, + }) : _toggleNotifier = toggleNotifier, + super( + child: _CommandPaletteController( + toggleNotifier: toggleNotifier, + child: child, + ), + ); + + final ValueNotifier _toggleNotifier; + + void toggle() => _toggleNotifier.value = !_toggleNotifier.value; + + static CommandPalette of(BuildContext context) { + final CommandPalette? result = + context.dependOnInheritedWidgetOfExactType(); + + assert(result != null, "CommandPalette could not be found"); + + return result!; + } + + @override + bool updateShouldNotify(covariant InheritedWidget oldWidget) => false; +} + +class _ToggleCommandPaletteIntent extends Intent { + const _ToggleCommandPaletteIntent(); +} + +class _CommandPaletteController extends StatefulWidget { + const _CommandPaletteController({ + required this.toggleNotifier, + required this.child, + }); + + final Widget? child; + final ValueNotifier toggleNotifier; + + @override + State<_CommandPaletteController> createState() => + _CommandPaletteControllerState(); +} + +class _CommandPaletteControllerState extends State<_CommandPaletteController> { + late final CommandPaletteBloc _commandPaletteBloc; + late ValueNotifier _toggleNotifier = widget.toggleNotifier; + bool _isOpen = false; + + @override + void didUpdateWidget(covariant _CommandPaletteController oldWidget) { + if (oldWidget.toggleNotifier != widget.toggleNotifier) { + _toggleNotifier.removeListener(_onToggle); + _toggleNotifier.dispose(); + _toggleNotifier = widget.toggleNotifier; + + // If widget is changed, eg. on theme mode hotkey used + // while modal is shown, set the value before listening + _toggleNotifier.value = _isOpen; + + _toggleNotifier.addListener(_onToggle); + } + + super.didUpdateWidget(oldWidget); + } + + @override + void initState() { + super.initState(); + _toggleNotifier.addListener(_onToggle); + _commandPaletteBloc = CommandPaletteBloc(); + } + + @override + void dispose() { + _toggleNotifier.removeListener(_onToggle); + _toggleNotifier.dispose(); + _commandPaletteBloc.close(); + super.dispose(); + } + + void _onToggle() { + if (widget.toggleNotifier.value && !_isOpen) { + _isOpen = true; + FlowyOverlay.show( + context: context, + builder: (_) => BlocProvider.value( + value: _commandPaletteBloc, + child: CommandPaletteModal(shortcutBuilder: _buildShortcut), + ), + ).then((_) { + _isOpen = false; + widget.toggleNotifier.value = false; + }); + } else if (!widget.toggleNotifier.value && _isOpen) { + FlowyOverlay.pop(context); + _isOpen = false; + } + } + + @override + Widget build(BuildContext context) => + _buildShortcut(widget.child ?? const SizedBox.shrink()); + + Widget _buildShortcut(Widget child) => FocusableActionDetector( + actions: { + _ToggleCommandPaletteIntent: + CallbackAction<_ToggleCommandPaletteIntent>( + onInvoke: (intent) => + _toggleNotifier.value = !_toggleNotifier.value, + ), + }, + shortcuts: { + LogicalKeySet( + PlatformExtension.isMacOS + ? LogicalKeyboardKey.meta + : LogicalKeyboardKey.control, + LogicalKeyboardKey.keyP, + ): const _ToggleCommandPaletteIntent(), + }, + child: child, + ); +} + +class CommandPaletteModal extends StatelessWidget { + const CommandPaletteModal({super.key, required this.shortcutBuilder}); + + final Widget Function(Widget) shortcutBuilder; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return FlowyDialog( + alignment: Alignment.topCenter, + insetPadding: const EdgeInsets.only(top: 100), + constraints: const BoxConstraints(maxHeight: 420, maxWidth: 510), + expandHeight: false, + child: shortcutBuilder( + Column( + mainAxisSize: MainAxisSize.min, + children: [ + SearchField(query: state.query, isLoading: state.isLoading), + if ((state.query?.isEmpty ?? true) || + state.isLoading && state.results.isEmpty) ...[ + const Divider(height: 0), + Flexible( + child: RecentViewsList( + onSelected: () => FlowyOverlay.pop(context), + ), + ), + ], + if (state.results.isNotEmpty) ...[ + const Divider(height: 0), + Flexible( + child: SearchResultsList( + trash: state.trash, + results: state.results, + ), + ), + ], + _CommandPaletteFooter( + shouldShow: state.results.isNotEmpty && + (state.query?.isNotEmpty ?? false), + ), + ], + ), + ), + ); + }, + ); + } +} + +class _CommandPaletteFooter extends StatelessWidget { + const _CommandPaletteFooter({ + required this.shouldShow, + }); + + final bool shouldShow; + + @override + Widget build(BuildContext context) { + if (!shouldShow) { + return const SizedBox.shrink(); + } + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: Theme.of(context).dividerColor, + ), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 1, + ), + decoration: BoxDecoration( + color: AFThemeExtension.of(context).lightGreyHover, + borderRadius: BorderRadius.circular(4), + ), + child: const FlowyText.semibold( + 'TAB', + fontSize: 10, + ), + ), + const HSpace(4), + FlowyText( + LocaleKeys.commandPalette_navigateHint.tr(), + fontSize: 11, + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart new file mode 100644 index 0000000000..713fe5bd14 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +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_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +class RecentViewTile extends StatelessWidget { + const RecentViewTile({ + super.key, + required this.icon, + required this.view, + required this.onSelected, + }); + + final Widget icon; + final ViewPB view; + final VoidCallback onSelected; + + @override + Widget build(BuildContext context) { + return ListTile( + dense: true, + title: Row( + children: [ + icon, + const HSpace(4), + FlowyText(view.name), + ], + ), + focusColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), + hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), + onTap: () { + onSelected(); + + getIt().add( + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: view.id), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart new file mode 100644 index 0000000000..2087d1e476 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.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'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class RecentViewsList extends StatelessWidget { + const RecentViewsList({super.key, required this.onSelected}); + + final VoidCallback onSelected; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + RecentViewsBloc()..add(const RecentViewsEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + // We remove duplicates by converting the list to a set first + final List recentViews = + state.views.reversed.toSet().toList(); + + return ListView.separated( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemCount: recentViews.length + 1, + itemBuilder: (_, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: FlowyText( + LocaleKeys.commandPalette_recentHistory.tr(), + ), + ); + } + + final view = recentViews[index - 1]; + final icon = view.icon.value.isNotEmpty + ? Text( + view.icon.value, + style: const TextStyle(fontSize: 18.0), + ) + : FlowySvg(view.iconData, size: const Size.square(20)); + + return RecentViewTile( + icon: icon, + view: view, + onSelected: onSelected, + ); + }, + separatorBuilder: (_, __) => const Divider(height: 0), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart new file mode 100644 index 0000000000..d171123e7d --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SearchField extends StatelessWidget { + const SearchField({super.key, this.query, this.isLoading = false}); + + final String? query; + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const HSpace(12), + FlowySvg( + FlowySvgs.search_m, + color: Theme.of(context).hintColor, + ), + Expanded( + child: FlowyTextField( + controller: TextEditingController(text: query), + textStyle: + Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 14), + decoration: InputDecoration( + constraints: const BoxConstraints(maxHeight: 48), + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: Corners.s8Border, + ), + isDense: false, + hintText: LocaleKeys.commandPalette_placeholder.tr(), + hintStyle: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 14, + color: Theme.of(context).hintColor, + ), + errorStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).colorScheme.error), + // TODO(Mathias): Remove beta when support document/database search + suffix: FlowyTooltip( + message: LocaleKeys.commandPalette_betaTooltip.tr(), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 1, + ), + decoration: BoxDecoration( + color: AFThemeExtension.of(context).lightGreyHover, + borderRadius: BorderRadius.circular(4), + ), + child: FlowyText.semibold( + LocaleKeys.commandPalette_betaLabel.tr(), + fontSize: 10, + ), + ), + ), + counterText: "", + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + borderRadius: Corners.s8Border, + ), + errorBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + borderRadius: Corners.s8Border, + ), + ), + onChanged: (value) => context + .read() + .add(CommandPaletteEvent.searchChanged(search: value)), + ), + ), + if (isLoading) ...[ + const HSpace(12), + FlowyTooltip( + message: LocaleKeys.commandPalette_loadingTooltip.tr(), + child: const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2.5), + ), + ), + const HSpace(12), + ], + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart new file mode 100644 index 0000000000..7918e5e723 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +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/search_result_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +class SearchResultTile extends StatelessWidget { + const SearchResultTile({ + super.key, + required this.result, + required this.onSelected, + this.isTrashed = false, + }); + + final SearchResultPB result; + final VoidCallback onSelected; + final bool isTrashed; + + @override + Widget build(BuildContext context) { + final icon = result.getIcon(); + + return ListTile( + dense: true, + title: Row( + children: [ + if (icon != null) ...[icon, const HSpace(6)], + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isTrashed) ...[ + FlowyText( + LocaleKeys.commandPalette_fromTrashHint.tr(), + color: AFThemeExtension.of(context).textColor.withAlpha(175), + fontSize: 10, + ), + ], + FlowyText(result.data), + ], + ), + ], + ), + focusColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), + hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.5), + onTap: () { + onSelected(); + + getIt().add( + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: result.viewId), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart new file mode 100644 index 0000000000..6f1f2f1a6e --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/entities.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +class SearchResultsList extends StatelessWidget { + const SearchResultsList({ + super.key, + required this.trash, + required this.results, + }); + + final List trash; + final List results; + + @override + Widget build(BuildContext context) { + return ListView.separated( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + separatorBuilder: (_, __) => const Divider(height: 0), + itemCount: results.length + 1, + itemBuilder: (_, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8) + + const EdgeInsets.only(left: 16), + child: FlowyText( + LocaleKeys.commandPalette_bestMatches.tr(), + ), + ); + } + + final result = results[index - 1]; + return SearchResultTile( + result: result, + onSelected: () => FlowyOverlay.pop(context), + isTrashed: trash.any((t) => t.id == result.viewId), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart index 1add004e82..2eeba329ed 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart @@ -1,3 +1,6 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; @@ -22,13 +25,12 @@ import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_infra_ui/style_widget/container.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:sized_context/sized_context.dart'; import 'package:styled_widget/styled_widget.dart'; import '../widgets/edit_panel/edit_panel.dart'; + import 'home_layout.dart'; import 'home_stack.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 8e845e1f8c..11cbcf71cf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -1,10 +1,12 @@ import 'dart:async'; +import 'package:flutter/material.dart'; + import 'package:appflowy/shared/feature_flags.dart'; 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/menu/sidebar_sections_bloc.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action.dart'; -import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -20,7 +22,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Home Sidebar is the left side bar of the home page. @@ -71,9 +72,7 @@ class HomeSideBar extends StatelessWidget { builder: (context, state) { return MultiBlocProvider( providers: [ - BlocProvider( - create: (_) => getIt(), - ), + BlocProvider(create: (_) => getIt()), BlocProvider( create: (_) => SidebarSectionsBloc() ..add( @@ -96,7 +95,7 @@ class HomeSideBar extends StatelessWidget { ), ), ), - BlocListener( + BlocListener( listenWhen: (_, curr) => curr.action != null, listener: _onNotificationAction, ), @@ -122,35 +121,28 @@ class HomeSideBar extends StatelessWidget { void _onNotificationAction( BuildContext context, - NotificationActionState state, + ActionNavigationState state, ) { final action = state.action; - if (action != null) { - if (action.type == ActionType.openView) { - final view = context - .read() - .state - .section - .publicViews - .findView(action.objectId); + if (action?.type == ActionType.openView) { + final view = state.views.findView(action!.objectId); - if (view != null) { - final Map arguments = {}; + if (view != null) { + final Map arguments = {}; - final nodePath = action.arguments?[ActionArgumentKeys.nodePath]; - if (nodePath != null) { - arguments[PluginArgumentKeys.selection] = Selection.collapsed( - Position(path: [nodePath]), - ); - } - - final rowId = action.arguments?[ActionArgumentKeys.rowId]; - if (rowId != null) { - arguments[PluginArgumentKeys.rowId] = rowId; - } - - context.read().openPlugin(view, arguments: arguments); + final nodePath = action.arguments?[ActionArgumentKeys.nodePath]; + if (nodePath != null) { + arguments[PluginArgumentKeys.selection] = Selection.collapsed( + Position(path: [nodePath]), + ); } + + final rowId = action.arguments?[ActionArgumentKeys.rowId]; + if (rowId != null) { + arguments[PluginArgumentKeys.rowId] = rowId; + } + + context.read().openPlugin(view, arguments: arguments); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 19876b8eab..b1053b9009 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; @@ -17,13 +19,13 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.d import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; typedef ViewItemOnSelected = void Function(ViewPB); @@ -418,6 +420,7 @@ class _SingleInnerViewItemState extends State { ViewBackendService.updateViewIcon( viewId: widget.view.id, viewIcon: result.emoji, + iconType: result.type.toProto(), ); controller.close(); }, diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart index d32663f470..a3aea9fb84 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart @@ -3,6 +3,8 @@ import 'dart:convert' show utf8; import 'dart:ffi'; import 'dart:typed_data'; +import 'package:flutter/services.dart'; + import 'package:appflowy_backend/ffi.dart' as ffi; import 'package:appflowy_backend/log.dart'; // ignore: unnecessary_import @@ -15,7 +17,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:ffi/ffi.dart'; -import 'package:flutter/services.dart'; import 'package:isolates/isolates.dart'; import 'package:isolates/ports.dart'; import 'package:protobuf/protobuf.dart'; @@ -24,14 +25,18 @@ import '../protobuf/flowy-config/entities.pb.dart'; import '../protobuf/flowy-config/event_map.pb.dart'; import '../protobuf/flowy-date/entities.pb.dart'; import '../protobuf/flowy-date/event_map.pb.dart'; +import '../protobuf/flowy-search/entities.pb.dart'; +import '../protobuf/flowy-search/event_map.pb.dart'; + import 'error.dart'; -part 'dart_event/flowy-config/dart_event.dart'; -part 'dart_event/flowy-database2/dart_event.dart'; -part 'dart_event/flowy-date/dart_event.dart'; -part 'dart_event/flowy-document/dart_event.dart'; part 'dart_event/flowy-folder/dart_event.dart'; part 'dart_event/flowy-user/dart_event.dart'; +part 'dart_event/flowy-database2/dart_event.dart'; +part 'dart_event/flowy-document/dart_event.dart'; +part 'dart_event/flowy-config/dart_event.dart'; +part 'dart_event/flowy-date/dart_event.dart'; +part 'dart_event/flowy-search/dart_event.dart'; enum FFIException { RequestIsEmpty, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart index cd37051220..d34fcf92f8 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart @@ -1,9 +1,12 @@ -import 'package:flutter/material.dart'; import 'dart:math'; +import 'package:flutter/material.dart'; + const _overlayContainerPadding = EdgeInsets.symmetric(vertical: 12); const overlayContainerMaxWidth = 760.0; const overlayContainerMinWidth = 320.0; +const _defaultInsetPadding = + EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0); class FlowyDialog extends StatelessWidget { const FlowyDialog({ @@ -14,6 +17,9 @@ class FlowyDialog extends StatelessWidget { this.constraints, this.padding = _overlayContainerPadding, this.backgroundColor, + this.expandHeight = true, + this.alignment, + this.insetPadding, }); final Widget? title; @@ -22,28 +28,40 @@ class FlowyDialog extends StatelessWidget { final BoxConstraints? constraints; final EdgeInsets padding; final Color? backgroundColor; + final bool expandHeight; + + // Position of the Dialog + final Alignment? alignment; + + // Inset of the Dialog + final EdgeInsets? insetPadding; @override Widget build(BuildContext context) { final windowSize = MediaQuery.of(context).size; final size = windowSize * 0.7; + return SimpleDialog( - contentPadding: EdgeInsets.zero, - backgroundColor: backgroundColor ?? Theme.of(context).cardColor, - title: title, - shape: shape ?? - RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - children: [ - Material( - type: MaterialType.transparency, - child: Container( - height: size.height, - width: max(min(size.width, overlayContainerMaxWidth), - overlayContainerMinWidth), - constraints: constraints, - child: child, - ), - ) - ]); + alignment: alignment, + insetPadding: insetPadding ?? _defaultInsetPadding, + contentPadding: EdgeInsets.zero, + backgroundColor: backgroundColor ?? Theme.of(context).cardColor, + title: title, + shape: shape ?? + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + clipBehavior: Clip.hardEdge, + children: [ + Material( + type: MaterialType.transparency, + child: Container( + height: expandHeight ? size.height : null, + width: max(min(size.width, overlayContainerMaxWidth), + overlayContainerMinWidth), + constraints: constraints, + child: child, + ), + ) + ], + ); } } diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 126df830cf..6d36505979 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -189,6 +189,7 @@ dependencies = [ "flowy-document", "flowy-error", "flowy-notification", + "flowy-search", "flowy-user", "lib-dispatch", "serde", @@ -201,6 +202,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "arrayvec" version = "0.7.4" @@ -366,6 +373,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "bitpacking" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +dependencies = [ + "crunchy", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -582,6 +598,12 @@ dependencies = [ "jobserver", ] +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + [[package]] name = "cesu8" version = "1.1.0" @@ -873,7 +895,7 @@ dependencies = [ "getrandom 0.2.10", "js-sys", "lazy_static", - "lru", + "lru 0.12.0", "nanoid", "parking_lot 0.12.1", "rayon", @@ -1188,6 +1210,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1299,7 +1327,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 2.0.47", ] @@ -1514,6 +1542,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "dtoa" version = "1.0.6" @@ -1595,23 +1629,12 @@ checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -1657,6 +1680,12 @@ dependencies = [ "syn 2.0.47", ] +[[package]] +name = "fastdivide" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25c7df09945d65ea8d70b3321547ed414bbc540aad5bac6883d021b970f35b04" + [[package]] name = "fastrand" version = "2.0.1" @@ -1727,7 +1756,7 @@ dependencies = [ "console", "fancy-regex 0.10.0", "flowy-ast", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "phf 0.8.0", @@ -1779,6 +1808,7 @@ dependencies = [ "flowy-error", "flowy-folder", "flowy-folder-pub", + "flowy-search", "flowy-server", "flowy-server-pub", "flowy-sqlite", @@ -1840,7 +1870,7 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", - "lru", + "lru 0.12.0", "nanoid", "parking_lot 0.12.1", "protobuf", @@ -1912,7 +1942,7 @@ dependencies = [ "indexmap 2.1.0", "lib-dispatch", "lib-infra", - "lru", + "lru 0.12.0", "nanoid", "parking_lot 0.12.1", "protobuf", @@ -1974,6 +2004,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", + "tantivy", "thiserror", "tokio", "url", @@ -1988,6 +2019,7 @@ dependencies = [ "bytes", "chrono", "collab", + "collab-document", "collab-entity", "collab-folder", "collab-integrate", @@ -1997,6 +2029,7 @@ dependencies = [ "flowy-error", "flowy-folder-pub", "flowy-notification", + "flowy-search-pub", "lazy_static", "lib-dispatch", "lib-infra", @@ -2039,6 +2072,47 @@ dependencies = [ "tracing", ] +[[package]] +name = "flowy-search" +version = "0.1.0" +dependencies = [ + "async-stream", + "bytes", + "collab", + "collab-folder", + "diesel", + "diesel_derives", + "diesel_migrations", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-notification", + "flowy-search-pub", + "flowy-sqlite", + "flowy-user", + "futures", + "lib-dispatch", + "protobuf", + "serde", + "serde_json", + "strsim 0.11.0", + "strum_macros 0.26.1", + "tantivy", + "tempfile", + "tokio", + "tracing", + "validator", +] + +[[package]] +name = "flowy-search-pub" +version = "0.1.0" +dependencies = [ + "collab", + "collab-folder", + "flowy-error", +] + [[package]] name = "flowy-server" version = "0.1.0" @@ -2229,6 +2303,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "funty" version = "2.0.0" @@ -2844,6 +2928,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "0.2.9" @@ -3134,6 +3224,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -3164,6 +3257,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -3300,6 +3402,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + [[package]] name = "lib-dispatch" version = "0.1.0" @@ -3365,9 +3473,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -3433,9 +3541,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" @@ -3461,6 +3569,7 @@ checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" dependencies = [ "cfg-if", "generator", + "pin-utils", "scoped-tls", "serde", "serde_json", @@ -3468,6 +3577,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +dependencies = [ + "hashbrown 0.14.3", +] + [[package]] name = "lru" version = "0.12.0" @@ -3477,6 +3595,12 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "lz4_flex" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912b45c753ff5f7f5208307e8ace7d2a2e30d024e26d3509f3dce546c044ce15" + [[package]] name = "mac" version = "0.1.1" @@ -3597,12 +3721,31 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +[[package]] +name = "measure_time" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +dependencies = [ + "instant", + "log", +] + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -3691,6 +3834,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "murmurhash32" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9380db4c04d219ac5c51d14996bbf2c2e9a15229771b53f8671eb6c83cf44df" + [[package]] name = "nanoid" version = "0.4.0" @@ -3947,6 +4096,15 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "oneshot" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" +dependencies = [ + "loom", +] + [[package]] name = "opaque-debug" version = "0.3.0" @@ -4039,6 +4197,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "ownedbytes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8a72b918ae8198abb3a18c190288123e1d442b6b9a7d709305fd194688b4b7" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "pango" version = "0.15.10" @@ -4564,7 +4731,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4585,7 +4752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.47", @@ -5137,6 +5304,16 @@ dependencies = [ "librocksdb-sys", ] +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "rust_decimal" version = "1.30.0" @@ -5188,15 +5365,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5662,6 +5839,15 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +[[package]] +name = "sketches-ddsketch" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" +dependencies = [ + "serde", +] + [[package]] name = "slab" version = "0.4.8" @@ -5796,6 +5982,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + [[package]] name = "strum" version = "0.25.0" @@ -5827,6 +6019,19 @@ dependencies = [ "syn 2.0.47", ] +[[package]] +name = "strum_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.47", +] + [[package]] name = "subtle" version = "2.5.0" @@ -5917,6 +6122,146 @@ dependencies = [ "version-compare 0.1.1", ] +[[package]] +name = "tantivy" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6083cd777fa94271b8ce0fe4533772cb8110c3044bab048d20f70108329a1f2" +dependencies = [ + "aho-corasick 1.0.2", + "arc-swap", + "async-trait", + "base64 0.21.5", + "bitpacking", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fastdivide", + "fs4", + "htmlescape", + "itertools 0.11.0", + "levenshtein_automata", + "log", + "lru 0.11.1", + "lz4_flex", + "measure_time", + "memmap2", + "murmurhash32", + "num_cpus", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror", + "time", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecb164321482301f514dd582264fa67f70da2d7eb01872ccd71e35e0d96655a" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d85f8019af9a78b3118c11298b36ffd21c2314bd76bbcd9d12e00124cbb7e70" +dependencies = [ + "fastdivide", + "fnv", + "itertools 0.11.0", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4a3a975e604a2aba6b1106a04505e1e7a025e6def477fab6e410b4126471e1" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time", +] + +[[package]] +name = "tantivy-fst" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +dependencies = [ + "byteorder", + "regex-syntax 0.6.29", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d39c5a03100ac10c96e0c8b07538e2ab8b17da56434ab348309b31f23fada77" +dependencies = [ + "nom", +] + +[[package]] +name = "tantivy-sstable" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0c1bb43e5e8b8e05eb8009610344dbf285f06066c844032fbb3e546b3c71df" +dependencies = [ + "tantivy-common", + "tantivy-fst", + "zstd 0.12.4", +] + +[[package]] +name = "tantivy-stacker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c078595413f13f218cf6f97b23dcfd48936838f1d3d13a1016e05acd64ed6c" +dependencies = [ + "murmurhash32", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "347b6fb212b26d3505d224f438e3c4b827ab8bd847fe9953ad5ac6b8f9443b66" +dependencies = [ + "serde", +] + [[package]] name = "tao" version = "0.16.2" @@ -6204,15 +6549,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", "redox_syscall 0.4.1", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -6369,6 +6714,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.5", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] @@ -6812,6 +7158,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + [[package]] name = "uuid" version = "1.6.1" @@ -7289,6 +7641,15 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.48.0" @@ -7699,7 +8060,7 @@ dependencies = [ "pbkdf2 0.11.0", "sha1", "time", - "zstd", + "zstd 0.11.2+zstd.1.5.2", ] [[package]] @@ -7708,7 +8069,16 @@ version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe", + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe 6.0.6", ] [[package]] @@ -7721,6 +8091,16 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.8+zstd.1.5.5" diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index a41d7df3e6..3378df0b74 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -61,6 +61,7 @@ flowy-error = { path = "../../rust-lib/flowy-error", features = [ "impl_from_serde", "tauri_ts", ] } +flowy-search = { path = "../../rust-lib/flowy-search", features = ["tauri_ts"] } flowy-document = { path = "../../rust-lib/flowy-document", features = [ "tauri_ts", ] } diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs index 40c0e5d47b..25f4f3c6cc 100644 --- a/frontend/appflowy_tauri/src-tauri/src/init.rs +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -1,4 +1,5 @@ use flowy_core::config::AppFlowyCoreConfig; +use flowy_core::integrate::log::create_log_filter; use flowy_core::{AppFlowyCore, DEFAULT_NAME}; use lib_dispatch::runtime::AFPluginRuntime; use std::sync::Arc; @@ -51,7 +52,10 @@ pub fn init_flowy_core() -> AppFlowyCore { device_id, DEFAULT_NAME.to_string(), ) - .log_filter("trace", vec!["appflowy_tauri".to_string()]); + .log_filter(create_log_filter( + "trace".to_owned(), + vec!["appflowy_tauri".to_string()], + )); let runtime = Arc::new(AFPluginRuntime::new().unwrap()); let cloned_runtime = runtime.clone(); diff --git a/frontend/appflowy_tauri/src/services/backend/index.ts b/frontend/appflowy_tauri/src/services/backend/index.ts index 9ee93aa123..1cc17c1e1b 100644 --- a/frontend/appflowy_tauri/src/services/backend/index.ts +++ b/frontend/appflowy_tauri/src/services/backend/index.ts @@ -5,3 +5,4 @@ export * from "./models/flowy-document"; export * from "./models/flowy-error"; export * from "./models/flowy-config"; export * from "./models/flowy-date"; +export * from "./models/flowy-search"; diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 67c80237e3..8c55eb4722 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1421,5 +1421,15 @@ "synced": "Everything is up to date", "noNetworkConnected": "No network connected" } + }, + "commandPalette": { + "placeholder": "Type to search for views...", + "bestMatches": "Best matches", + "recentHistory": "Recent history", + "navigateHint": "to navigate", + "loadingTooltip": "We are looking for results...", + "betaLabel": "BETA", + "betaTooltip": "We currently only support searching for pages", + "fromTrashHint": "From trash" } -} \ No newline at end of file +} diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 74240017cd..96deb557de 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -178,6 +178,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "arrayvec" version = "0.7.4" @@ -426,6 +432,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "bitpacking" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +dependencies = [ + "crunchy", +] + [[package]] name = "bitvec" version = "1.0.1" @@ -597,6 +612,12 @@ dependencies = [ "libc", ] +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + [[package]] name = "cexpr" version = "0.6.0" @@ -799,7 +820,7 @@ dependencies = [ "getrandom 0.2.10", "js-sys", "lazy_static", - "lru", + "lru 0.12.0", "nanoid", "parking_lot 0.12.1", "rayon", @@ -1105,6 +1126,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1189,7 +1216,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] @@ -1301,6 +1328,9 @@ name = "deranged" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +dependencies = [ + "serde", +] [[package]] name = "derivative" @@ -1361,6 +1391,12 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d95203a6a50906215a502507c0f879a0ce7ff205a6111e2db2a5ef8e4bb92e43" +[[package]] +name = "deunicode" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94" + [[package]] name = "diesel" version = "2.1.4" @@ -1423,6 +1459,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + [[package]] name = "dtoa" version = "1.0.9" @@ -1489,23 +1531,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -1532,6 +1563,7 @@ dependencies = [ "flowy-folder", "flowy-folder-pub", "flowy-notification", + "flowy-search", "flowy-server", "flowy-server-pub", "flowy-storage", @@ -1552,6 +1584,7 @@ dependencies = [ "tokio-postgres", "tracing", "uuid", + "walkdir", "zip", ] @@ -1579,12 +1612,12 @@ dependencies = [ [[package]] name = "fake" -version = "2.8.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9af7b0c58ac9d03169e27f080616ce9f64004edca3d2ef4147a811c21b23b319" +checksum = "1c25829bde82205da46e1823b2259db6273379f626fc211f126f65654a2669be" dependencies = [ + "deunicode 1.4.3", "rand 0.8.5", - "unidecode", ] [[package]] @@ -1626,10 +1659,16 @@ dependencies = [ ] [[package]] -name = "fastrand" -version = "2.0.0" +name = "fastdivide" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25c7df09945d65ea8d70b3321547ed414bbc540aad5bac6883d021b970f35b04" + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "finl_unicode" @@ -1684,7 +1723,7 @@ dependencies = [ "console", "fancy-regex 0.10.0", "flowy-ast", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "phf 0.8.0", @@ -1737,6 +1776,7 @@ dependencies = [ "flowy-error", "flowy-folder", "flowy-folder-pub", + "flowy-search", "flowy-server", "flowy-server-pub", "flowy-sqlite", @@ -1799,7 +1839,7 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", - "lru", + "lru 0.12.0", "nanoid", "parking_lot 0.12.1", "protobuf", @@ -1871,7 +1911,7 @@ dependencies = [ "indexmap 2.1.0", "lib-dispatch", "lib-infra", - "lru", + "lru 0.12.0", "nanoid", "parking_lot 0.12.1", "protobuf", @@ -1935,6 +1975,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", + "tantivy", "thiserror", "tokio", "url", @@ -1949,6 +1990,7 @@ dependencies = [ "bytes", "chrono", "collab", + "collab-document", "collab-entity", "collab-folder", "collab-integrate", @@ -1958,6 +2000,7 @@ dependencies = [ "flowy-error", "flowy-folder-pub", "flowy-notification", + "flowy-search-pub", "lazy_static", "lib-dispatch", "lib-infra", @@ -2001,6 +2044,47 @@ dependencies = [ "tracing", ] +[[package]] +name = "flowy-search" +version = "0.1.0" +dependencies = [ + "async-stream", + "bytes", + "collab", + "collab-folder", + "diesel", + "diesel_derives", + "diesel_migrations", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-notification", + "flowy-search-pub", + "flowy-sqlite", + "flowy-user", + "futures", + "lib-dispatch", + "protobuf", + "serde", + "serde_json", + "strsim 0.11.0", + "strum_macros 0.26.1", + "tantivy", + "tempfile", + "tokio", + "tracing", + "validator", +] + +[[package]] +name = "flowy-search-pub" +version = "0.1.0" +dependencies = [ + "collab", + "collab-folder", + "flowy-error", +] + [[package]] name = "flowy-server" version = "0.1.0" @@ -2203,6 +2287,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -2329,6 +2423,19 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2581,7 +2688,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2598,6 +2705,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "0.2.9" @@ -2868,6 +2981,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -2885,6 +3001,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -2935,6 +3060,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + [[package]] name = "lib-dispatch" version = "0.1.0" @@ -3003,9 +3134,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -3061,9 +3192,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" @@ -3081,6 +3212,29 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "pin-utils", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +dependencies = [ + "hashbrown 0.14.3", +] + [[package]] name = "lru" version = "0.12.0" @@ -3090,6 +3244,12 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "lz4_flex" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912b45c753ff5f7f5208307e8ace7d2a2e30d024e26d3509f3dce546c044ce15" + [[package]] name = "mac" version = "0.1.1" @@ -3202,12 +3362,31 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +[[package]] +name = "measure_time" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +dependencies = [ + "instant", + "log", +] + [[package]] name = "memchr" version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +[[package]] +name = "memmap2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -3277,7 +3456,7 @@ checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3286,6 +3465,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "murmurhash32" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9380db4c04d219ac5c51d14996bbf2c2e9a15229771b53f8671eb6c83cf44df" + [[package]] name = "nanoid" version = "0.4.0" @@ -3415,6 +3600,15 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "oneshot" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" +dependencies = [ + "loom", +] + [[package]] name = "opaque-debug" version = "0.3.0" @@ -3491,6 +3685,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "ownedbytes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8a72b918ae8198abb3a18c190288123e1d442b6b9a7d709305fd194688b4b7" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -3967,7 +4170,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -3988,7 +4191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.47", @@ -4398,15 +4601,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "regex" version = "1.9.5" @@ -4586,6 +4780,16 @@ dependencies = [ "librocksdb-sys", ] +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "rust_decimal" version = "1.32.0" @@ -4626,15 +4830,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -4717,7 +4921,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -4729,6 +4933,12 @@ dependencies = [ "parking_lot 0.12.1", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -5024,6 +5234,15 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "sketches-ddsketch" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" +dependencies = [ + "serde", +] + [[package]] name = "slab" version = "0.4.9" @@ -5039,7 +5258,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" dependencies = [ - "deunicode", + "deunicode 0.4.4", ] [[package]] @@ -5074,7 +5293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -5132,6 +5351,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + [[package]] name = "strum" version = "0.25.0" @@ -5163,6 +5388,19 @@ dependencies = [ "syn 2.0.47", ] +[[package]] +name = "strum_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.47", +] + [[package]] name = "subtle" version = "2.5.0" @@ -5233,6 +5471,146 @@ dependencies = [ "libc", ] +[[package]] +name = "tantivy" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6083cd777fa94271b8ce0fe4533772cb8110c3044bab048d20f70108329a1f2" +dependencies = [ + "aho-corasick", + "arc-swap", + "async-trait", + "base64 0.21.5", + "bitpacking", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fastdivide", + "fs4", + "htmlescape", + "itertools 0.11.0", + "levenshtein_automata", + "log", + "lru 0.11.1", + "lz4_flex", + "measure_time", + "memmap2", + "murmurhash32", + "num_cpus", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror", + "time", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecb164321482301f514dd582264fa67f70da2d7eb01872ccd71e35e0d96655a" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d85f8019af9a78b3118c11298b36ffd21c2314bd76bbcd9d12e00124cbb7e70" +dependencies = [ + "fastdivide", + "fnv", + "itertools 0.11.0", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4a3a975e604a2aba6b1106a04505e1e7a025e6def477fab6e410b4126471e1" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time", +] + +[[package]] +name = "tantivy-fst" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +dependencies = [ + "byteorder", + "regex-syntax 0.6.29", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d39c5a03100ac10c96e0c8b07538e2ab8b17da56434ab348309b31f23fada77" +dependencies = [ + "nom", +] + +[[package]] +name = "tantivy-sstable" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0c1bb43e5e8b8e05eb8009610344dbf285f06066c844032fbb3e546b3c71df" +dependencies = [ + "tantivy-common", + "tantivy-fst", + "zstd 0.12.4", +] + +[[package]] +name = "tantivy-stacker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c078595413f13f218cf6f97b23dcfd48936838f1d3d13a1016e05acd64ed6c" +dependencies = [ + "murmurhash32", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "347b6fb212b26d3505d224f438e3c4b827ab8bd847fe9953ad5ac6b8f9443b66" +dependencies = [ + "serde", +] + [[package]] name = "tap" version = "1.0.1" @@ -5251,15 +5629,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.4.1", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -5406,7 +5783,7 @@ dependencies = [ "socket2 0.5.5", "tokio-macros", "tracing", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -5886,12 +6263,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "unidecode" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc" - [[package]] name = "universal-hash" version = "0.5.1" @@ -5931,6 +6302,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + [[package]] name = "uuid" version = "1.6.1" @@ -6005,9 +6382,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -6234,6 +6611,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -6364,7 +6750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -6449,7 +6835,7 @@ dependencies = [ "pbkdf2 0.11.0", "sha1", "time", - "zstd", + "zstd 0.11.2+zstd.1.5.2", ] [[package]] @@ -6458,7 +6844,16 @@ version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe", + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe 6.0.6", ] [[package]] @@ -6471,6 +6866,16 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.8+zstd.1.5.5" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index b278c1c36a..b8bdd76246 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -24,10 +24,12 @@ members = [ "collab-integrate", "flowy-ai", "flowy-date", + "flowy-search", "lib-infra", "build-tool/flowy-ast", "build-tool/flowy-codegen", "build-tool/flowy-derive", + "flowy-search-pub", ] resolver = "2" @@ -56,6 +58,8 @@ flowy-server-pub = { workspace = true, path = "flowy-server-pub" } flowy-config = { workspace = true, path = "flowy-config" } flowy-encrypt = { workspace = true, path = "flowy-encrypt" } flowy-storage = { workspace = true, path = "flowy-storage" } +flowy-search = { workspace = true, path = "flowy-search" } +flowy-search-pub = { workspace = true, path = "flowy-search-pub" } collab-integrate = { workspace = true, path = "collab-integrate" } flowy-ai = { workspace = true, path = "flowy-ai" } flowy-date = { workspace = true, path = "flowy-date" } diff --git a/frontend/rust-lib/collab-integrate/Cargo.toml b/frontend/rust-lib/collab-integrate/Cargo.toml index 19f5e879ab..1db6d0c88a 100644 --- a/frontend/rust-lib/collab-integrate/Cargo.toml +++ b/frontend/rust-lib/collab-integrate/Cargo.toml @@ -17,8 +17,8 @@ anyhow.workspace = true tracing.workspace = true parking_lot.workspace = true async-trait.workspace = true -tokio = { workspace = true, features = ["sync"]} +tokio = { workspace = true, features = ["sync"] } lib-infra = { workspace = true } [features] -default = [] \ No newline at end of file +default = [] diff --git a/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs b/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs index 86c4a26a63..0c4b3b3558 100644 --- a/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs +++ b/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs @@ -2,7 +2,6 @@ use crate::collab_builder::{CollabPluginProviderContext, CollabPluginProviderTyp use collab::preclude::CollabPlugin; use lib_infra::future::Fut; use std::rc::Rc; -use std::sync::Arc; pub trait CollabCloudPluginProvider: 'static { fn provider_type(&self) -> CollabPluginProviderType; diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 0ae56ce015..b534be34b6 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -65,15 +65,13 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { let _ = save_appflowy_cloud_config(&configuration.root, &configuration.appflowy_cloud_config); } - let log_crates = vec!["flowy-ffi".to_string()]; let config = AppFlowyCoreConfig::new( configuration.app_version, configuration.custom_app_path, configuration.origin_app_path, configuration.device_id, DEFAULT_NAME.to_string(), - ) - .log_filter("info", log_crates); + ); // Ensure that the database is closed before initialization. Also, verify that the init_sdk function can be called // multiple times (is reentrant). Currently, only the database resource is exclusive. @@ -112,10 +110,7 @@ pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) { AFPluginDispatcher::boxed_async_send_with_callback( dispatcher.as_ref(), request, - move |resp: AFPluginEventResponse| { - trace!("[FFI]: Post data to dart through {} port", port); - Box::pin(post_to_flutter(resp, port)) - }, + move |resp: AFPluginEventResponse| Box::pin(post_to_flutter(resp, port)), ); } @@ -161,9 +156,7 @@ async fn post_to_flutter(response: AFPluginEventResponse, port: i64) { }) .await { - Ok(_success) => { - trace!("[FFI]: Post data to dart success"); - }, + Ok(_success) => {}, Err(e) => { if let Some(msg) = e.downcast_ref::<&str>() { error!("[FFI]: {:?}", msg); diff --git a/frontend/rust-lib/event-integration/Cargo.toml b/frontend/rust-lib/event-integration/Cargo.toml index ef8275004f..c31bca3c29 100644 --- a/frontend/rust-lib/event-integration/Cargo.toml +++ b/frontend/rust-lib/event-integration/Cargo.toml @@ -23,6 +23,7 @@ flowy-server-pub = { workspace = true } flowy-notification = { workspace = true } anyhow.workspace = true flowy-storage = { workspace = true } +flowy-search = { workspace = true } serde.workspace = true serde_json.workspace = true @@ -51,6 +52,7 @@ assert-json-diff = "2.0.2" tokio-postgres = { version = "0.7.8" } chrono = "0.4.31" zip = "0.6.6" +walkdir = "2.5.0" [features] default = ["supabase_cloud_test"] diff --git a/frontend/rust-lib/event-integration/src/document/document_event.rs b/frontend/rust-lib/event-integration/src/document/document_event.rs index 49f0f62a9b..a8456d19ea 100644 --- a/frontend/rust-lib/event-integration/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration/src/document/document_event.rs @@ -29,7 +29,7 @@ pub struct OpenDocumentData { impl DocumentEventTest { pub async fn new() -> Self { - let sdk = EventIntegrationTest::new_with_guest_user().await; + let sdk = EventIntegrationTest::new_anon().await; Self { event_test: sdk } } diff --git a/frontend/rust-lib/event-integration/src/folder_event.rs b/frontend/rust-lib/event-integration/src/folder_event.rs index 604bd1475d..416f0301ac 100644 --- a/frontend/rust-lib/event-integration/src/folder_event.rs +++ b/frontend/rust-lib/event-integration/src/folder_event.rs @@ -1,13 +1,16 @@ +use collab_folder::{FolderData, View}; use flowy_folder::entities::icon::UpdateViewIconPayloadPB; -use flowy_folder::entities::*; use flowy_folder::event_map::FolderEvent; use flowy_folder::event_map::FolderEvent::*; +use flowy_folder::{entities::*, ViewLayout}; +use flowy_search::services::manager::{SearchHandler, SearchType}; use flowy_user::entities::{ AddWorkspaceMemberPB, QueryWorkspacePB, RemoveWorkspaceMemberPB, RepeatedWorkspaceMemberPB, WorkspaceMemberPB, }; use flowy_user::errors::FlowyError; use flowy_user::event_map::UserEvent; +use std::sync::Arc; use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; @@ -55,6 +58,49 @@ impl EventIntegrationTest { .parse::() } + pub fn get_folder_search_handler(&self) -> &Arc { + self + .appflowy_core + .search_manager + .get_handler(SearchType::Folder) + .unwrap() + } + + /// create views in the folder. + pub async fn create_views(&self, views: Vec) { + let create_view_params = views + .into_iter() + .map(|view| CreateViewParams { + parent_view_id: view.parent_view_id, + name: view.name, + desc: "".to_string(), + layout: view.layout.into(), + view_id: view.id, + initial_data: vec![], + meta: Default::default(), + set_as_current: false, + index: None, + section: None, + }) + .collect::>(); + + for params in create_view_params { + self + .appflowy_core + .folder_manager + .create_view_with_params(params) + .await + .unwrap(); + } + } + + pub fn get_folder_data(&self) -> FolderData { + let mutex_folder = self.appflowy_core.folder_manager.get_mutex_folder().clone(); + let folder_lock_guard = mutex_folder.lock(); + let folder = folder_lock_guard.as_ref().unwrap(); + folder.get_folder_data().clone().unwrap() + } + pub async fn get_all_workspace_views(&self) -> Vec { EventBuilder::new(self.clone()) .event(FolderEvent::ReadCurrentWorkspaceViews) @@ -153,7 +199,7 @@ pub struct ViewTest { } impl ViewTest { #[allow(dead_code)] - pub async fn new(sdk: &EventIntegrationTest, layout: ViewLayoutPB, data: Vec) -> Self { + pub async fn new(sdk: &EventIntegrationTest, layout: ViewLayout, data: Vec) -> Self { let workspace = sdk.folder_manager.get_current_workspace().await.unwrap(); let payload = CreateViewPayloadPB { @@ -161,7 +207,7 @@ impl ViewTest { name: "View A".to_string(), desc: "".to_string(), thumbnail: Some("http://1.png".to_string()), - layout, + layout: layout.into(), initial_data: data, meta: Default::default(), set_as_current: true, @@ -175,6 +221,7 @@ impl ViewTest { .async_send() .await .parse::(); + Self { sdk: sdk.clone(), workspace, @@ -183,15 +230,15 @@ impl ViewTest { } pub async fn new_grid_view(sdk: &EventIntegrationTest, data: Vec) -> Self { - Self::new(sdk, ViewLayoutPB::Grid, data).await + Self::new(sdk, ViewLayout::Grid, data).await } pub async fn new_board_view(sdk: &EventIntegrationTest, data: Vec) -> Self { - Self::new(sdk, ViewLayoutPB::Board, data).await + Self::new(sdk, ViewLayout::Board, data).await } pub async fn new_calendar_view(sdk: &EventIntegrationTest, data: Vec) -> Self { - Self::new(sdk, ViewLayoutPB::Calendar, data).await + Self::new(sdk, ViewLayout::Calendar, data).await } } diff --git a/frontend/rust-lib/event-integration/src/lib.rs b/frontend/rust-lib/event-integration/src/lib.rs index a91125ca54..f1d13e2e17 100644 --- a/frontend/rust-lib/event-integration/src/lib.rs +++ b/frontend/rust-lib/event-integration/src/lib.rs @@ -14,6 +14,7 @@ use tokio::select; use tokio::time::sleep; use flowy_core::config::AppFlowyCoreConfig; +use flowy_core::integrate::log::create_log_filter; use flowy_core::AppFlowyCore; use flowy_notification::register_notification_sender; use flowy_server::AppFlowyServer; @@ -54,15 +55,8 @@ impl EventIntegrationTest { let path = path_buf.to_str().unwrap().to_string(); let device_id = uuid::Uuid::new_v4().to_string(); - let config = AppFlowyCoreConfig::new("".to_string(), path.clone(), path, device_id, name) - .log_filter( - "trace", - vec![ - "flowy_test".to_string(), - "tokio".to_string(), - // "lib_dispatch".to_string(), - ], - ); + let config = AppFlowyCoreConfig::new(String::new(), path.clone(), path, device_id, name) + .log_filter(create_log_filter("trace".to_owned(), vec![])); let inner = init_core(config).await; let notification_sender = TestNotificationSender::new(); @@ -79,6 +73,14 @@ impl EventIntegrationTest { } } + pub fn instance_name(&self) -> String { + self.appflowy_core.config.name.clone() + } + + pub fn user_data_path(&self) -> String { + self.appflowy_core.config.application_path.clone() + } + pub fn get_server(&self) -> Arc { self.appflowy_core.server_provider.get_server().unwrap() } @@ -108,14 +110,14 @@ impl EventIntegrationTest { pub async fn get_collab_doc_state( &self, oid: &str, - collay_type: CollabType, + collab_type: CollabType, ) -> Result { let server = self.server_provider.get_server().unwrap(); let workspace_id = self.get_current_workspace().await.id; let uid = self.get_user_profile().await?.id; let doc_state = server .folder_service() - .get_folder_doc_state(&workspace_id, uid, collay_type, oid) + .get_folder_doc_state(&workspace_id, uid, collab_type, oid) .await?; Ok(doc_state) diff --git a/frontend/rust-lib/event-integration/src/user_event.rs b/frontend/rust-lib/event-integration/src/user_event.rs index 07c8560a09..05d09457ae 100644 --- a/frontend/rust-lib/event-integration/src/user_event.rs +++ b/frontend/rust-lib/event-integration/src/user_event.rs @@ -51,13 +51,14 @@ impl EventIntegrationTest { config.encrypt_secret } - pub async fn new_with_guest_user() -> Self { + /// Create a anonymous user for given test. + pub async fn new_anon() -> Self { let test = Self::new().await; - test.sign_up_as_guest().await; + test.sign_up_as_anon().await; test } - pub async fn sign_up_as_guest(&self) -> SignUpContext { + pub async fn sign_up_as_anon(&self) -> SignUpContext { let password = login_password(); let email = unique_email(); let payload = SignUpPayloadPB { @@ -116,7 +117,7 @@ impl EventIntegrationTest { } pub async fn init_anon_user(&self) -> UserProfilePB { - self.sign_up_as_guest().await.user_profile + self.sign_up_as_anon().await.user_profile } pub async fn get_user_profile(&self) -> Result { diff --git a/frontend/rust-lib/event-integration/tests/asset/folder_1000_view.zip b/frontend/rust-lib/event-integration/tests/asset/folder_1000_view.zip new file mode 100644 index 0000000000..febc1d87d9 Binary files /dev/null and b/frontend/rust-lib/event-integration/tests/asset/folder_1000_view.zip differ diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs index 556624e7ff..e59645d4cb 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs @@ -3,7 +3,7 @@ use event_integration::EventIntegrationTest; // The number of groups should be 0 if there is no group by field in grid #[tokio::test] async fn get_groups_event_with_grid_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -15,7 +15,7 @@ async fn get_groups_event_with_grid_test() { #[tokio::test] async fn get_groups_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -27,7 +27,7 @@ async fn get_groups_event_test() { #[tokio::test] async fn move_group_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -61,7 +61,7 @@ async fn move_group_event_test() { #[tokio::test] async fn move_group_event_with_invalid_id_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -83,7 +83,7 @@ async fn move_group_event_with_invalid_id_test() { #[tokio::test] async fn rename_group_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -104,7 +104,7 @@ async fn rename_group_event_test() { #[tokio::test] async fn hide_group_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -132,7 +132,7 @@ async fn hide_group_event_test() { #[tokio::test] async fn update_group_name_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -157,7 +157,7 @@ async fn update_group_name_test() { #[tokio::test] async fn delete_group_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs index 1c2edd339d..3d293d2283 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs @@ -13,7 +13,7 @@ use lib_infra::util::timestamp; #[tokio::test] async fn get_database_id_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -35,7 +35,7 @@ async fn get_database_id_event_test() { #[tokio::test] async fn get_database_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -49,7 +49,7 @@ async fn get_database_event_test() { #[tokio::test] async fn get_field_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -64,7 +64,7 @@ async fn get_field_event_test() { #[tokio::test] async fn create_field_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -78,7 +78,7 @@ async fn create_field_event_test() { #[tokio::test] async fn delete_field_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -99,7 +99,7 @@ async fn delete_field_event_test() { // The primary field is not allowed to be deleted. #[tokio::test] async fn delete_primary_field_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -114,7 +114,7 @@ async fn delete_primary_field_event_test() { #[tokio::test] async fn update_field_type_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -132,7 +132,7 @@ async fn update_field_type_event_test() { #[tokio::test] async fn update_primary_field_type_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -151,7 +151,7 @@ async fn update_primary_field_type_event_test() { #[tokio::test] async fn duplicate_field_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -169,7 +169,7 @@ async fn duplicate_field_event_test() { // The primary field is not allowed to be duplicated. So this test should return an error. #[tokio::test] async fn duplicate_primary_field_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -183,7 +183,7 @@ async fn duplicate_primary_field_test() { #[tokio::test] async fn get_primary_field_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -196,7 +196,7 @@ async fn get_primary_field_event_test() { #[tokio::test] async fn create_row_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -211,7 +211,7 @@ async fn create_row_event_test() { #[tokio::test] async fn delete_row_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -234,7 +234,7 @@ async fn delete_row_event_test() { #[tokio::test] async fn get_row_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -250,7 +250,7 @@ async fn get_row_event_test() { #[tokio::test] async fn update_row_meta_event_with_url_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -279,7 +279,7 @@ async fn update_row_meta_event_with_url_test() { #[tokio::test] async fn update_row_meta_event_with_cover_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -308,7 +308,7 @@ async fn update_row_meta_event_with_cover_test() { #[tokio::test] async fn delete_row_event_with_invalid_row_id_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -321,7 +321,7 @@ async fn delete_row_event_with_invalid_row_id_test() { #[tokio::test] async fn duplicate_row_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -338,7 +338,7 @@ async fn duplicate_row_event_test() { #[tokio::test] async fn duplicate_row_event_with_invalid_row_id_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -355,7 +355,7 @@ async fn duplicate_row_event_with_invalid_row_id_test() { #[tokio::test] async fn move_row_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -375,7 +375,7 @@ async fn move_row_event_test() { #[tokio::test] async fn move_row_event_test2() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -395,7 +395,7 @@ async fn move_row_event_test2() { #[tokio::test] async fn move_row_event_with_invalid_row_id_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -422,7 +422,7 @@ async fn move_row_event_with_invalid_row_id_test() { #[tokio::test] async fn update_text_cell_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -452,7 +452,7 @@ async fn update_text_cell_event_test() { #[tokio::test] async fn update_checkbox_cell_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -483,7 +483,7 @@ async fn update_checkbox_cell_event_test() { #[tokio::test] async fn update_single_select_cell_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -510,7 +510,7 @@ async fn update_single_select_cell_event_test() { #[tokio::test] async fn update_date_cell_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -547,7 +547,7 @@ async fn update_date_cell_event_test() { #[tokio::test] async fn update_date_cell_event_with_empty_time_str_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -583,7 +583,7 @@ async fn update_date_cell_event_with_empty_time_str_test() { #[tokio::test] async fn create_checklist_field_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -604,7 +604,7 @@ async fn create_checklist_field_test() { #[tokio::test] async fn update_checklist_cell_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -661,7 +661,7 @@ async fn update_checklist_cell_test() { // Update the database layout type from grid to board #[tokio::test] async fn update_database_layout_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -683,7 +683,7 @@ async fn update_database_layout_event_test() { // Update the database layout type from grid to board. Set the checkbox field as the grouping field #[tokio::test] async fn update_database_layout_event_test2() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -715,7 +715,7 @@ async fn update_database_layout_event_test2() { // Create a checkbox field in the default board and then set it as the grouping field. #[tokio::test] async fn set_group_by_checkbox_field_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -732,7 +732,7 @@ async fn set_group_by_checkbox_field_test() { #[tokio::test] async fn get_all_calendar_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let calendar_view = test .create_calendar(¤t_workspace.id, "my calendar view".to_owned(), vec![]) @@ -745,7 +745,7 @@ async fn get_all_calendar_event_test() { #[tokio::test] async fn create_calendar_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let calendar_view = test .create_calendar(¤t_workspace.id, "my calendar view".to_owned(), vec![]) @@ -781,7 +781,7 @@ async fn create_calendar_event_test() { #[tokio::test] async fn update_relation_cell_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -840,7 +840,7 @@ async fn update_relation_cell_test() { #[tokio::test] async fn get_detailed_relation_cell_data() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let origin_grid_view = test diff --git a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs index c0165bd8ca..9569fe163a 100644 --- a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs +++ b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs @@ -8,7 +8,7 @@ use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB}; -use crate::util::{receive_with_timeout, unzip_history_user_db}; +use crate::util::{receive_with_timeout, unzip}; #[tokio::test] async fn af_cloud_edit_document_test() { @@ -43,8 +43,7 @@ async fn af_cloud_edit_document_test() { #[tokio::test] async fn af_cloud_sync_anon_user_document_test() { - let (cleaner, user_db_path) = - unzip_history_user_db("./tests/asset", "040_sync_local_document").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "040_sync_local_document").unwrap(); user_localhost_af_cloud().await; let test = EventIntegrationTest::new_with_user_data_path(user_db_path.clone(), DEFAULT_NAME.to_string()) diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/import_test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/import_test.rs index 7b812fc821..e91465195d 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/import_test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/import_test.rs @@ -1,4 +1,4 @@ -use crate::util::unzip_history_user_db; +use crate::util::unzip; use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; use flowy_folder::entities::{ImportPB, ImportTypePB, ViewLayoutPB}; @@ -7,11 +7,11 @@ use flowy_folder::entities::{ImportPB, ImportTypePB, ViewLayoutPB}; async fn import_492_row_csv_file_test() { // csv_500r_15c.csv is a file with 492 rows and 17 columns let file_name = "csv_492r_17c.csv".to_string(); - let (cleaner, csv_file_path) = unzip_history_user_db("./tests/asset", &file_name).unwrap(); + let (cleaner, csv_file_path) = unzip("./tests/asset", &file_name).unwrap(); let csv_string = std::fs::read_to_string(csv_file_path).unwrap(); let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; - test.sign_up_as_guest().await; + test.sign_up_as_anon().await; let workspace_id = test.get_current_workspace().await.id; let import_data = gen_import_data(file_name, csv_string, workspace_id); @@ -26,11 +26,11 @@ async fn import_492_row_csv_file_test() { async fn import_10240_row_csv_file_test() { // csv_22577r_15c.csv is a file with 10240 rows and 15 columns let file_name = "csv_10240r_15c.csv".to_string(); - let (cleaner, csv_file_path) = unzip_history_user_db("./tests/asset", &file_name).unwrap(); + let (cleaner, csv_file_path) = unzip("./tests/asset", &file_name).unwrap(); let csv_string = std::fs::read_to_string(csv_file_path).unwrap(); let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; - test.sign_up_as_guest().await; + test.sign_up_as_anon().await; let workspace_id = test.get_current_workspace().await.id; let import_data = gen_import_data(file_name, csv_string, workspace_id); diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs index b2a1ee98d3..3023db1e9c 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs @@ -16,8 +16,6 @@ pub enum FolderScript { AssertWorkspace(WorkspacePB), #[allow(dead_code)] ReadWorkspace(String), - - // App CreateParentView { name: String, desc: String, @@ -81,16 +79,16 @@ impl FolderTest { let parent_view = create_view( &sdk, &workspace.id, - "Folder App", - "Folder test app", + "first level view", + "", ViewLayout::Document, ) .await; let view = create_view( &sdk, &parent_view.id, - "Folder View", - "Folder test view", + "second level view", + "", ViewLayout::Document, ) .await; diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs index 90af905806..93b8ca3d56 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/subscription_test.rs @@ -16,7 +16,7 @@ use crate::util::receive_with_timeout; /// 5. Await the notification for workspace view updates with a timeout of 30 seconds. /// 6. Ensure that the received views contain the newly created "test_view". async fn create_child_view_in_workspace_subscription_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let workspace = test.get_current_workspace().await; let rx = test .notification_sender @@ -40,7 +40,7 @@ async fn create_child_view_in_workspace_subscription_test() { #[tokio::test] async fn create_child_view_in_view_subscription_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let mut workspace = test.get_current_workspace().await; let workspace_child_view = workspace.views.pop().unwrap(); let rx = test.notification_sender.subscribe::( @@ -72,7 +72,7 @@ async fn create_child_view_in_view_subscription_test() { #[tokio::test] async fn delete_view_subscription_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let workspace = test.get_current_workspace().await; let rx = test .notification_sender @@ -103,7 +103,7 @@ async fn delete_view_subscription_test() { #[tokio::test] async fn update_view_subscription_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let mut workspace = test.get_current_workspace().await; let rx = test .notification_sender diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs index 8e60baef3a..277c8eea2e 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs @@ -6,7 +6,7 @@ use flowy_user::errors::ErrorCode; #[tokio::test] async fn create_workspace_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let request = CreateWorkspacePayloadPB { name: "my second workspace".to_owned(), desc: "".to_owned(), @@ -53,7 +53,7 @@ async fn create_workspace_event_test() { #[tokio::test] async fn create_view_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -65,7 +65,7 @@ async fn create_view_event_test() { #[tokio::test] async fn update_view_event_with_name_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -86,7 +86,7 @@ async fn update_view_event_with_name_test() { #[tokio::test] async fn update_view_icon_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -110,7 +110,7 @@ async fn update_view_icon_event_test() { #[tokio::test] async fn delete_view_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -133,7 +133,7 @@ async fn delete_view_event_test() { #[tokio::test] async fn put_back_trash_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -176,7 +176,7 @@ async fn put_back_trash_event_test() { #[tokio::test] async fn delete_view_permanently_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -225,7 +225,7 @@ async fn delete_view_permanently_event_test() { #[tokio::test] async fn delete_all_trash_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; for i in 0..3 { @@ -269,7 +269,7 @@ async fn delete_all_trash_test() { #[tokio::test] async fn multiple_hierarchy_view_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; for i in 1..4 { let parent = test @@ -345,7 +345,7 @@ async fn multiple_hierarchy_view_test() { #[tokio::test] async fn move_view_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; for i in 1..4 { let parent = test @@ -383,7 +383,7 @@ async fn move_view_event_test() { #[tokio::test] async fn move_view_event_after_delete_view_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; for i in 1..6 { let _ = test @@ -425,7 +425,7 @@ async fn move_view_event_after_delete_view_test() { #[tokio::test] async fn move_view_event_after_delete_view_test2() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let parent = test .create_view(¤t_workspace.id, "My view".to_string()) @@ -495,7 +495,7 @@ fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> { #[tokio::test] async fn move_view_across_parent_test() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let current_workspace = test.get_current_workspace().await; let parent_1 = test .create_view(¤t_workspace.id, "My view 1".to_string()) diff --git a/frontend/rust-lib/event-integration/tests/main.rs b/frontend/rust-lib/event-integration/tests/main.rs index 91d1d2a44f..1edfc1548c 100644 --- a/frontend/rust-lib/event-integration/tests/main.rs +++ b/frontend/rust-lib/event-integration/tests/main.rs @@ -3,3 +3,5 @@ mod document; mod folder; mod user; pub mod util; + +mod search; diff --git a/frontend/rust-lib/event-integration/tests/search/local_test/folder_search_test.rs b/frontend/rust-lib/event-integration/tests/search/local_test/folder_search_test.rs new file mode 100644 index 0000000000..4c9600a8cb --- /dev/null +++ b/frontend/rust-lib/event-integration/tests/search/local_test/folder_search_test.rs @@ -0,0 +1,215 @@ +use crate::util::{unzip_test_asset, zip}; +use collab_folder::View; +use event_integration::EventIntegrationTest; +use flowy_core::DEFAULT_NAME; +use flowy_folder::entities::UpdateViewPayloadPB; +use flowy_folder_pub::folder_builder::{FlattedViews, WorkspaceViewBuilder}; +use std::time::Duration; +use tokio::time::sleep; + +#[tokio::test] +async fn test_folder_index_all_startup() { + let folder_name = "folder_1000_view"; + // comment out the following line to create a test asset if you modify the test data + // don't forget to delete unnecessary test assets + // create_folder_test_data(folder_name).await; + + let (cleaner, user_db_path) = unzip_test_asset(folder_name).unwrap(); + let test = + EventIntegrationTest::new_with_user_data_path(user_db_path.clone(), DEFAULT_NAME.to_string()) + .await; + + let first_level_views = test.get_all_workspace_views().await; + assert_eq!(first_level_views.len(), 3); + assert_eq!(first_level_views[1].name, "1"); + assert_eq!(first_level_views[2].name, "2"); + + let view_1 = test.get_view(&first_level_views[1].id).await; + assert_eq!(view_1.child_views.len(), 500); + + let folder_data = test.get_folder_data(); + // Get started + 1002 Views + assert_eq!(folder_data.views.len(), 1003); + + // Wait for the index to be created/updated + sleep(Duration::from_secs(1)).await; + + let folder_search_manager = test.get_folder_search_handler(); + let num_docs = folder_search_manager.index_count(); + assert_eq!(num_docs, 1004); + + drop(cleaner); +} + +#[tokio::test] +async fn test_folder_index_create_20_views() { + let test = EventIntegrationTest::new_anon().await; + let folder_search_manager = test.get_folder_search_handler(); + + // Wait for the index to be created/updated + sleep(Duration::from_secs(1)).await; + let workspace_id = test.get_current_workspace().await.id; + + for i in 0..20 { + let view = test.create_view(&workspace_id, format!("View {}", i)).await; + sleep(Duration::from_millis(500)).await; + assert_eq!(view.name, format!("View {}", i)); + } + + // Wait for the index update to finish + sleep(Duration::from_secs(2)).await; + + let num_docs = folder_search_manager.index_count(); + // Workspace + Get started + 20 Views + assert_eq!(num_docs, 22); +} + +#[tokio::test] +async fn test_folder_index_create_view() { + let test = EventIntegrationTest::new_anon().await; + + let folder_search_manager = test.get_folder_search_handler(); + + // Wait for the index to be created/updated + sleep(Duration::from_secs(1)).await; + + let workspace_id = test.get_current_workspace().await.id; + let view = test.create_view(&workspace_id, "Flowers".to_owned()).await; + + // Wait for the index to be updated + sleep(Duration::from_millis(500)).await; + + let results = folder_search_manager.perform_search(view.name.clone()); + if let Err(e) = results { + panic!("Error performing search: {:?}", e); + } + + let results = results.unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].data, view.name); +} + +#[tokio::test] +async fn test_folder_index_rename_view() { + let test = EventIntegrationTest::new_anon().await; + let folder_search_manager = test.get_folder_search_handler(); + + // Wait for the index to be created/updated + sleep(Duration::from_secs(1)).await; + + let workspace_id = test.get_current_workspace().await.id; + let view = test.create_view(&workspace_id, "Flowers".to_owned()).await; + + // Wait for the index to be updated + sleep(Duration::from_millis(500)).await; + + let new_view_name = "Bouquets".to_string(); + let update_payload = UpdateViewPayloadPB { + view_id: view.id, + name: Some(new_view_name.clone()), + ..Default::default() + }; + test.update_view(update_payload).await; + + // Wait for the index to be updated + sleep(Duration::from_millis(500)).await; + + let first = folder_search_manager.perform_search(view.name); + if let Err(e) = first { + panic!("Error performing search: {:?}", e); + } + + let second = folder_search_manager.perform_search(new_view_name.clone()); + if let Err(e) = second { + panic!("Error performing search: {:?}", e); + } + + let first = first.unwrap(); + assert_eq!(first.len(), 0); + + let second = second.unwrap(); + assert_eq!(second.len(), 1); + assert_eq!(second[0].data, new_view_name); +} + +/// Using this method to create a folder test asset. Only use when you want to create a new asset. +/// The file will be created at tests/asset/{file_name}.zip and it will be committed to the repo. +/// +#[allow(dead_code)] +async fn create_folder_test_data(file_name: &str) { + let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; + test.sign_up_as_anon().await; + + let uid = test.get_user_profile().await.unwrap().id; + let workspace_id = test.get_current_workspace().await.id; + let views = create_1002_views(uid, workspace_id.clone()).await; + test.create_views(views).await; + + let first_level_views = test.get_all_workspace_views().await; + assert_eq!(first_level_views.len(), 3); + assert_eq!(first_level_views[1].name, "1"); + assert_eq!(first_level_views[2].name, "2"); + + let view_1 = test.get_view(&first_level_views[1].id).await; + assert_eq!(view_1.child_views.len(), 500); + + let folder_data = test.get_folder_data(); + // Get started + 1002 Views + assert_eq!(folder_data.views.len(), 1003); + + let data_path = test.config.application_path.clone(); + zip( + data_path.into(), + format!("tests/asset/{}.zip", file_name).into(), + ) + .unwrap(); + sleep(Duration::from_secs(2)).await; +} + +/// Create view without create the view's content(document/database). +/// workspace +/// - get_started +/// - view_1 +/// - view_1_1 +/// - view_1_2 +/// - view_2 +/// - view_2_1 +/// - view_2_2 +async fn create_1002_views(uid: i64, workspace_id: String) -> Vec { + let mut builder = WorkspaceViewBuilder::new(workspace_id.clone(), uid); + builder + .with_view_builder(|view_builder| async { + let mut builder = view_builder.with_name("1"); + for i in 0..500 { + builder = builder + .with_child_view_builder(|child_view_builder| async { + child_view_builder.with_name(format!("1_{}", i)).build() + }) + .await; + } + builder.build() + }) + .await; + builder + .with_view_builder(|view_builder| async { + let mut builder = view_builder.with_name("2"); + for i in 0..500 { + builder = builder + .with_child_view_builder(|child_view_builder| async { + child_view_builder.with_name(format!("2_{}", i)).build() + }) + .await; + } + builder.build() + }) + .await; + // The output views should be: + // view_1 + // view_1_1 + // view_1_x + // view_2 + // view_2_1 + // view_2_x + let views = builder.build(); + FlattedViews::flatten_views(views) +} diff --git a/frontend/rust-lib/event-integration/tests/search/local_test/mod.rs b/frontend/rust-lib/event-integration/tests/search/local_test/mod.rs new file mode 100644 index 0000000000..be0f274302 --- /dev/null +++ b/frontend/rust-lib/event-integration/tests/search/local_test/mod.rs @@ -0,0 +1 @@ +mod folder_search_test; diff --git a/frontend/rust-lib/event-integration/tests/search/mod.rs b/frontend/rust-lib/event-integration/tests/search/mod.rs new file mode 100644 index 0000000000..a8bc6de63f --- /dev/null +++ b/frontend/rust-lib/event-integration/tests/search/mod.rs @@ -0,0 +1 @@ +mod local_test; diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/anon_user_test.rs index b60248d5ef..eb696538b7 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/anon_user_test.rs @@ -3,11 +3,11 @@ use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; use flowy_user::entities::AuthenticatorPB; -use crate::util::unzip_history_user_db; +use crate::util::unzip; #[tokio::test] async fn reading_039_anon_user_data_test() { - let (cleaner, user_db_path) = unzip_history_user_db("./tests/asset", "039_local").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "039_local").unwrap(); let test = EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; let first_level_views = test.get_all_workspace_views().await; @@ -42,7 +42,7 @@ async fn reading_039_anon_user_data_test() { #[tokio::test] async fn migrate_anon_user_data_to_af_cloud_test() { - let (cleaner, user_db_path) = unzip_history_user_db("./tests/asset", "040_local").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "040_local").unwrap(); // In the 040_local, the structure is: // workspace: // view: Document1 diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/import_af_data_folder_test.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/import_af_data_folder_test.rs index b61c872658..20e4b16d99 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/import_af_data_folder_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/import_af_data_folder_test.rs @@ -1,4 +1,4 @@ -use crate::util::unzip_history_user_db; +use crate::util::unzip; use assert_json_diff::assert_json_include; use collab_database::rows::database_row_document_id_from_row_id; use collab_entity::CollabType; @@ -13,8 +13,7 @@ use std::env::temp_dir; async fn import_appflowy_data_need_migration_test() { // In 037, the workspace array will be migrated to view. let import_container_name = "037_local".to_string(); - let (cleaner, user_db_path) = - unzip_history_user_db("./tests/asset", &import_container_name).unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); // Getting started // Document1 // Document2(fav) @@ -53,8 +52,7 @@ async fn import_appflowy_data_need_migration_test() { #[tokio::test] async fn import_appflowy_data_folder_into_new_view_test() { let import_container_name = "040_local".to_string(); - let (cleaner, user_db_path) = - unzip_history_user_db("./tests/asset", &import_container_name).unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); // In the 040_local, the structure is: // workspace: // view: Document1 @@ -122,8 +120,7 @@ async fn import_appflowy_data_folder_into_new_view_test() { #[tokio::test] async fn import_appflowy_data_folder_into_current_workspace_test() { let import_container_name = "040_local".to_string(); - let (cleaner, user_db_path) = - unzip_history_user_db("./tests/asset", &import_container_name).unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); // In the 040_local, the structure is: // workspace: // view: Document1 @@ -170,8 +167,7 @@ async fn import_appflowy_data_folder_into_current_workspace_test() { #[tokio::test] async fn import_appflowy_data_folder_into_new_view_test2() { let import_container_name = "040_local_2".to_string(); - let (cleaner, user_db_path) = - unzip_history_user_db("./tests/asset", &import_container_name).unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); user_localhost_af_cloud().await; let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; let _ = test.af_cloud_sign_up().await; @@ -210,8 +206,7 @@ async fn import_empty_appflowy_data_folder_test() { #[tokio::test] async fn import_appflowy_data_folder_multiple_times_test() { let import_container_name = "040_local_2".to_string(); - let (cleaner, user_db_path) = - unzip_history_user_db("./tests/asset", &import_container_name).unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); // In the 040_local_2, the structure is: // Getting Started // Doc1 diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/import_af_data_local_test.rs b/frontend/rust-lib/event-integration/tests/user/local_test/import_af_data_local_test.rs index 0c801c77be..82cded674f 100644 --- a/frontend/rust-lib/event-integration/tests/user/local_test/import_af_data_local_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/local_test/import_af_data_local_test.rs @@ -1,4 +1,4 @@ -use crate::util::unzip_history_user_db; +use crate::util::unzip; use event_integration::user_event::user_localhost_af_cloud; use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; @@ -7,10 +7,9 @@ use std::time::Duration; #[tokio::test] async fn import_appflowy_data_folder_into_new_view_test() { let import_container_name = "040_local".to_string(); - let (cleaner, user_db_path) = - unzip_history_user_db("./tests/asset", &import_container_name).unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); let (imported_af_folder_cleaner, imported_af_data_path) = - unzip_history_user_db("./tests/asset", &import_container_name).unwrap(); + unzip("./tests/asset", &import_container_name).unwrap(); user_localhost_af_cloud().await; let test = diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs b/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs index 8e1223f566..408af350f6 100644 --- a/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs @@ -8,7 +8,7 @@ use flowy_user::event_map::UserEvent::*; #[tokio::test] async fn user_update_with_reminder() { let sdk = EventIntegrationTest::new().await; - let _ = sdk.sign_up_as_guest().await; + let _ = sdk.sign_up_as_anon().await; let mut meta = HashMap::new(); meta.insert("object_id".to_string(), "".to_string()); diff --git a/frontend/rust-lib/event-integration/tests/user/migration_test/collab_db_restore.rs b/frontend/rust-lib/event-integration/tests/user/migration_test/collab_db_restore.rs index 363cce2af2..7a395a3601 100644 --- a/frontend/rust-lib/event-integration/tests/user/migration_test/collab_db_restore.rs +++ b/frontend/rust-lib/event-integration/tests/user/migration_test/collab_db_restore.rs @@ -1,11 +1,11 @@ use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; -use crate::util::unzip_history_user_db; +use crate::util::unzip; #[tokio::test] async fn collab_db_restore_test() { - let (cleaner, user_db_path) = unzip_history_user_db( + let (cleaner, user_db_path) = unzip( "./tests/user/migration_test/history_user_db", "038_collab_db_corrupt_restore", ) diff --git a/frontend/rust-lib/event-integration/tests/user/migration_test/document_test.rs b/frontend/rust-lib/event-integration/tests/user/migration_test/document_test.rs index 0d1aadc682..62cc556ee2 100644 --- a/frontend/rust-lib/event-integration/tests/user/migration_test/document_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/migration_test/document_test.rs @@ -2,11 +2,11 @@ use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; use flowy_folder::entities::ViewLayoutPB; -use crate::util::unzip_history_user_db; +use crate::util::unzip; #[tokio::test] async fn migrate_historical_empty_document_test() { - let (cleaner, user_db_path) = unzip_history_user_db( + let (cleaner, user_db_path) = unzip( "./tests/user/migration_test/history_user_db", "historical_empty_document", ) diff --git a/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs b/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs index 88ea6ac1ba..c58dbf8e74 100644 --- a/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/migration_test/version_test.rs @@ -3,11 +3,11 @@ use flowy_core::DEFAULT_NAME; use flowy_folder::entities::ViewLayoutPB; use std::time::Duration; -use crate::util::unzip_history_user_db; +use crate::util::unzip; #[tokio::test] async fn migrate_020_historical_empty_document_test() { - let (cleaner, user_db_path) = unzip_history_user_db( + let (cleaner, user_db_path) = unzip( "./tests/user/migration_test/history_user_db", "020_historical_user_data", ) @@ -43,7 +43,7 @@ async fn migrate_020_historical_empty_document_test() { #[tokio::test] async fn migrate_036_fav_v1_workspace_array_test() { // Used to test migration: FavoriteV1AndWorkspaceArrayMigration - let (cleaner, user_db_path) = unzip_history_user_db( + let (cleaner, user_db_path) = unzip( "./tests/user/migration_test/history_user_db", "036_fav_v1_workspace_array", ) @@ -65,7 +65,7 @@ async fn migrate_036_fav_v1_workspace_array_test() { #[tokio::test] async fn migrate_038_trash_test() { // Used to test migration: WorkspaceTrashMapToSectionMigration - let (cleaner, user_db_path) = unzip_history_user_db("./tests/asset", "038_local").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "038_local").unwrap(); // Getting started // Document1 // Document2(deleted) @@ -102,8 +102,7 @@ async fn migrate_038_trash_test() { #[tokio::test] async fn migrate_038_trash_test2() { // Used to test migration: WorkspaceTrashMapToSectionMigration - let (cleaner, user_db_path) = - unzip_history_user_db("./tests/asset", "038_document_with_grid").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "038_document_with_grid").unwrap(); // Getting started // document // grid @@ -131,7 +130,7 @@ async fn migrate_038_trash_test2() { #[tokio::test] async fn collab_db_backup_test() { // Used to test migration: WorkspaceTrashMapToSectionMigration - let (cleaner, user_db_path) = unzip_history_user_db("./tests/asset", "038_local").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "038_local").unwrap(); let test = EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; @@ -149,8 +148,7 @@ async fn collab_db_backup_test() { #[tokio::test] async fn delete_outdated_collab_db_backup_test() { // Used to test migration: WorkspaceTrashMapToSectionMigration - let (cleaner, user_db_path) = - unzip_history_user_db("./tests/asset", "040_collab_backups").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "040_collab_backups").unwrap(); let test = EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; diff --git a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs index f42671cb1c..26bd586456 100644 --- a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs @@ -120,7 +120,7 @@ async fn third_party_sign_up_with_duplicated_email() { #[tokio::test] async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() { if get_supabase_config().is_some() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let old_views = test .folder_manager .get_current_workspace_views() @@ -151,7 +151,7 @@ async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() { #[tokio::test] async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() { if get_supabase_config().is_some() { - let test = EventIntegrationTest::new_with_guest_user().await; + let test = EventIntegrationTest::new_anon().await; let uuid = uuid::Uuid::new_v4().to_string(); let email = format!("{}@appflowy.io", nanoid!(6)); @@ -172,7 +172,7 @@ async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() { // sign out and then sign in as a guest test.sign_out().await; - let _sign_up_context = test.sign_up_as_guest().await; + let _sign_up_context = test.sign_up_as_anon().await; let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); test .create_view(&new_workspace.id, "new workspace child view".to_string()) @@ -253,7 +253,7 @@ async fn update_user_profile_with_existing_email_test() { async fn migrate_anon_document_on_cloud_signup() { if get_supabase_config().is_some() { let test = EventIntegrationTest::new().await; - let user_profile = test.sign_up_as_guest().await.user_profile; + let user_profile = test.sign_up_as_anon().await.user_profile; let view = test .create_view(&user_profile.workspace_id, "My first view".to_string()) @@ -292,7 +292,7 @@ async fn migrate_anon_document_on_cloud_signup() { #[tokio::test] async fn migrate_anon_data_on_cloud_signup() { if get_supabase_config().is_some() { - let (cleaner, user_db_path) = unzip_history_user_db( + let (cleaner, user_db_path) = unzip( "./tests/user/supabase_test/history_user_db", "workspace_sync", ) diff --git a/frontend/rust-lib/event-integration/tests/util.rs b/frontend/rust-lib/event-integration/tests/util.rs index 1eac411d23..5983043ff6 100644 --- a/frontend/rust-lib/event-integration/tests/util.rs +++ b/frontend/rust-lib/event-integration/tests/util.rs @@ -1,9 +1,10 @@ -use std::fs::{create_dir_all, File}; +use std::fs::{create_dir_all, File, OpenOptions}; use std::io::copy; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; +use std::{fs, io}; use anyhow::Error; use collab_folder::FolderData; @@ -13,7 +14,9 @@ use tokio::sync::mpsc::Receiver; use tokio::time::timeout; use uuid::Uuid; -use zip::ZipArchive; +use walkdir::WalkDir; +use zip::write::FileOptions; +use zip::{CompressionMethod, ZipArchive, ZipWriter}; use event_integration::event_builder::EventBuilder; use event_integration::Cleaner; @@ -163,7 +166,78 @@ pub fn appflowy_server( (SupabaseServerServiceImpl::new(server), encryption_impl) } -pub fn unzip_history_user_db(root: &str, folder_name: &str) -> std::io::Result<(Cleaner, PathBuf)> { +/// zip the asset to the destination +/// Zips the specified directory into a zip file. +/// +/// # Arguments +/// - `src_dir`: Path to the directory to zip. +/// - `output_file`: Path to the output zip file. +/// +/// # Errors +/// Returns `io::Result<()>` indicating the operation's success or failure. +pub fn zip(src_dir: PathBuf, output_file_path: PathBuf) -> io::Result<()> { + // Ensure the output directory exists + if let Some(parent) = output_file_path.parent() { + if !parent.exists() { + fs::create_dir_all(parent)?; + } + } + + // Open or create the output file, truncating it if it exists + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&output_file_path)?; + + let options = FileOptions::default().compression_method(CompressionMethod::Deflated); + + let mut zip = ZipWriter::new(file); + + // Calculate the name of the new folder within the ZIP file based on the last component of the output path + let new_folder_name = output_file_path + .file_stem() + .and_then(|name| name.to_str()) + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid output file name"))?; + + let src_dir_str = src_dir.to_str().expect("Invalid source directory path"); + + for entry in WalkDir::new(&src_dir).into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + let relative_path = path + .strip_prefix(src_dir_str) + .map_err(|_| io::Error::new(io::ErrorKind::Other, "Error calculating relative path"))?; + + // Construct the path within the ZIP, prefixing with the new folder's name + let zip_path = Path::new(new_folder_name).join(relative_path); + + if path.is_file() { + zip.start_file( + zip_path + .to_str() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid file name"))?, + options, + )?; + + let mut f = File::open(path)?; + io::copy(&mut f, &mut zip)?; + } else if entry.file_type().is_dir() && !relative_path.as_os_str().is_empty() { + zip.add_directory( + zip_path + .to_str() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid directory name"))?, + options, + )?; + } + } + zip.finish()?; + Ok(()) +} +pub fn unzip_test_asset(folder_name: &str) -> io::Result<(Cleaner, PathBuf)> { + unzip("./tests/asset", folder_name) +} + +pub fn unzip(root: &str, folder_name: &str) -> io::Result<(Cleaner, PathBuf)> { // Open the zip file let zip_file_path = format!("{}/{}.zip", root, folder_name); let reader = File::open(zip_file_path)?; diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 1798e1fefb..12a4829a29 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -23,6 +23,7 @@ flowy-server-pub = { workspace = true } flowy-config = { workspace = true } flowy-date = { workspace = true } collab-integrate = { workspace = true } +flowy-search = { workspace = true } collab-entity = { version = "0.1.0" } collab-plugins = { version = "0.1.0" } collab = { version = "0.1.0" } @@ -35,7 +36,7 @@ tracing.workspace = true futures-core = { version = "0.3", default-features = false } bytes.workspace = true tokio = { workspace = true, features = ["full"] } -tokio-stream = { workspace = true, features = ["sync"]} +tokio-stream = { workspace = true, features = ["sync"] } console-subscriber = { version = "0.2", optional = true } parking_lot.workspace = true anyhow.workspace = true @@ -56,14 +57,18 @@ http_sync = [] native_sync = [] use_bunyan = ["lib-log/use_bunyan"] dart = [ + "flowy-user/dart", + "flowy-date/dart", + "flowy-search/dart", "flowy-folder/dart", "flowy-database2/dart", ] ts = [ "flowy-user/tauri_ts", "flowy-folder/tauri_ts", + "flowy-search/tauri_ts", "flowy-database2/ts", "flowy-config/tauri_ts", ] rev-sqlite = ["flowy-user/rev-sqlite"] -openssl_vendored = ["flowy-sqlite/openssl_vendored"] \ No newline at end of file +openssl_vendored = ["flowy-sqlite/openssl_vendored"] diff --git a/frontend/rust-lib/flowy-core/src/config.rs b/frontend/rust-lib/flowy-core/src/config.rs index 9501b05716..2d6a604bc7 100644 --- a/frontend/rust-lib/flowy-core/src/config.rs +++ b/frontend/rust-lib/flowy-core/src/config.rs @@ -15,7 +15,7 @@ use crate::integrate::log::create_log_filter; pub struct AppFlowyCoreConfig { /// Different `AppFlowyCoreConfig` instance should have different name pub(crate) app_version: String, - pub(crate) name: String, + pub name: String, pub(crate) device_id: String, /// Used to store the user data pub storage_path: String, @@ -102,8 +102,8 @@ impl AppFlowyCoreConfig { } } - pub fn log_filter(mut self, level: &str, with_crates: Vec) -> Self { - self.log_filter = create_log_filter(level.to_owned(), with_crates); + pub fn log_filter(mut self, log_filter: String) -> Self { + self.log_filter = log_filter; self } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index d422478923..1c2642e633 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -1,4 +1,7 @@ use bytes::Bytes; + +use tokio::sync::RwLock; + use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; use flowy_database2::entities::DatabaseLayoutPB; @@ -14,10 +17,10 @@ use flowy_folder::manager::{FolderManager, FolderUser}; use flowy_folder::share::ImportType; use flowy_folder::view_operation::{FolderOperationHandler, FolderOperationHandlers, View}; use flowy_folder::ViewLayout; +use flowy_search::folder::indexer::FolderIndexManagerImpl; use std::collections::HashMap; use std::convert::TryFrom; use std::sync::{Arc, Weak}; -use tokio::sync::RwLock; use flowy_folder_pub::folder_builder::WorkspaceViewBuilder; use flowy_user::services::authenticate_user::AuthenticateUser; @@ -35,6 +38,7 @@ impl FolderDepsResolver { database_manager: &Arc, collab_builder: Arc, server_provider: Arc, + folder_indexer: Arc, ) -> Arc { let user: Arc = Arc::new(FolderUserImpl { authenticate_user: authenticate_user.clone(), @@ -47,6 +51,7 @@ impl FolderDepsResolver { collab_builder, handlers, server_provider.clone(), + folder_indexer, ) .await .unwrap(), diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs index e2ca46b639..a93530e519 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs @@ -2,12 +2,13 @@ pub use collab_deps::*; pub use database_deps::*; pub use document_deps::*; pub use folder_deps::*; +pub use search_deps::*; pub use user_deps::*; mod collab_deps; mod document_deps; mod folder_deps; -mod util; mod database_deps; +mod search_deps; mod user_deps; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs new file mode 100644 index 0000000000..23e6af0b51 --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs @@ -0,0 +1,12 @@ +use flowy_search::folder::handler::FolderSearchHandler; +use flowy_search::folder::indexer::FolderIndexManagerImpl; +use flowy_search::services::manager::SearchManager; +use std::sync::Arc; + +pub struct SearchDepsResolver(); +impl SearchDepsResolver { + pub async fn resolve(folder_indexer: Arc) -> Arc { + let folder_handler = Arc::new(FolderSearchHandler::new(folder_indexer)); + Arc::new(SearchManager::new(vec![folder_handler])) + } +} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/util.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/util.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/util.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/rust-lib/flowy-core/src/integrate/log.rs b/frontend/rust-lib/flowy-core/src/integrate/log.rs index 7a66353275..c351b394e7 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/log.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/log.rs @@ -12,7 +12,7 @@ pub(crate) fn init_log(config: &AppFlowyCoreConfig) { .build(); } } -pub(crate) fn create_log_filter(level: String, with_crates: Vec) -> String { +pub fn create_log_filter(level: String, with_crates: Vec) -> String { let level = std::env::var("RUST_LOG").unwrap_or(level); let mut filters = with_crates .into_iter() @@ -32,9 +32,13 @@ pub(crate) fn create_log_filter(level: String, with_crates: Vec) -> Stri filters.push(format!("flowy_server={}", level)); filters.push(format!("flowy_notification={}", "info")); filters.push(format!("lib_infra={}", level)); - // filters.push(format!("lib_dispatch={}", level)); + filters.push(format!("flowy_search={}", level)); + + // Most of the time, we don't need to see the logs from the following crates + // unless we are debugging the ffi or event dispatching + // filters.push(format!("lib_dispatch={}", level)); + // filters.push(format!("dart_ffi={}", level)); - filters.push(format!("dart_ffi={}", "info")); filters.push(format!("flowy_sqlite={}", "info")); filters.push(format!("client_api={}", level)); #[cfg(feature = "profiling")] diff --git a/frontend/rust-lib/flowy-core/src/integrate/mod.rs b/frontend/rust-lib/flowy-core/src/integrate/mod.rs index 7484472f5a..129a22a99f 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/mod.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/mod.rs @@ -1,5 +1,5 @@ pub(crate) mod collab_interact; -pub(crate) mod log; +pub mod log; pub(crate) mod server; mod trait_impls; pub(crate) mod user; diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index c1e2fbcb82..c49c3f6d73 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,5 +1,7 @@ #![allow(unused_doc_comments)] +use flowy_search::folder::indexer::FolderIndexManagerImpl; +use flowy_search::services::manager::SearchManager; use flowy_storage::ObjectStorageService; use std::sync::Arc; use std::time::Duration; @@ -11,6 +13,7 @@ use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabPluginProvid use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; use flowy_folder::manager::FolderManager; + use flowy_sqlite::kv::StorePreferences; use flowy_user::services::authenticate_user::AuthenticateUser; use flowy_user::services::entities::UserConfig; @@ -30,7 +33,7 @@ use crate::integrate::user::UserStatusCallbackImpl; pub mod config; mod deps_resolve; -mod integrate; +pub mod integrate; pub mod module; /// This name will be used as to identify the current [AppFlowyCore] instance. @@ -49,6 +52,7 @@ pub struct AppFlowyCore { pub server_provider: Arc, pub task_dispatcher: Arc>, pub store_preference: Arc, + pub search_manager: Arc, } impl AppFlowyCore { @@ -102,6 +106,7 @@ impl AppFlowyCore { database_manager, document_manager, collab_builder, + search_manager, ) = async { /// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded /// on demand based on the [CollabPluginConfig]. @@ -141,17 +146,21 @@ impl AppFlowyCore { Arc::downgrade(&(server_provider.clone() as Arc)), ); + let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Arc::downgrade( + &authenticate_user, + ))); let folder_manager = FolderDepsResolver::resolve( Arc::downgrade(&authenticate_user), &document_manager, &database_manager, collab_builder.clone(), server_provider.clone(), + folder_indexer.clone(), ) .await; let user_manager = UserDepsResolver::resolve( - authenticate_user, + authenticate_user.clone(), collab_builder.clone(), server_provider.clone(), store_preference.clone(), @@ -160,6 +169,8 @@ impl AppFlowyCore { ) .await; + let search_manager = SearchDepsResolver::resolve(folder_indexer).await; + ( user_manager, folder_manager, @@ -167,6 +178,7 @@ impl AppFlowyCore { database_manager, document_manager, collab_builder, + search_manager, ) } .await; @@ -201,6 +213,7 @@ impl AppFlowyCore { Arc::downgrade(&database_manager), Arc::downgrade(&user_manager), Arc::downgrade(&document_manager), + Arc::downgrade(&search_manager), ), )); @@ -214,6 +227,7 @@ impl AppFlowyCore { server_provider, task_dispatcher, store_preference, + search_manager, } } diff --git a/frontend/rust-lib/flowy-core/src/module.rs b/frontend/rust-lib/flowy-core/src/module.rs index d76262a054..8d021955ef 100644 --- a/frontend/rust-lib/flowy-core/src/module.rs +++ b/frontend/rust-lib/flowy-core/src/module.rs @@ -3,6 +3,7 @@ use std::sync::Weak; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager as DocumentManager2; use flowy_folder::manager::FolderManager; +use flowy_search::services::manager::SearchManager; use flowy_user::user_manager::UserManager; use lib_dispatch::prelude::AFPlugin; @@ -11,6 +12,7 @@ pub fn make_plugins( database_manager: Weak, user_session: Weak, document_manager2: Weak, + search_manager: Weak, ) -> Vec { let store_preferences = user_session .upgrade() @@ -22,6 +24,7 @@ pub fn make_plugins( let document_plugin2 = flowy_document::event_map::init(document_manager2); let config_plugin = flowy_config::event_map::init(store_preferences); let date_plugin = flowy_date::event_map::init(); + let search_plugin = flowy_search::event_map::init(search_manager); vec![ user_plugin, folder_plugin, @@ -29,5 +32,6 @@ pub fn make_plugins( document_plugin2, config_plugin, date_plugin, + search_plugin, ] } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index 6134b7d265..980fee21b2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -162,7 +162,7 @@ where #[tracing::instrument(level = "trace", skip(self))] pub(crate) fn delete_group(&mut self, deleted_group_id: &str) -> FlowyResult<()> { - self.group_by_id.remove(deleted_group_id); + self.group_by_id.shift_remove(deleted_group_id); self.mut_configuration(|configuration| { configuration .groups diff --git a/frontend/rust-lib/flowy-date/Cargo.toml b/frontend/rust-lib/flowy-date/Cargo.toml index 936199b6c2..40015cad77 100644 --- a/frontend/rust-lib/flowy-date/Cargo.toml +++ b/frontend/rust-lib/flowy-date/Cargo.toml @@ -19,9 +19,9 @@ date_time_parser = { version = "0.2.0" } chrono.workspace = true fancy-regex = { version = "0.11.0" } +[build-dependencies] +flowy-codegen.workspace = true + [features] dart = ["flowy-codegen/dart"] tauri_ts = ["flowy-codegen/ts"] - -[build-dependencies] -flowy-codegen.workspace = true diff --git a/frontend/rust-lib/flowy-document/src/document.rs b/frontend/rust-lib/flowy-document/src/document.rs index 928ebebea5..cc33921226 100644 --- a/frontend/rust-lib/flowy-document/src/document.rs +++ b/frontend/rust-lib/flowy-document/src/document.rs @@ -4,7 +4,10 @@ use std::{ }; use collab::core::collab::MutexCollab; -use collab_document::{blocks::DocumentData, document::Document}; +use collab_document::{ + blocks::DocumentData, + document::{Document, DocumentIndexContent}, +}; use futures::StreamExt; use parking_lot::Mutex; @@ -109,3 +112,10 @@ impl DerefMut for MutexDocument { &mut self.0 } } + +impl From<&MutexDocument> for DocumentIndexContent { + fn from(doc: &MutexDocument) -> Self { + let doc = doc.lock(); + DocumentIndexContent::from(&*doc) + } +} diff --git a/frontend/rust-lib/flowy-document/src/lib.rs b/frontend/rust-lib/flowy-document/src/lib.rs index 365ba63da7..024685bf79 100644 --- a/frontend/rust-lib/flowy-document/src/lib.rs +++ b/frontend/rust-lib/flowy-document/src/lib.rs @@ -11,3 +11,4 @@ pub mod deps; pub mod notification; mod parse; pub mod reminder; +pub use collab_document::document::DocumentIndexContent; diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index adb03672a0..5b17b59118 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -14,7 +14,7 @@ bytes.workspace = true anyhow.workspace = true thiserror = "1.0" validator = "0.16.0" -tokio = { workspace = true, features = ["sync"]} +tokio = { workspace = true, features = ["sync", "rt"] } fancy-regex = { version = "0.11.0" } lib-dispatch = { workspace = true, optional = true } @@ -32,16 +32,23 @@ collab-document = { version = "0.1.0", optional = true } collab-plugins = { version = "0.1.0", optional = true } collab-folder = { version = "0.1.0", optional = true } client-api = { version = "0.1.0", optional = true } +tantivy = { version = "0.21.1", optional = true } + [features] impl_from_dispatch_error = ["lib-dispatch"] impl_from_serde = [] impl_from_reqwest = ["reqwest"] impl_from_collab_persistence = ["collab-plugins"] -impl_from_collab_document = ["collab-document", "impl_from_reqwest", "collab-plugins"] +impl_from_collab_document = [ + "collab-document", + "impl_from_reqwest", + "collab-plugins", +] impl_from_collab_folder = ["collab-folder"] -impl_from_collab_database= ["collab-database"] +impl_from_collab_database = ["collab-database"] impl_from_url = ["url"] +impl_from_tantivy = ["tantivy"] impl_from_sqlite = ["flowy-sqlite", "r2d2"] impl_from_appflowy_cloud = ["client-api"] @@ -50,6 +57,4 @@ tauri_ts = ["flowy-codegen/ts"] web_ts = ["flowy-codegen/ts"] [build-dependencies] -flowy-codegen = { workspace = true, features = [ - "proto_gen", -] } +flowy-codegen = { workspace = true, features = ["proto_gen"] } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 404b30b54b..4a2f3de4cf 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -260,11 +260,23 @@ pub enum ErrorCode { #[error("Cloud request payload too large")] CloudRequestPayloadTooLarge = 90, + #[error("IndexWriter failed to commit")] + IndexWriterFailedCommit = 91, + + #[error("Failed to open Index directory")] + FailedToOpenIndexDir = 92, + + #[error("Failed to parse query")] + FailedToParseQuery = 93, + + #[error("FolderIndexManager or its dependencies are unavailable")] + FolderIndexManagerUnavailable = 94, + #[error("Workspace limit exceeded")] - WorkspaceLimitExeceeded = 91, + WorkspaceLimitExeceeded = 95, #[error("Workspace member limit exceeded")] - WorkspaceMemberLimitExeceeded = 92, + WorkspaceMemberLimitExeceeded = 96, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index 47151ed8ec..32d22d489d 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -113,6 +113,10 @@ impl FlowyError { static_flowy_error!(server_error, ErrorCode::InternalServerError); static_flowy_error!(not_support, ErrorCode::NotSupportYet); static_flowy_error!(local_version_not_support, ErrorCode::LocalVersionNotSupport); + static_flowy_error!( + folder_index_manager_unavailable, + ErrorCode::FolderIndexManagerUnavailable + ); } impl std::convert::From for FlowyError { diff --git a/frontend/rust-lib/flowy-error/src/impl_from/mod.rs b/frontend/rust-lib/flowy-error/src/impl_from/mod.rs index c52e6f7750..b3d0351cd4 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/mod.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/mod.rs @@ -24,3 +24,6 @@ mod cloud; #[cfg(feature = "impl_from_url")] mod url; + +#[cfg(feature = "impl_from_tantivy")] +mod tantivy; diff --git a/frontend/rust-lib/flowy-error/src/impl_from/tantivy.rs b/frontend/rust-lib/flowy-error/src/impl_from/tantivy.rs new file mode 100644 index 0000000000..ead0b26d96 --- /dev/null +++ b/frontend/rust-lib/flowy-error/src/impl_from/tantivy.rs @@ -0,0 +1,21 @@ +use tantivy::{directory::error::OpenDirectoryError, query::QueryParserError, TantivyError}; + +use crate::{ErrorCode, FlowyError}; + +impl std::convert::From for FlowyError { + fn from(error: TantivyError) -> Self { + FlowyError::new(ErrorCode::IndexWriterFailedCommit, error) + } +} + +impl std::convert::From for FlowyError { + fn from(error: OpenDirectoryError) -> Self { + FlowyError::new(ErrorCode::FailedToOpenIndexDir, error) + } +} + +impl std::convert::From for FlowyError { + fn from(error: QueryParserError) -> Self { + FlowyError::new(ErrorCode::FailedToParseQuery, error) + } +} diff --git a/frontend/rust-lib/flowy-folder-pub/src/entities.rs b/frontend/rust-lib/flowy-folder-pub/src/entities.rs index 950f7144ab..41163fae73 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/entities.rs @@ -23,3 +23,19 @@ pub struct ImportViews { /// Used to update the [DatabaseViewTrackerList] when importing the database. pub database_view_ids_by_database_id: HashMap>, } + +pub struct SearchData { + /// The type of data that is stored in the search index row. + pub index_type: String, + + /// The `View` that the row references. + pub view_id: String, + + /// The ID that corresponds to the type that is stored. + /// View: view_id + /// Document: page_id + pub id: String, + + /// The data that is stored in the search index row. + pub data: String, +} diff --git a/frontend/rust-lib/flowy-folder-pub/src/folder_service.rs b/frontend/rust-lib/flowy-folder-pub/src/folder_service.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/frontend/rust-lib/flowy-folder-pub/src/folder_service.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/rust-lib/flowy-folder-pub/src/lib.rs b/frontend/rust-lib/flowy-folder-pub/src/lib.rs index f553f95acb..feaa5c2a0e 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/lib.rs @@ -1,4 +1,3 @@ pub mod cloud; pub mod entities; pub mod folder_builder; -mod folder_service; diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index 5a4ee05ec3..0bbc78bc6b 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -8,24 +8,29 @@ edition = "2021" [dependencies] collab = { version = "0.1.0" } collab-folder = { version = "0.1.0" } +collab-document = { version = "0.1.0" } collab-entity = { version = "0.1.0" } collab-plugins = { version = "0.1.0" } collab-integrate = { workspace = true } flowy-folder-pub = { workspace = true } +flowy-search-pub = { workspace = true } flowy-derive.workspace = true -flowy-notification = { workspace = true } +flowy-notification = { workspace = true } parking_lot.workspace = true unicode-segmentation = "1.10" tracing.workspace = true -flowy-error = { path = "../flowy-error", features = ["impl_from_dispatch_error", "impl_from_collab_folder"]} +flowy-error = { path = "../flowy-error", features = [ + "impl_from_dispatch_error", + "impl_from_collab_folder", +] } lib-dispatch = { workspace = true } bytes.workspace = true lib-infra = { workspace = true } tokio = { workspace = true, features = ["sync"] } nanoid = "0.4.0" lazy_static = "1.4.0" -chrono = { workspace = true, default-features = false, features = ["clock"] } +chrono = { workspace = true, default-features = false, features = ["clock"] } strum_macros = "0.21" protobuf.workspace = true uuid.workspace = true diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 84b5ad8bb1..4d95f3930f 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -9,6 +9,7 @@ use collab_folder::{ Folder, FolderData, FolderNotify, Section, SectionItem, TrashInfo, UserId, View, ViewLayout, ViewUpdate, Workspace, }; +use flowy_search_pub::entities::FolderIndexManager; use parking_lot::{Mutex, RwLock}; use tracing::{error, info, instrument}; @@ -48,12 +49,16 @@ conditional_send_sync_trait! { } pub struct FolderManager { + /// workspace_id represents as the id of the Folder. pub(crate) workspace_id: RwLock>, + + /// MutexFolder is the folder that is used to store the data. pub(crate) mutex_folder: Arc, pub(crate) collab_builder: Arc, pub(crate) user: Arc, pub(crate) operation_handlers: FolderOperationHandlers, pub cloud_service: Arc, + pub(crate) folder_indexer: Arc, } impl FolderManager { @@ -62,6 +67,7 @@ impl FolderManager { collab_builder: Arc, operation_handlers: FolderOperationHandlers, cloud_service: Arc, + folder_indexer: Arc, ) -> FlowyResult { let mutex_folder = Arc::new(MutexFolder::default()); let manager = Self { @@ -71,6 +77,7 @@ impl FolderManager { operation_handlers, cloud_service, workspace_id: Default::default(), + folder_indexer, }; Ok(manager) @@ -138,7 +145,7 @@ impl FolderManager { if let Some(workspace_id) = workspace_id { self.get_workspace_views(&workspace_id).await } else { - tracing::warn!("Can't get current workspace views"); + tracing::warn!("Can't get the workspace id from the folder. Return empty list."); Ok(vec![]) } } @@ -473,6 +480,13 @@ impl FolderManager { }, ); + if let Ok(workspace_id) = self.get_current_workspace_id().await { + let folder = &self.mutex_folder.lock(); + if let Some(folder) = folder.as_ref() { + notify_did_update_workspace(&workspace_id, folder); + } + } + Ok(view) } @@ -1205,6 +1219,8 @@ pub(crate) fn get_workspace_private_view_pbs(_workspace_id: &str, folder: &Folde .collect() } +/// The MutexFolder is a wrapper of the [Folder] that is used to share the folder between different +/// threads. #[derive(Clone, Default)] pub struct MutexFolder(Arc>>); impl Deref for MutexFolder { diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index f73ea35953..d79e79c750 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -1,13 +1,14 @@ use collab_entity::CollabType; use collab_folder::{Folder, FolderNotify, UserId}; +use tokio::task::spawn_blocking; +use tracing::{event, Level}; use collab_integrate::CollabKVDB; use flowy_error::{FlowyError, FlowyResult}; use std::sync::{Arc, Weak}; -use tracing::{event, Level}; use crate::manager::{FolderInitDataSource, FolderManager}; use crate::manager_observer::{ @@ -116,6 +117,22 @@ impl FolderManager { }; let folder_state_rx = folder.subscribe_sync_state(); + let index_content_rx = folder.subscribe_index_content(); + self + .folder_indexer + .set_index_content_receiver(index_content_rx); + + // Index all views in the folder if needed + if !self.folder_indexer.is_indexed() { + let views = folder.get_all_views_recursively(); + let folder_indexer = self.folder_indexer.clone(); + + // We spawn a blocking task to index all views in the folder + spawn_blocking(move || { + folder_indexer.index_all_views(views); + }); + } + *self.mutex_folder.lock() = Some(folder); let weak_mutex_folder = Arc::downgrade(&self.mutex_folder); diff --git a/frontend/rust-lib/flowy-search-pub/Cargo.toml b/frontend/rust-lib/flowy-search-pub/Cargo.toml new file mode 100644 index 0000000000..19f784771e --- /dev/null +++ b/frontend/rust-lib/flowy-search-pub/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "flowy-search-pub" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +collab = { version = "0.1.0" } +collab-folder = { version = "0.1.0" } + +flowy-error = { workspace = true } diff --git a/frontend/rust-lib/flowy-search-pub/src/entities.rs b/frontend/rust-lib/flowy-search-pub/src/entities.rs new file mode 100644 index 0000000000..a96a774502 --- /dev/null +++ b/frontend/rust-lib/flowy-search-pub/src/entities.rs @@ -0,0 +1,26 @@ +use std::any::Any; + +use collab::core::collab::IndexContentReceiver; +use collab_folder::{View, ViewIcon, ViewLayout}; +use flowy_error::FlowyError; + +pub struct IndexableData { + pub id: String, + pub data: String, + pub icon: Option, + pub layout: ViewLayout, +} + +pub trait IndexManager: Send + Sync { + fn set_index_content_receiver(&self, rx: IndexContentReceiver); + fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>; + fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>; + fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError>; + fn is_indexed(&self) -> bool; + + fn as_any(&self) -> &dyn Any; +} + +pub trait FolderIndexManager: IndexManager { + fn index_all_views(&self, views: Vec); +} diff --git a/frontend/rust-lib/flowy-search-pub/src/lib.rs b/frontend/rust-lib/flowy-search-pub/src/lib.rs new file mode 100644 index 0000000000..0b8f0b5a5a --- /dev/null +++ b/frontend/rust-lib/flowy-search-pub/src/lib.rs @@ -0,0 +1 @@ +pub mod entities; diff --git a/frontend/rust-lib/flowy-search/Cargo.toml b/frontend/rust-lib/flowy-search/Cargo.toml new file mode 100644 index 0000000000..d558df1c78 --- /dev/null +++ b/frontend/rust-lib/flowy-search/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "flowy-search" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +collab = { version = "0.1.0" } +collab-folder = { version = "0.1.0" } + +flowy-derive.workspace = true +flowy-error = { workspace = true, features = [ + "impl_from_sqlite", + "impl_from_dispatch_error", + "impl_from_collab_document", + "impl_from_tantivy", + "impl_from_serde", +] } +flowy-notification.workspace = true +flowy-sqlite.workspace = true +flowy-user.workspace = true +flowy-search-pub.workspace = true + +bytes.workspace = true +futures.workspace = true +lib-dispatch.workspace = true +protobuf.workspace = true +serde.workspace = true +serde_json.workspace = true +tokio = { workspace = true, features = ["full", "rt-multi-thread", "tracing"] } +tracing.workspace = true + +async-stream = "0.3.4" +strsim = "0.11.0" +strum_macros = "0.26.1" +tantivy = { version = "0.21.1" } +tempfile = "3.9.0" +validator = { version = "0.16.0", features = ["derive"] } + +diesel.workspace = true +diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } +diesel_migrations = { version = "2.1.0", features = ["sqlite"] } + +[build-dependencies] +flowy-codegen.workspace = true + +[dev-dependencies] +tempfile = "3.10.0" + +[features] +dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-search/Flowy.toml b/frontend/rust-lib/flowy-search/Flowy.toml new file mode 100644 index 0000000000..bca142f2fe --- /dev/null +++ b/frontend/rust-lib/flowy-search/Flowy.toml @@ -0,0 +1,2 @@ +proto_input = ["src/event_map.rs", "src/entities.rs"] +event_files = ["src/event_map.rs"] diff --git a/frontend/rust-lib/flowy-search/build.rs b/frontend/rust-lib/flowy-search/build.rs new file mode 100644 index 0000000000..2600d32fb7 --- /dev/null +++ b/frontend/rust-lib/flowy-search/build.rs @@ -0,0 +1,19 @@ +#[cfg(feature = "tauri_ts")] +use flowy_codegen::Project; + +fn main() { + #[cfg(any(feature = "dart", feature = "tauri_ts"))] + let crate_name = env!("CARGO_PKG_NAME"); + + #[cfg(feature = "dart")] + { + flowy_codegen::protobuf_file::dart_gen(crate_name); + flowy_codegen::dart_event::gen(crate_name); + } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::protobuf_file::ts_gen(crate_name, crate_name, Project::Tauri); + flowy_codegen::ts_event::gen(crate_name, Project::Tauri); + } +} diff --git a/frontend/rust-lib/flowy-search/src/entities.rs b/frontend/rust-lib/flowy-search/src/entities.rs new file mode 100644 index 0000000000..55b217a463 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/entities.rs @@ -0,0 +1,189 @@ +use collab_folder::{IconType, ViewIcon}; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; + +#[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)] +pub struct SearchQueryPB { + #[pb(index = 1)] + pub search: String, + + #[pb(index = 2, one_of)] + pub limit: Option, +} + +#[derive(Debug, Default, ProtoBuf, Clone)] +pub struct RepeatedSearchResultPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(ProtoBuf, Default, Debug, Clone)] +pub struct SearchResultPB { + #[pb(index = 1)] + pub index_type: IndexTypePB, + + #[pb(index = 2)] + pub view_id: String, + + #[pb(index = 3)] + pub id: String, + + #[pb(index = 4)] + pub data: String, + + #[pb(index = 5, one_of)] + pub icon: Option, + + #[pb(index = 6)] + pub score: f64, +} + +impl SearchResultPB { + pub fn with_score(&self, score: f64) -> Self { + SearchResultPB { + index_type: self.index_type.clone(), + view_id: self.view_id.clone(), + id: self.id.clone(), + data: self.data.clone(), + icon: self.icon.clone(), + score, + } + } +} + +#[derive(ProtoBuf_Enum, Clone, Debug, PartialEq, Eq, Default)] +pub enum ResultIconTypePB { + #[default] + Emoji = 0, + Url = 1, + Icon = 2, +} + +impl std::convert::From for IconType { + fn from(rev: ResultIconTypePB) -> Self { + match rev { + ResultIconTypePB::Emoji => IconType::Emoji, + ResultIconTypePB::Url => IconType::Url, + ResultIconTypePB::Icon => IconType::Icon, + } + } +} + +impl From for ResultIconTypePB { + fn from(val: IconType) -> Self { + match val { + IconType::Emoji => ResultIconTypePB::Emoji, + IconType::Url => ResultIconTypePB::Url, + IconType::Icon => ResultIconTypePB::Icon, + } + } +} + +impl std::convert::From for ResultIconTypePB { + fn from(icon_ty: i64) -> Self { + match icon_ty { + 0 => ResultIconTypePB::Emoji, + 1 => ResultIconTypePB::Url, + 2 => ResultIconTypePB::Icon, + _ => ResultIconTypePB::Emoji, + } + } +} + +impl std::convert::From for i64 { + fn from(val: ResultIconTypePB) -> Self { + match val { + ResultIconTypePB::Emoji => 0, + ResultIconTypePB::Url => 1, + ResultIconTypePB::Icon => 2, + } + } +} + +#[derive(Default, ProtoBuf, Debug, Clone, PartialEq, Eq)] +pub struct ResultIconPB { + #[pb(index = 1)] + pub ty: ResultIconTypePB, + + #[pb(index = 2)] + pub value: String, +} + +impl std::convert::From for ViewIcon { + fn from(rev: ResultIconPB) -> Self { + ViewIcon { + ty: rev.ty.into(), + value: rev.value, + } + } +} + +impl From for ResultIconPB { + fn from(val: ViewIcon) -> Self { + ResultIconPB { + ty: val.ty.into(), + value: val.value, + } + } +} + +#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] +pub enum IndexTypePB { + View = 0, + DocumentBlock = 1, + DatabaseRow = 2, +} + +impl Default for IndexTypePB { + fn default() -> Self { + Self::View + } +} + +impl std::convert::From for i32 { + fn from(notification: IndexTypePB) -> Self { + notification as i32 + } +} + +impl std::convert::From for IndexTypePB { + fn from(notification: i32) -> Self { + match notification { + 1 => IndexTypePB::View, + 2 => IndexTypePB::DocumentBlock, + _ => IndexTypePB::DatabaseRow, + } + } +} + +#[derive(ProtoBuf, Default, Debug, Clone)] +pub struct SearchResultNotificationPB { + #[pb(index = 1)] + pub items: Vec, + + #[pb(index = 2)] + pub closed: bool, +} + +#[derive(ProtoBuf_Enum, Debug, Default)] +pub enum SearchNotification { + #[default] + Unknown = 0, + DidUpdateResults = 1, + DidCloseResults = 2, +} + +impl std::convert::From for i32 { + fn from(notification: SearchNotification) -> Self { + notification as i32 + } +} + +impl std::convert::From for SearchNotification { + fn from(notification: i32) -> Self { + match notification { + 1 => SearchNotification::DidUpdateResults, + 2 => SearchNotification::DidCloseResults, + _ => SearchNotification::Unknown, + } + } +} diff --git a/frontend/rust-lib/flowy-search/src/event_handler.rs b/frontend/rust-lib/flowy-search/src/event_handler.rs new file mode 100644 index 0000000000..d39757dd4a --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/event_handler.rs @@ -0,0 +1,27 @@ +use std::sync::{Arc, Weak}; + +use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::{AFPluginData, AFPluginState}; + +use crate::{entities::SearchQueryPB, services::manager::SearchManager}; + +fn upgrade_manager( + search_manager: AFPluginState>, +) -> FlowyResult> { + let manager = search_manager + .upgrade() + .ok_or(FlowyError::internal().with_context("The SearchManager has already been dropped"))?; + Ok(manager) +} + +#[tracing::instrument(level = "debug", skip(manager), err)] +pub(crate) async fn search_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let query = data.into_inner(); + let manager = upgrade_manager(manager)?; + manager.perform_search(query.search); + + Ok(()) +} diff --git a/frontend/rust-lib/flowy-search/src/event_map.rs b/frontend/rust-lib/flowy-search/src/event_map.rs new file mode 100644 index 0000000000..7ab8838633 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/event_map.rs @@ -0,0 +1,21 @@ +use std::sync::Weak; +use strum_macros::Display; + +use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; +use lib_dispatch::prelude::*; + +use crate::{event_handler::search_handler, services::manager::SearchManager}; + +pub fn init(search_manager: Weak) -> AFPlugin { + AFPlugin::new() + .state(search_manager) + .name(env!("CARGO_PKG_NAME")) + .event(SearchEvent::Search, search_handler) +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] +#[event_err = "FlowyError"] +pub enum SearchEvent { + #[event(input = "SearchQueryPB")] + Search = 0, +} diff --git a/frontend/rust-lib/flowy-search/src/folder/entities.rs b/frontend/rust-lib/flowy-search/src/folder/entities.rs new file mode 100644 index 0000000000..ef2dafa60b --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/folder/entities.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +use crate::entities::{IndexTypePB, ResultIconPB, SearchResultPB}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct FolderIndexData { + pub id: String, + pub title: String, + pub icon: String, + pub icon_ty: i64, +} + +impl From for SearchResultPB { + fn from(data: FolderIndexData) -> Self { + let icon = if data.icon.is_empty() { + None + } else { + Some(ResultIconPB { + ty: data.icon_ty.into(), + value: data.icon, + }) + }; + + Self { + index_type: IndexTypePB::View, + view_id: data.id.clone(), + id: data.id, + data: data.title, + score: 0.0, + icon, + } + } +} diff --git a/frontend/rust-lib/flowy-search/src/folder/handler.rs b/frontend/rust-lib/flowy-search/src/folder/handler.rs new file mode 100644 index 0000000000..d83e646577 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/folder/handler.rs @@ -0,0 +1,30 @@ +use crate::entities::SearchResultPB; +use crate::services::manager::{SearchHandler, SearchType}; +use flowy_error::FlowyResult; +use std::sync::Arc; + +use super::indexer::FolderIndexManagerImpl; + +pub struct FolderSearchHandler { + pub index_manager: Arc, +} + +impl FolderSearchHandler { + pub fn new(index_manager: Arc) -> Self { + Self { index_manager } + } +} + +impl SearchHandler for FolderSearchHandler { + fn search_type(&self) -> SearchType { + SearchType::Folder + } + + fn perform_search(&self, query: String) -> FlowyResult> { + self.index_manager.search(query) + } + + fn index_count(&self) -> u64 { + self.index_manager.num_docs() + } +} diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs new file mode 100644 index 0000000000..4b5a9657a0 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -0,0 +1,376 @@ +use std::{any::Any, collections::HashMap, fs, path::Path, sync::Weak}; + +use crate::{ + entities::ResultIconTypePB, + folder::schema::{FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_TITLE_FIELD_NAME}, +}; +use collab::core::collab::{IndexContent, IndexContentReceiver}; +use collab_folder::{View, ViewIcon, ViewIndexContent, ViewLayout}; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_search_pub::entities::{FolderIndexManager, IndexManager, IndexableData}; +use flowy_user::services::authenticate_user::AuthenticateUser; +use lib_dispatch::prelude::af_spawn; +use strsim::levenshtein; +use tantivy::{ + collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, Index, IndexReader, + IndexWriter, Term, +}; + +use crate::entities::SearchResultPB; + +use super::{ + entities::FolderIndexData, + schema::{FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME}, +}; + +#[derive(Clone)] +pub struct FolderIndexManagerImpl { + folder_schema: Option, + index: Option, + index_reader: Option, +} + +const FOLDER_INDEX_DIR: &str = "folder_index"; + +impl FolderIndexManagerImpl { + pub fn new(auth_user: Weak) -> Self { + // AuthenticateUser is required to get the index path + let authenticate_user = auth_user.upgrade(); + + // Storage path is the users data path with an index directory + // Eg. /usr/flowy-data/indexes + let storage_path = match authenticate_user { + Some(auth_user) => auth_user.get_index_path(), + None => { + tracing::error!("FolderIndexManager: AuthenticateUser is not available"); + return FolderIndexManagerImpl::empty(); + }, + }; + + // We check if the `folder_index` directory exists, if not we create it + let index_path = storage_path.join(Path::new(FOLDER_INDEX_DIR)); + if !index_path.exists() { + let res = fs::create_dir_all(&index_path); + if let Err(e) = res { + tracing::error!( + "FolderIndexManager failed to create index directory: {:?}", + e + ); + return FolderIndexManagerImpl::empty(); + } + } + + // We open the existing or newly created folder_index directory + // This is required by the Tantivy Index, as it will use it to store + // and read index data + let dir = MmapDirectory::open(index_path); + if let Err(e) = dir { + tracing::error!("FolderIndexManager failed to open index directory: {:?}", e); + return FolderIndexManagerImpl::empty(); + } + + // The folder schema is used to define the fields of the index along + // with how they are stored and if the field is indexed + let folder_schema = FolderSchema::new(); + + // We open or create an index that takes the directory r/w and the schema. + let index_res = Index::open_or_create(dir.unwrap(), folder_schema.schema.clone()); + if let Err(e) = index_res { + tracing::error!("FolderIndexManager failed to open index: {:?}", e); + return FolderIndexManagerImpl::empty(); + } + + let index = index_res.unwrap(); + + // We read the index reader, we only need one IndexReader per index + let index_reader = index.reader(); + if let Err(e) = index_reader { + tracing::error!( + "FolderIndexManager failed to instantiate index reader: {:?}", + e + ); + return FolderIndexManagerImpl::empty(); + } + + Self { + folder_schema: Some(folder_schema), + index: Some(index), + index_reader: Some(index_reader.unwrap()), + } + } + + fn index_all(&self, indexes: Vec) -> Result<(), FlowyError> { + if self.is_indexed() || indexes.is_empty() { + return Ok(()); + } + + let mut index_writer = self.get_index_writer()?; + let folder_schema = self.get_folder_schema()?; + + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; + let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; + let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; + + for data in indexes { + let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); + + let _ = index_writer.add_document(doc![ + id_field => data.id.clone(), + title_field => data.data.clone(), + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + ]); + } + + index_writer.commit()?; + + Ok(()) + } + + pub fn num_docs(&self) -> u64 { + self + .index_reader + .clone() + .map(|reader| reader.searcher().num_docs()) + .unwrap_or(0) + } + + fn empty() -> Self { + Self { + folder_schema: None, + index: None, + index_reader: None, + } + } + + fn get_index_writer(&self) -> FlowyResult { + match &self.index { + // Creates an IndexWriter with a heap size of 50 MB (50.000.000 bytes) + Some(index) => Ok(index.writer(50_000_000)?), + None => Err(FlowyError::folder_index_manager_unavailable()), + } + } + + fn get_folder_schema(&self) -> FlowyResult { + match &self.folder_schema { + Some(folder_schema) => Ok(folder_schema.clone()), + None => Err(FlowyError::folder_index_manager_unavailable()), + } + } + + fn extract_icon( + &self, + view_icon: Option, + view_layout: ViewLayout, + ) -> (Option, i64) { + let icon_ty: i64; + let icon: Option; + + if view_icon.clone().is_some_and(|v| !v.value.is_empty()) { + let view_icon = view_icon.unwrap(); + let result_icon_ty: ResultIconTypePB = view_icon.ty.into(); + icon_ty = result_icon_ty.into(); + icon = Some(view_icon.value); + } else { + icon_ty = ResultIconTypePB::Icon.into(); + let layout_ty: i64 = view_layout.into(); + icon = Some(layout_ty.to_string()); + } + + (icon, icon_ty) + } + + pub fn search(&self, query: String) -> Result, FlowyError> { + let folder_schema = self.get_folder_schema()?; + + let index = match &self.index { + Some(index) => index, + None => return Err(FlowyError::folder_index_manager_unavailable()), + }; + + let index_reader = match &self.index_reader { + Some(index_reader) => index_reader, + None => return Err(FlowyError::folder_index_manager_unavailable()), + }; + + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; + + let length = query.len(); + let distance: u8 = match length { + _ if length > 4 => 2, + _ if length > 2 => 1, + _ => 0, + }; + + let mut query_parser = QueryParser::for_index(&index.clone(), vec![title_field]); + query_parser.set_field_fuzzy(title_field, true, distance, true); + let built_query = query_parser.parse_query(&query.clone())?; + + let searcher = index_reader.searcher(); + let mut search_results: Vec = vec![]; + let top_docs = searcher.search(&built_query, &TopDocs::with_limit(10))?; + for (_score, doc_address) in top_docs { + let retrieved_doc = searcher.doc(doc_address)?; + + let mut content = HashMap::new(); + let named_doc = folder_schema.schema.to_named_doc(&retrieved_doc); + for (k, v) in named_doc.0 { + content.insert(k, v[0].clone()); + } + + if content.is_empty() { + continue; + } + + let s = serde_json::to_string(&content)?; + let result: SearchResultPB = serde_json::from_str::(&s)?.into(); + let score = self.score_result(&query, &result.data); + search_results.push(result.with_score(score)); + } + + Ok(search_results) + } + + // Score result by distance + fn score_result(&self, query: &str, term: &str) -> f64 { + let distance = levenshtein(query, term) as f64; + 1.0 / (distance + 1.0) + } +} + +impl IndexManager for FolderIndexManagerImpl { + fn is_indexed(&self) -> bool { + self + .index_reader + .clone() + .map(|reader| reader.searcher().num_docs() > 0) + .unwrap_or(false) + } + + fn set_index_content_receiver(&self, mut rx: IndexContentReceiver) { + let indexer = self.clone(); + af_spawn(async move { + while let Ok(msg) = rx.recv().await { + match msg { + IndexContent::Create(value) => match serde_json::from_value::(value) { + Ok(view) => { + let _ = indexer.add_index(IndexableData { + id: view.id, + data: view.name, + icon: view.icon, + layout: view.layout, + }); + }, + Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), + }, + IndexContent::Update(value) => match serde_json::from_value::(value) { + Ok(view) => { + let _ = indexer.update_index(IndexableData { + id: view.id, + data: view.name, + icon: view.icon, + layout: view.layout, + }); + }, + Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), + }, + IndexContent::Delete(ids) => { + if let Err(e) = indexer.remove_indices(ids) { + tracing::error!("FolderIndexManager error deserialize: {:?}", e); + } + }, + } + } + }); + } + + fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; + + let folder_schema = self.get_folder_schema()?; + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; + let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; + let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; + + let delete_term = Term::from_field_text(id_field, &data.id.clone()); + + // Remove old index + index_writer.delete_term(delete_term); + + let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); + + // Add new index + let _ = index_writer.add_document(doc![ + id_field => data.id.clone(), + title_field => data.data, + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + ]); + + index_writer.commit()?; + + Ok(()) + } + + fn remove_indices(&self, ids: Vec) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; + let folder_schema = self.get_folder_schema()?; + + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + for id in ids { + let delete_term = Term::from_field_text(id_field, &id); + index_writer.delete_term(delete_term); + } + + index_writer.commit()?; + + Ok(()) + } + + fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; + + let folder_schema = self.get_folder_schema()?; + + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; + let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; + let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; + + let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); + + // Add new index + let _ = index_writer.add_document(doc![ + id_field => data.id, + title_field => data.data, + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + ]); + + index_writer.commit()?; + + Ok(()) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl FolderIndexManager for FolderIndexManagerImpl { + fn index_all_views(&self, views: Vec) { + let indexable_data = views + .into_iter() + .map(|view| IndexableData { + id: view.id, + data: view.name, + icon: view.icon, + layout: view.layout, + }) + .collect(); + + let _ = self.index_all(indexable_data); + } +} diff --git a/frontend/rust-lib/flowy-search/src/folder/mod.rs b/frontend/rust-lib/flowy-search/src/folder/mod.rs new file mode 100644 index 0000000000..26d1058ef0 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/folder/mod.rs @@ -0,0 +1,4 @@ +pub mod entities; +pub mod handler; +pub mod indexer; +pub mod schema; diff --git a/frontend/rust-lib/flowy-search/src/folder/schema.rs b/frontend/rust-lib/flowy-search/src/folder/schema.rs new file mode 100644 index 0000000000..9e86988d7f --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/folder/schema.rs @@ -0,0 +1,47 @@ +use tantivy::schema::Schema; + +pub const FOLDER_ID_FIELD_NAME: &str = "id"; +pub const FOLDER_TITLE_FIELD_NAME: &str = "title"; +pub const FOLDER_ICON_FIELD_NAME: &str = "icon"; +pub const FOLDER_ICON_TY_FIELD_NAME: &str = "icon_ty"; + +#[derive(Clone)] +pub struct FolderSchema { + pub schema: Schema, +} + +/// Do not change the schema after the index has been created. +/// Changing field_options or fields, will result in the schema being different +/// from previously created index, causing tantivy to panic and search to stop functioning. +/// +/// If you need to change the schema, create a migration that removes the old index, +/// and creates a new one with the new schema. +/// +impl FolderSchema { + pub fn new() -> Self { + let mut schema_builder = Schema::builder(); + schema_builder.add_text_field( + FOLDER_ID_FIELD_NAME, + tantivy::schema::STRING | tantivy::schema::STORED, + ); + schema_builder.add_text_field( + FOLDER_TITLE_FIELD_NAME, + tantivy::schema::TEXT | tantivy::schema::STORED, + ); + schema_builder.add_text_field( + FOLDER_ICON_FIELD_NAME, + tantivy::schema::TEXT | tantivy::schema::STORED, + ); + schema_builder.add_i64_field(FOLDER_ICON_TY_FIELD_NAME, tantivy::schema::STORED); + + let schema = schema_builder.build(); + + Self { schema } + } +} + +impl Default for FolderSchema { + fn default() -> Self { + Self::new() + } +} diff --git a/frontend/rust-lib/flowy-search/src/lib.rs b/frontend/rust-lib/flowy-search/src/lib.rs new file mode 100644 index 0000000000..9b2ea272d8 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/lib.rs @@ -0,0 +1,6 @@ +pub mod entities; +pub mod event_handler; +pub mod event_map; +pub mod folder; +pub mod protobuf; +pub mod services; diff --git a/frontend/rust-lib/flowy-search/src/services/manager.rs b/frontend/rust-lib/flowy-search/src/services/manager.rs new file mode 100644 index 0000000000..b548825a16 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/services/manager.rs @@ -0,0 +1,77 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use flowy_error::FlowyResult; +use lib_dispatch::prelude::af_spawn; +use tokio::{sync::broadcast, task::spawn_blocking}; + +use crate::entities::{SearchResultNotificationPB, SearchResultPB}; + +use super::notifier::{SearchNotifier, SearchResultChanged, SearchResultReceiverRunner}; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum SearchType { + Folder, +} + +pub trait SearchHandler: Send + Sync + 'static { + /// returns the type of search this handler is responsible for + fn search_type(&self) -> SearchType; + /// performs a search and returns the results + fn perform_search(&self, query: String) -> FlowyResult>; + /// returns the number of indexed objects + fn index_count(&self) -> u64; +} + +/// The [SearchManager] is used to inject multiple [SearchHandler]'s +/// to delegate a search to all relevant handlers, and stream the result +/// to the client until the query has been fully completed. +/// +pub struct SearchManager { + pub handlers: HashMap>, + notifier: SearchNotifier, +} + +impl SearchManager { + pub fn new(handlers: Vec>) -> Self { + let handlers: HashMap> = handlers + .into_iter() + .map(|handler| (handler.search_type(), handler)) + .collect(); + + // Initialize Search Notifier + let (notifier, _) = broadcast::channel(100); + af_spawn(SearchResultReceiverRunner(Some(notifier.subscribe())).run()); + + Self { handlers, notifier } + } + + pub fn get_handler(&self, search_type: SearchType) -> Option<&Arc> { + self.handlers.get(&search_type) + } + + pub fn perform_search(&self, query: String) { + let mut sends: usize = 0; + let max: usize = self.handlers.len(); + let handlers = self.handlers.clone(); + + for (_, handler) in handlers { + let q = query.clone(); + let notifier = self.notifier.clone(); + + spawn_blocking(move || { + let res = handler.perform_search(q); + sends += 1; + + let close = sends == max; + let items = res.unwrap_or_default(); + let notification = SearchResultNotificationPB { + items, + closed: close, + }; + + let _ = notifier.send(SearchResultChanged::SearchResultUpdate(notification)); + }); + } + } +} diff --git a/frontend/rust-lib/flowy-search/src/services/mod.rs b/frontend/rust-lib/flowy-search/src/services/mod.rs new file mode 100644 index 0000000000..2a417e6c62 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/services/mod.rs @@ -0,0 +1,2 @@ +pub mod manager; +pub mod notifier; diff --git a/frontend/rust-lib/flowy-search/src/services/notifier.rs b/frontend/rust-lib/flowy-search/src/services/notifier.rs new file mode 100644 index 0000000000..9b515cbd10 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/services/notifier.rs @@ -0,0 +1,53 @@ +use async_stream::stream; +use flowy_notification::NotificationBuilder; +use futures::stream::StreamExt; +use tokio::sync::broadcast; + +use crate::entities::{SearchNotification, SearchResultNotificationPB}; + +const OBSERVABLE_SOURCE: &str = "SEARCH"; +const SEARCH_ID: &str = "SEARCH_IDENTIFIER"; + +#[derive(Clone)] +pub enum SearchResultChanged { + SearchResultUpdate(SearchResultNotificationPB), +} + +pub type SearchNotifier = broadcast::Sender; + +pub(crate) struct SearchResultReceiverRunner( + pub(crate) Option>, +); + +impl SearchResultReceiverRunner { + pub(crate) async fn run(mut self) { + let mut receiver = self.0.take().expect("Only take once"); + let stream = stream! { + while let Ok(changed) = receiver.recv().await { + yield changed; + } + }; + stream + .for_each(|changed| async { + match changed { + SearchResultChanged::SearchResultUpdate(notification) => { + let ty = if notification.closed { + SearchNotification::DidCloseResults + } else { + SearchNotification::DidUpdateResults + }; + + send_notification(SEARCH_ID, ty) + .payload(notification) + .send(); + }, + } + }) + .await; + } +} + +#[tracing::instrument(level = "trace")] +pub fn send_notification(id: &str, ty: SearchNotification) -> NotificationBuilder { + NotificationBuilder::new(id, ty, OBSERVABLE_SOURCE) +} diff --git a/frontend/rust-lib/flowy-search/tests/main.rs b/frontend/rust-lib/flowy-search/tests/main.rs new file mode 100644 index 0000000000..797a86518e --- /dev/null +++ b/frontend/rust-lib/flowy-search/tests/main.rs @@ -0,0 +1,3 @@ +// mod search; + +mod tantivy_test; diff --git a/frontend/rust-lib/flowy-search/tests/tantivy_test.rs b/frontend/rust-lib/flowy-search/tests/tantivy_test.rs new file mode 100644 index 0000000000..b07853c7de --- /dev/null +++ b/frontend/rust-lib/flowy-search/tests/tantivy_test.rs @@ -0,0 +1,53 @@ +use tantivy::collector::TopDocs; +use tantivy::query::QueryParser; +use tantivy::schema::*; +use tantivy::{doc, DocAddress, Index, Score}; + +#[test] +fn search_folder_test() { + let mut schema_builder = Schema::builder(); + let id = schema_builder.add_text_field("id", TEXT); + let title = schema_builder.add_text_field("title", TEXT | STORED); + let schema = schema_builder.build(); + + // Indexing documents + let index = Index::create_from_tempdir(schema.clone()).unwrap(); + + // Here we use a buffer of 100MB that will be split + // between indexing threads. + let mut index_writer = index.writer(100_000_000).unwrap(); + + // Let's index one documents! + index_writer + .add_document(doc!( + id => "123456789", + title => "The Old Man and the Seawhale", + )) + .unwrap(); + + // We need to call .commit() explicitly to force the + // index_writer to finish processing the documents in the queue, + // flush the current index to the disk, and advertise + // the existence of new documents. + index_writer.commit().unwrap(); + + // # Searching + let reader = index.reader().unwrap(); + + let searcher = reader.searcher(); + + let mut query_parser = QueryParser::for_index(&index, vec![title]); + query_parser.set_field_fuzzy(title, true, 2, true); + let query = query_parser.parse_query("sewhals").unwrap(); + + // Perform search. + // `topdocs` contains the 10 most relevant doc ids, sorted by decreasing scores... + let top_docs: Vec<(Score, DocAddress)> = + searcher.search(&query, &TopDocs::with_limit(10)).unwrap(); + + for (_score, doc_address) in top_docs { + // Retrieve the actual content of documents given its `doc_address`. + let retrieved_doc = searcher.doc(doc_address).unwrap(); + println!("{}", schema.to_json(&retrieved_doc)); + } +} diff --git a/frontend/rust-lib/flowy-sqlite/src/lib.rs b/frontend/rust-lib/flowy-sqlite/src/lib.rs index a052d7afe5..0911b48fd2 100644 --- a/frontend/rust-lib/flowy-sqlite/src/lib.rs +++ b/frontend/rust-lib/flowy-sqlite/src/lib.rs @@ -11,8 +11,7 @@ pub use diesel::*; pub use diesel_derives::*; use diesel_migrations::{EmbeddedMigrations, MigrationHarness}; -use crate::sqlite_impl::PoolConfig; -pub use crate::sqlite_impl::{ConnectionPool, DBConnection, Database}; +pub use crate::sqlite_impl::{ConnectionPool, DBConnection, Database, PoolConfig}; pub mod kv; mod sqlite_impl; @@ -44,6 +43,7 @@ pub fn init>(storage_path: P) -> Result { (*conn) .run_pending_migrations(MIGRATIONS) .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{:?}", e)))?; + Ok(database) } diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index ec423d9bda..47ae35f35a 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -12,7 +12,7 @@ flowy-encrypt = { workspace = true } flowy-error = { workspace = true, features = ["impl_from_dispatch_error", "impl_from_sqlite", "impl_from_collab_folder", "impl_from_collab_persistence"] } flowy-folder-pub = { workspace = true } lib-infra = { workspace = true } -flowy-notification = { workspace = true } +flowy-notification = { workspace = true } flowy-server-pub = { workspace = true } lib-dispatch = { workspace = true } collab-integrate = { workspace = true } @@ -43,7 +43,7 @@ validator = "0.16.0" unicode-segmentation = "1.10" fancy-regex = "0.11.0" uuid.workspace = true -chrono = { workspace = true, default-features = false, features = ["clock"] } +chrono = { workspace = true, default-features = false, features = ["clock"] } base64 = "^0.21" tokio-stream = "0.1.14" diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index 6f560f0811..2cce4597ac 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -8,6 +8,7 @@ use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_sqlite::kv::StorePreferences; use flowy_sqlite::DBConnection; use flowy_user_pub::session::Session; +use std::path::PathBuf; use std::sync::{Arc, Weak}; use tracing::{debug, error, info}; @@ -72,6 +73,11 @@ impl AuthenticateUser { self.database.get_connection(uid) } + pub fn get_index_path(&self) -> PathBuf { + let uid = self.user_id().unwrap_or(0); + PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes") + } + pub fn close_db(&self) -> FlowyResult<()> { let session = self.get_session()?; info!("Close db for user: {}", session.user_id); diff --git a/frontend/rust-lib/lib-infra/src/file_util.rs b/frontend/rust-lib/lib-infra/src/file_util.rs index 8435c30d1f..2186c71eaa 100644 --- a/frontend/rust-lib/lib-infra/src/file_util.rs +++ b/frontend/rust-lib/lib-infra/src/file_util.rs @@ -120,7 +120,7 @@ pub fn unzip_and_replace( // Unzip the file let file = File::open(zip_path.as_ref()) - .context(format!("Can't find the zip file: {:?}", zip_path.as_ref()))?; + .with_context(|| format!("Can't find the zip file: {:?}", zip_path.as_ref()))?; let mut archive = ZipArchive::new(file).context("Unzip file fail")?; for i in 0..archive.len() { @@ -143,14 +143,43 @@ pub fn unzip_and_replace( // Replace the contents of the target folder if target_folder.exists() { fs::remove_dir_all(target_folder) - .context(format!("Remove all files in {:?}", target_folder))?; + .with_context(|| format!("Remove all files in {:?}", target_folder))?; } fs::create_dir_all(target_folder)?; for entry in fs::read_dir(temp_dir.path())? { let entry = entry?; - fs::rename(entry.path(), target_folder.join(entry.file_name()))?; + let target_file = target_folder.join(entry.file_name()); + + // Use a copy and delete approach instead of fs::rename + if entry.path().is_dir() { + // Recursively copy directory contents + copy_dir_all(entry.path(), &target_file)?; + } else { + fs::copy(entry.path(), &target_file)?; + } + // Remove the original file/directory after copying + if entry.path().is_dir() { + fs::remove_dir_all(entry.path())?; + } else { + fs::remove_file(entry.path())?; + } } Ok(()) } + +// Helper function for recursively copying directories +fn copy_dir_all(src: PathBuf, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + copy_dir_all(entry.path(), &dst.join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dst.join(entry.file_name()))?; + } + } + Ok(()) +}