From ef9891abfef5713f32e013040699cdc47b7f525d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 21 Mar 2024 11:02:03 +0700 Subject: [PATCH] feat: support private section (#4882) --- frontend/.vscode/launch.json | 5 +- .../cloud/anon_user_continue_test.dart | 4 +- .../workspace/change_name_and_icon_test.dart | 18 +- .../collaborative_workspace_test.dart | 10 +- .../desktop/sidebar/sidebar_expand_test.dart | 16 +- .../sidebar/sidebar_favorites_test.dart | 2 +- .../desktop/sidebar/sidebar_test_runner.dart | 3 +- .../presentation/home/mobile_folders.dart | 77 +++-- .../presentation/home/mobile_home_page.dart | 107 ++++--- .../home/mobile_home_page_header.dart | 112 ++++++-- .../mobile_home_section_folder.dart} | 17 +- .../mobile_home_section_folder_header.dart} | 17 +- .../mobile_notifications_page.dart | 14 +- .../page_item/mobile_view_item.dart | 1 + .../document/application/doc_bloc.dart | 62 ++-- .../lib/shared/feature_flags.dart | 22 +- .../lib/user/application/user_service.dart | 40 +++ .../menu/sidebar_root_views_bloc.dart | 95 +++--- .../menu/sidebar_sections_bloc.dart | 261 +++++++++++++++++ .../sidebar/folder/folder_bloc.dart | 15 +- .../application/user/user_workspace_bloc.dart | 13 +- .../workspace/application/view/view_bloc.dart | 7 +- .../application/view/view_service.dart | 9 + .../workspace/workspace_listener.dart | 11 +- .../workspace_sections_listener.dart | 68 +++++ .../workspace/workspace_service.dart | 31 +- ...rite_folder.dart => _favorite_folder.dart} | 0 .../menu/sidebar/folder/_folder_header.dart | 63 ++++ .../menu/sidebar/folder/_section_folder.dart | 116 ++++++++ .../menu/sidebar/folder/personal_folder.dart | 146 ---------- .../home/menu/sidebar/sidebar.dart | 272 +++++++++--------- .../home/menu/sidebar/sidebar_folder.dart | 116 ++++++-- .../menu/sidebar/sidebar_new_page_button.dart | 12 +- .../home/menu/sidebar/sidebar_top_menu.dart | 4 +- .../home/menu/sidebar/sidebar_user.dart | 7 +- .../home/menu/sidebar/sidebar_workspace.dart | 5 +- .../workspace/_sidebar_workspace_menu.dart | 6 +- .../home/menu/view/draggable_view_item.dart | 14 + .../home/menu/view/view_item.dart | 1 + .../widgets/notification_button.dart | 9 +- .../members/workspace_member_bloc.dart | 61 ++-- .../presentation/widgets/user_avatar.dart | 2 +- .../appflowy_result/lib/src/async_result.dart | 4 + .../bloc_test/home_test/home_bloc_test.dart | 9 +- .../bloc_test/home_test/menu_bloc_test.dart | 46 --- .../home_test/sidebar_section_bloc_test.dart | 57 ++++ .../bloc_test/home_test/trash_bloc_test.dart | 3 + .../bloc_test/home_test/view_bloc_test.dart | 68 ++++- frontend/appflowy_flutter/test/util.dart | 7 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 37 +-- frontend/appflowy_tauri/src-tauri/Cargo.toml | 16 +- .../application/folder/workspace.service.ts | 21 +- frontend/appflowy_web/wasm-libs/Cargo.lock | 35 +-- frontend/appflowy_web/wasm-libs/Cargo.toml | 16 +- frontend/resources/translations/en.json | 6 + frontend/rust-lib/Cargo.lock | 37 +-- frontend/rust-lib/Cargo.toml | 16 +- .../event-integration/src/database_event.rs | 3 + .../src/document/document_event.rs | 1 + .../event-integration/src/document_event.rs | 1 + .../event-integration/src/folder_event.rs | 4 +- .../event-integration/src/user_event.rs | 4 +- .../tests/folder/local_test/script.rs | 3 + .../tests/folder/local_test/test.rs | 2 + .../user/af_cloud_test/workspace_test.rs | 4 +- .../flowy-folder-pub/src/folder_builder.rs | 1 + .../flowy-folder/src/entities/view.rs | 39 +++ .../flowy-folder/src/entities/workspace.rs | 36 +++ .../flowy-folder/src/event_handler.rs | 28 +- .../rust-lib/flowy-folder/src/event_map.rs | 14 +- frontend/rust-lib/flowy-folder/src/manager.rs | 100 ++++++- .../flowy-folder/src/manager_observer.rs | 36 ++- .../rust-lib/flowy-folder/src/notification.rs | 5 + .../rust-lib/flowy-folder/src/test_helper.rs | 3 +- .../rust-lib/flowy-folder/src/user_default.rs | 1 + 75 files changed, 1758 insertions(+), 776 deletions(-) rename frontend/appflowy_flutter/lib/mobile/presentation/home/{personal_folder/mobile_home_personal_folder.dart => section_folder/mobile_home_section_folder.dart} (85%) rename frontend/appflowy_flutter/lib/mobile/presentation/home/{personal_folder/mobile_home_personal_folder_header.dart => section_folder/mobile_home_section_folder_header.dart} (82%) create mode 100644 frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/{favorite_folder.dart => _favorite_folder.dart} (100%) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart delete mode 100644 frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart create mode 100644 frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json index 1bc6978a44..72d398e0fa 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -115,9 +115,12 @@ }, { "name": "AF-desktop: Debug Rust", - "request": "attach", "type": "lldb", + "request": "attach", "pid": "${command:pickMyProcess}" + // To launch the application directly, use the following configuration: + // "request": "launch", + // "program": "[YOUR_APPLICATION_PATH]", }, { // https://tauri.app/v1/guides/debugging/vs-code diff --git a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart index 6f58ba6354..1e555b1667 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: unused_import import 'dart:io'; +import 'dart:ui'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -14,8 +15,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:path/path.dart' as p; import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + import '../shared/dir.dart'; import '../shared/mock/mock_file_picker.dart'; import '../shared/util.dart'; diff --git a/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart index b19de8059f..5e0122c5ef 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart @@ -28,15 +28,16 @@ void main() { final email = '${uuid()}@appflowy.io'; testWidgets('change name and icon', (tester) async { + // only run the test when the feature flag is on + if (!FeatureFlag.collaborativeWorkspace.isOn) { + return; + } + await tester.initializeAppFlowy( cloudType: AuthenticatorType.appflowyCloudSelfHost, email: email, // use the same email to check the next test ); - // turn on the collaborative workspace feature flag before testing, - // if the feature is released to the public, this step can be removed - await FeatureFlag.collaborativeWorkspace.turnOn(); - await tester.tapGoogleLoginInButton(); await tester.expectToSeeHomePageWithGetStartedPage(); @@ -57,15 +58,16 @@ void main() { }); testWidgets('verify the result again after relaunching', (tester) async { + // only run the test when the feature flag is on + if (!FeatureFlag.collaborativeWorkspace.isOn) { + return; + } + await tester.initializeAppFlowy( cloudType: AuthenticatorType.appflowyCloudSelfHost, email: email, // use the same email to check the next test ); - // turn on the collaborative workspace feature flag before testing, - // if the feature is released to the public, this step can be removed - await FeatureFlag.collaborativeWorkspace.turnOn(); - await tester.tapGoogleLoginInButton(); await tester.expectToSeeHomePageWithGetStartedPage(); diff --git a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart index 856d68f0c1..31348b6485 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart @@ -35,14 +35,14 @@ void main() { final email = '${uuid()}@appflowy.io'; group('collaborative workspace', () { - // only run the test when the feature flag is on - if (!FeatureFlag.collaborativeWorkspace.isOn) { - return; - } - // combine the create and delete workspace test to reduce the time testWidgets('create a new workspace, open it and then delete it', (tester) async { + // only run the test when the feature flag is on + if (!FeatureFlag.collaborativeWorkspace.isOn) { + return; + } + await tester.initializeAppFlowy( cloudType: AuthenticatorType.appflowyCloudSelfHost, email: email, diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart index 7568a81def..9ff563604d 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,10 +13,10 @@ void main() { group('sidebar expand test', () { bool isExpanded({required FolderCategoryType type}) { - if (type == FolderCategoryType.personal) { + if (type == FolderCategoryType.private) { return find .descendant( - of: find.byType(PersonalFolder), + of: find.byType(PrivateSectionFolder), matching: find.byType(ViewItem), ) .evaluate() @@ -30,19 +30,19 @@ void main() { await tester.tapGoButton(); // first time is expanded - expect(isExpanded(type: FolderCategoryType.personal), true); + expect(isExpanded(type: FolderCategoryType.private), true); // collapse the personal folder await tester.tapButton( - find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()), + find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), ); - expect(isExpanded(type: FolderCategoryType.personal), false); + expect(isExpanded(type: FolderCategoryType.private), false); // expand the personal folder await tester.tapButton( - find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()), + find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), ); - expect(isExpanded(type: FolderCategoryType.personal), true); + expect(isExpanded(type: FolderCategoryType.private), true); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart index aa4f151ab8..9ccd06d526 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart index bf199036a8..35bcf599ab 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart @@ -1,6 +1,5 @@ import 'package:integration_test/integration_test.dart'; -import 'sidebar_expand_test.dart' as sidebar_expanded_test; import 'sidebar_favorites_test.dart' as sidebar_favorite_test; import 'sidebar_icon_test.dart' as sidebar_icon_test; import 'sidebar_test.dart' as sidebar_test; @@ -10,7 +9,7 @@ void startTesting() { // Sidebar integration tests sidebar_test.main(); - sidebar_expanded_test.main(); + // sidebar_expanded_test.main(); sidebar_favorite_test.main(); sidebar_icon_test.main(); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart index cc42b3c9b9..211ce95bc5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -1,14 +1,18 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart'; +import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart'; +import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +// Contains Public And Private Sections class MobileFolders extends StatelessWidget { const MobileFolders({ super.key, @@ -26,9 +30,9 @@ class MobileFolders extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => SidebarRootViewsBloc() + create: (_) => SidebarSectionsBloc() ..add( - SidebarRootViewsEvent.initial( + SidebarSectionsEvent.initial( user, workspaceSetting.workspaceId, ), @@ -38,30 +42,45 @@ class MobileFolders extends StatelessWidget { create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), ), ], - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => - p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, - listener: (context, state) => - context.pushView(state.lastCreatedRootView!), - ), - ], - child: Builder( - builder: (context) { - final menuState = context.watch().state; - return SlidableAutoCloseBehavior( - child: Column( - children: [ - MobilePersonalFolder( - views: menuState.views, - ), - const VSpace(8.0), - ], - ), - ); - }, - ), + child: BlocConsumer( + listenWhen: (p, c) => + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, + listener: (context, state) { + final lastCreatedRootView = state.lastCreatedRootView; + if (lastCreatedRootView != null) { + context.pushView(lastCreatedRootView); + } + }, + builder: (context, state) { + final isCollaborativeWorkspace = + user.authenticator != AuthenticatorPB.Local && + FeatureFlag.collaborativeWorkspace.isOn; + return SlidableAutoCloseBehavior( + child: Column( + children: [ + ...isCollaborativeWorkspace + ? [ + MobileSectionFolder( + title: LocaleKeys.sideBar_public.tr(), + views: state.section.publicViews, + ), + const VSpace(8.0), + MobileSectionFolder( + title: LocaleKeys.sideBar_private.tr(), + views: state.section.privateViews, + ), + ] + : [ + MobileSectionFolder( + title: LocaleKeys.sideBar_personal.tr(), + views: state.section.publicViews, + ), + ], + const VSpace(8.0), + ], + ), + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index cab7da1fc7..b56b36a839 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -8,6 +8,7 @@ import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; @@ -15,6 +16,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -82,55 +84,68 @@ class MobileHomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - // Header - Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: Platform.isAndroid ? 8.0 : 0.0, - ), - child: MobileHomePageHeader( - userProfile: userProfile, - ), + return BlocProvider( + create: (_) => UserWorkspaceBloc(userProfile: userProfile) + ..add( + const UserWorkspaceEvent.initial(), ), - const Divider(), - - // Folder - Expanded( - child: Scrollbar( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Recent files - const MobileRecentFolder(), - - // Folders - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: MobileFolders( - user: userProfile, - workspaceSetting: workspaceSetting, - showFavorite: false, - ), - ), - const SizedBox(height: 8), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: _TrashButton(), - ), - ], + child: BlocBuilder( + buildWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, + builder: (context, state) { + return Column( + children: [ + // Header + Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: Platform.isAndroid ? 8.0 : 0.0, + ), + child: MobileHomePageHeader( + userProfile: userProfile, ), ), - ), - ), - ), - ], + const Divider(), + + // Folder + Expanded( + child: Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Recent files + const MobileRecentFolder(), + + // Folders + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: MobileFolders( + user: userProfile, + workspaceSetting: workspaceSetting, + showFavorite: false, + ), + ), + const SizedBox(height: 8), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: _TrashButton(), + ), + ], + ), + ), + ), + ), + ), + ], + ); + }, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 157f878b75..05fc82eb88 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -3,10 +3,13 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -14,7 +17,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class MobileHomePageHeader extends StatelessWidget { - const MobileHomePageHeader({super.key, required this.userProfile}); + const MobileHomePageHeader({ + super.key, + required this.userProfile, + }); final UserProfilePB userProfile; @@ -25,29 +31,18 @@ class MobileHomePageHeader extends StatelessWidget { ..add(const SettingsUserEvent.initial()), child: BlocBuilder( builder: (context, state) { - final userIcon = state.userProfile.iconUrl; + final isCollaborativeWorkspace = + userProfile.authenticator != AuthenticatorPB.Local && + FeatureFlag.collaborativeWorkspace.isOn; return ConstrainedBox( constraints: const BoxConstraints(minHeight: 52), child: Row( + mainAxisSize: MainAxisSize.min, children: [ - _UserIcon(userIcon: userIcon), - const HSpace(12), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const FlowyText.medium('AppFlowy', fontSize: 18), - const VSpace(4), - FlowyText.regular( - userProfile.email.isNotEmpty - ? state.userProfile.email - : state.userProfile.name, - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface, - overflow: TextOverflow.ellipsis, - ), - ], - ), + child: isCollaborativeWorkspace + ? _MobileWorkspace(userProfile: userProfile) + : _MobileUser(userProfile: userProfile), ), IconButton( onPressed: () => @@ -63,6 +58,83 @@ class MobileHomePageHeader extends StatelessWidget { } } +class _MobileUser extends StatelessWidget { + const _MobileUser({ + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final userIcon = userProfile.iconUrl; + return Row( + children: [ + _UserIcon(userIcon: userIcon), + const HSpace(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const FlowyText.medium('AppFlowy', fontSize: 18), + const VSpace(4), + FlowyText.regular( + userProfile.email.isNotEmpty + ? userProfile.email + : userProfile.name, + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ); + } +} + +class _MobileWorkspace extends StatelessWidget { + const _MobileWorkspace({ + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final currentWorkspace = state.currentWorkspace; + final workspaces = state.workspaces; + if (currentWorkspace == null || workspaces.isEmpty) { + return const SizedBox.shrink(); + } + return Row( + children: [ + const HSpace(2.0), + SizedBox.square( + dimension: 34.0, + child: WorkspaceIcon( + workspace: currentWorkspace, + iconSize: 26, + enableEdit: false, + ), + ), + const HSpace(8), + Expanded( + child: FlowyText.medium( + currentWorkspace.name, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + }, + ); + } +} + class _UserIcon extends StatelessWidget { const _UserIcon({ required this.userIcon, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart similarity index 85% rename from frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart rename to frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart index a5b04a7093..0042fe1cc5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart @@ -1,6 +1,6 @@ import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; -import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart'; +import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; @@ -9,18 +9,20 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class MobilePersonalFolder extends StatelessWidget { - const MobilePersonalFolder({ +class MobileSectionFolder extends StatelessWidget { + const MobileSectionFolder({ super.key, + required this.title, required this.views, }); + final String title; final List views; @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => FolderBloc(type: FolderCategoryType.personal) + create: (context) => FolderBloc(type: FolderCategoryType.private) ..add( const FolderEvent.initial(), ), @@ -28,7 +30,8 @@ class MobilePersonalFolder extends StatelessWidget { builder: (context, state) { return Column( children: [ - MobilePersonalFolderHeader( + MobileSectionFolderHeader( + title: title, isExpanded: context.read().state.isExpanded, onPressed: () => context .read() @@ -45,9 +48,9 @@ class MobilePersonalFolder extends StatelessWidget { ...views.map( (view) => MobileViewItem( key: ValueKey( - '${FolderCategoryType.personal.name} ${view.id}', + '${FolderCategoryType.private.name} ${view.id}', ), - categoryType: FolderCategoryType.personal, + categoryType: FolderCategoryType.private, isFirstChild: view.id == views.first.id, view: view, level: 0, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart similarity index 82% rename from frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart rename to frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart index 6e77f86454..16383c8b4b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart @@ -1,30 +1,32 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class MobilePersonalFolderHeader extends StatefulWidget { - const MobilePersonalFolderHeader({ +class MobileSectionFolderHeader extends StatefulWidget { + const MobileSectionFolderHeader({ super.key, + required this.title, required this.onPressed, required this.onAdded, required this.isExpanded, }); + final String title; final VoidCallback onPressed; final VoidCallback onAdded; final bool isExpanded; @override - State createState() => - _MobilePersonalFolderHeaderState(); + State createState() => + _MobileSectionFolderHeaderState(); } -class _MobilePersonalFolderHeaderState - extends State { +class _MobileSectionFolderHeaderState extends State { double _turns = 0; @override @@ -35,7 +37,7 @@ class _MobilePersonalFolderHeaderState Expanded( child: FlowyButton( text: FlowyText.semibold( - LocaleKeys.sideBar_personal.tr(), + widget.title, fontSize: 20.0, ), margin: const EdgeInsets.symmetric(vertical: 8), @@ -71,6 +73,7 @@ class _MobilePersonalFolderHeaderState SidebarRootViewsEvent.createRootView( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), index: 0, + viewSection: ViewSectionPB.Private, ), ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart index f87ef8645f..64e3e8824d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart @@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notifi import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart'; @@ -80,15 +80,15 @@ class _NotificationScreenContent extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => SidebarRootViewsBloc() + create: (_) => SidebarSectionsBloc() ..add( - SidebarRootViewsEvent.initial( + SidebarSectionsEvent.initial( userProfile, workspaceSetting.workspaceId, ), ), - child: BlocBuilder( - builder: (context, menuState) => + child: BlocBuilder( + builder: (context, sectionState) => BlocBuilder( builder: (context, filterState) => BlocBuilder( @@ -122,7 +122,7 @@ class _NotificationScreenContent extends StatelessWidget { NotificationsView( shownReminders: pastReminders, reminderBloc: reminderBloc, - views: menuState.views, + views: sectionState.section.publicViews, onAction: _onAction, onDelete: _onDelete, onReadChanged: _onReadChanged, @@ -134,7 +134,7 @@ class _NotificationScreenContent extends StatelessWidget { NotificationsView( shownReminders: upcomingReminders, reminderBloc: reminderBloc, - views: menuState.views, + views: sectionState.section.publicViews, isUpcoming: true, onAction: _onAction, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart index 6f6c3adf49..c1ffc78e76 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart @@ -406,6 +406,7 @@ class _SingleMobileInnerViewItemState extends State { ViewEvent.createView( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layout, + section: widget.categoryType.toViewSectionPB, ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart index 4bcbbbaafe..37dbdfefe2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart @@ -6,6 +6,7 @@ import 'package:appflowy/plugins/document/application/editor_transaction_adapter import 'package:appflowy/plugins/trash/application/trash_service.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/util/json_print.dart'; import 'package:appflowy/workspace/application/doc/doc_listener.dart'; import 'package:appflowy/workspace/application/doc/sync_state_listener.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; @@ -81,30 +82,24 @@ class DocumentBloc extends Bloc { final editorState = await _fetchDocumentState(); _onViewChanged(); _onDocumentChanged(); - await editorState.fold( + final newState = await editorState.fold( (s) async { - final result = await getIt().getUser(); - final userProfilePB = result.fold( - (s) => s, - (e) => null, - ); - emit( - state.copyWith( - error: null, - editorState: s, - isLoading: false, - userProfilePB: userProfilePB, - ), + final userProfilePB = + await getIt().getUser().toNullable(); + return state.copyWith( + error: null, + editorState: s, + isLoading: false, + userProfilePB: userProfilePB, ); }, - (f) async => emit( - state.copyWith( - error: f, - editorState: null, - isLoading: false, - ), + (f) async => state.copyWith( + error: f, + editorState: null, + isLoading: false, ), ); + emit(newState); }, moveToTrash: () async { emit(state.copyWith(isDeleted: true)); @@ -242,21 +237,20 @@ class DocumentBloc extends Bloc { } void syncDocumentDataPB(DocEventPB docEvent) { - // prettyPrintJson(docEvent.toProto3Json()); - // todo: integrate the document change to the editor - // for (final event in docEvent.events) { - // for (final blockEvent in event.event) { - // switch (blockEvent.command) { - // case DeltaTypePB.Inserted: - // break; - // case DeltaTypePB.Updated: - // break; - // case DeltaTypePB.Removed: - // break; - // default: - // } - // } - // } + prettyPrintJson(docEvent.toProto3Json()); + for (final event in docEvent.events) { + for (final blockEvent in event.event) { + switch (blockEvent.command) { + case DeltaTypePB.Inserted: + break; + case DeltaTypePB.Updated: + break; + case DeltaTypePB.Removed: + break; + default: + } + } + } } } diff --git a/frontend/appflowy_flutter/lib/shared/feature_flags.dart b/frontend/appflowy_flutter/lib/shared/feature_flags.dart index 83a2a341a0..91914c679e 100644 --- a/frontend/appflowy_flutter/lib/shared/feature_flags.dart +++ b/frontend/appflowy_flutter/lib/shared/feature_flags.dart @@ -1,9 +1,9 @@ -import 'dart:collection'; import 'dart:convert'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:collection/collection.dart'; typedef FeatureFlagMap = Map; @@ -19,16 +19,22 @@ enum FeatureFlag { // used to control the visibility of the members settings // if it's on, you can see the members settings in the settings page - membersSettings; + membersSettings, + + // used for ignore the conflicted feature flag + unknown; static Future initialize() async { final values = await getIt().getWithFormat( KVKeys.featureFlag, (value) => Map.from(jsonDecode(value)).map( - (key, value) => MapEntry( - FeatureFlag.values.firstWhere((e) => e.name == key), - value as bool, - ), + (key, value) { + final k = FeatureFlag.values.firstWhereOrNull( + (e) => e.name == key, + ) ?? + FeatureFlag.unknown; + return MapEntry(k, value as bool); + }, ), ) ?? {}; @@ -76,6 +82,8 @@ enum FeatureFlag { return false; case FeatureFlag.membersSettings: return false; + case FeatureFlag.unknown: + return false; } } @@ -85,6 +93,8 @@ enum FeatureFlag { return 'if it\'s on, you can see the workspace list and the workspace settings in the top-left corner of the app'; case FeatureFlag.membersSettings: return 'if it\'s on, you can see the members settings in the settings page'; + case FeatureFlag.unknown: + return ''; } } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index c250125c65..5c07e11af6 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -150,4 +150,44 @@ class UserBackendService { ..newIcon = icon; return UserEventChangeWorkspaceIcon(request).send(); } + + Future> + getWorkspaceMembers( + String workspaceId, + ) async { + final data = QueryWorkspacePB()..workspaceId = workspaceId; + return UserEventGetWorkspaceMember(data).send(); + } + + Future> addWorkspaceMember( + String workspaceId, + String email, + ) async { + final data = AddWorkspaceMemberPB() + ..workspaceId = workspaceId + ..email = email; + return UserEventAddWorkspaceMember(data).send(); + } + + Future> removeWorkspaceMember( + String workspaceId, + String email, + ) async { + final data = RemoveWorkspaceMemberPB() + ..workspaceId = workspaceId + ..email = email; + return UserEventRemoveWorkspaceMember(data).send(); + } + + Future> updateWorkspaceMember( + String workspaceId, + String email, + AFRolePB role, + ) async { + final data = UpdateWorkspaceMemberPB() + ..workspaceId = workspaceId + ..email = email + ..role = role; + return UserEventUpdateWorkspaceMember(data).send(); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_root_views_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_root_views_bloc.dart index 1ad50401b7..8aa73d5b22 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_root_views_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_root_views_bloc.dart @@ -33,18 +33,19 @@ class SidebarRootViewsBloc await event.when( initial: (userProfile, workspaceId) async { _initial(userProfile, workspaceId); - await _fetchApps(emit); + await _fetchRootViews(emit); }, reset: (userProfile, workspaceId) async { await _listener?.stop(); _initial(userProfile, workspaceId); - await _fetchApps(emit); + await _fetchRootViews(emit); }, - createRootView: (name, desc, index) async { - final result = await _workspaceService.createApp( + createRootView: (name, desc, index, section) async { + final result = await _workspaceService.createView( name: name, desc: desc, index: index, + viewSection: section, ); result.fold( (view) => emit(state.copyWith(lastCreatedRootView: view)), @@ -59,48 +60,59 @@ class SidebarRootViewsBloc ); }, didReceiveViews: (viewsOrFailure) async { - emit( - viewsOrFailure.fold( - (views) => state.copyWith( - views: views, - successOrFailure: FlowyResult.success(null), - ), - (err) => - state.copyWith(successOrFailure: FlowyResult.failure(err)), - ), - ); + // emit( + // viewsOrFailure.fold( + // (views) => state.copyWith( + // views: views, + // successOrFailure: FlowyResult.success(null), + // ), + // (err) => + // state.copyWith(successOrFailure: FlowyResult.failure(err)), + // ), + // ); }, moveRootView: (int fromIndex, int toIndex) { - if (state.views.length > fromIndex) { - final view = state.views[fromIndex]; + // if (state.views.length > fromIndex) { + // final view = state.views[fromIndex]; - _workspaceService.moveApp( - appId: view.id, - fromIndex: fromIndex, - toIndex: toIndex, - ); + // _workspaceService.moveApp( + // appId: view.id, + // fromIndex: fromIndex, + // toIndex: toIndex, + // ); - final views = List.from(state.views); - views.insert(toIndex, views.removeAt(fromIndex)); - emit(state.copyWith(views: views)); - } + // final views = List.from(state.views); + // views.insert(toIndex, views.removeAt(fromIndex)); + // emit(state.copyWith(views: views)); + // } }, ); }, ); } - Future _fetchApps(Emitter emit) async { - final viewsOrError = await _workspaceService.getViews(); - emit( - viewsOrError.fold( - (views) => state.copyWith(views: views), - (error) { - Log.error(error); - return state.copyWith(successOrFailure: FlowyResult.failure(error)); - }, - ), - ); + Future _fetchRootViews( + Emitter emit, + ) async { + try { + final publicViews = await _workspaceService.getPublicViews().getOrThrow(); + final privateViews = + await _workspaceService.getPrivateViews().getOrThrow(); + emit( + state.copyWith( + publicViews: publicViews, + privateViews: privateViews, + ), + ); + } catch (e) { + Log.error(e); + // TODO: handle error + // emit( + // state.copyWith( + // successOrFailure: FlowyResult.failure(e), + // ), + // ); + } } void _handleAppsOrFail(FlowyResult, FlowyError> viewsOrFail) { @@ -137,9 +149,12 @@ class SidebarRootViewsEvent with _$SidebarRootViewsEvent { String name, { String? desc, int? index, + required ViewSectionPB viewSection, }) = _createRootView; - const factory SidebarRootViewsEvent.moveRootView(int fromIndex, int toIndex) = - _MoveRootView; + const factory SidebarRootViewsEvent.moveRootView( + int fromIndex, + int toIndex, + ) = _MoveRootView; const factory SidebarRootViewsEvent.didReceiveViews( FlowyResult, FlowyError> appsOrFail, ) = _ReceiveApps; @@ -148,13 +163,13 @@ class SidebarRootViewsEvent with _$SidebarRootViewsEvent { @freezed class SidebarRootViewState with _$SidebarRootViewState { const factory SidebarRootViewState({ - required List views, + @Default([]) List privateViews, + @Default([]) List publicViews, required FlowyResult successOrFailure, @Default(null) ViewPB? lastCreatedRootView, }) = _SidebarRootViewState; factory SidebarRootViewState.initial() => SidebarRootViewState( - views: [], successOrFailure: FlowyResult.success(null), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart new file mode 100644 index 0000000000..8f3e4d1f59 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart @@ -0,0 +1,261 @@ +import 'dart:async'; + +import 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart'; +import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sidebar_sections_bloc.freezed.dart'; + +class SidebarSection { + const SidebarSection({ + required this.publicViews, + required this.privateViews, + }); + + const SidebarSection.empty() + : publicViews = const [], + privateViews = const []; + + final List publicViews; + final List privateViews; + + List get views => publicViews + privateViews; + + SidebarSection copyWith({ + List? publicViews, + List? privateViews, + }) { + return SidebarSection( + publicViews: publicViews ?? this.publicViews, + privateViews: privateViews ?? this.privateViews, + ); + } +} + +/// The [SidebarSectionsBloc] is responsible for +/// managing the root views in different sections of the workspace. +class SidebarSectionsBloc + extends Bloc { + SidebarSectionsBloc() : super(SidebarSectionsState.initial()) { + on( + (event, emit) async { + await event.when( + initial: (userProfile, workspaceId) async { + _initial(userProfile, workspaceId); + final sectionViews = await _getSectionViews(); + if (sectionViews != null) { + emit( + state.copyWith( + section: sectionViews, + ), + ); + } + }, + reset: (userProfile, workspaceId) async { + _reset(userProfile, workspaceId); + final sectionViews = await _getSectionViews(); + if (sectionViews != null) { + emit( + state.copyWith( + section: sectionViews, + ), + ); + } + }, + createRootViewInSection: (name, section, desc, index) async { + final result = await _workspaceService.createView( + name: name, + viewSection: section, + desc: desc, + index: index, + ); + result.fold( + (view) => emit( + state.copyWith( + lastCreatedRootView: view, + createRootViewResult: FlowyResult.success(null), + ), + ), + (error) { + Log.error('Failed to create root view: $error'); + emit( + state.copyWith( + createRootViewResult: FlowyResult.failure(error), + ), + ); + }, + ); + }, + receiveSectionViewsUpdate: (sectionViews) async { + final section = sectionViews.section; + switch (section) { + case ViewSectionPB.Public: + emit( + state.copyWith( + section: state.section.copyWith( + publicViews: sectionViews.views, + ), + ), + ); + case ViewSectionPB.Private: + emit( + state.copyWith( + section: state.section.copyWith( + privateViews: sectionViews.views, + ), + ), + ); + break; + default: + break; + } + }, + moveRootView: (fromIndex, toIndex, fromSection, toSection) async { + final views = fromSection == ViewSectionPB.Public + ? List.from(state.section.publicViews) + : List.from(state.section.privateViews); + if (fromIndex < 0 || fromIndex >= views.length) { + Log.error( + 'Invalid fromIndex: $fromIndex, maxIndex: ${views.length - 1}', + ); + return; + } + final view = views[fromIndex]; + final result = await _workspaceService.moveView( + viewId: view.id, + fromIndex: fromIndex, + toIndex: toIndex, + ); + result.fold( + (value) { + views.insert(toIndex, views.removeAt(fromIndex)); + var newState = state; + if (fromSection == ViewSectionPB.Public) { + newState = newState.copyWith( + section: newState.section.copyWith(publicViews: views), + ); + } else if (fromSection == ViewSectionPB.Private) { + newState = newState.copyWith( + section: newState.section.copyWith(privateViews: views), + ); + } + emit(newState); + }, + (error) { + Log.error('Failed to move root view: $error'); + }, + ); + }, + ); + }, + ); + } + + late WorkspaceService _workspaceService; + WorkspaceSectionsListener? _listener; + + @override + Future close() async { + await _listener?.stop(); + _listener = null; + return super.close(); + } + + ViewSectionPB? getViewSection(ViewPB view) { + final publicViews = state.section.publicViews.map((e) => e.id); + final privateViews = state.section.privateViews.map((e) => e.id); + if (publicViews.contains(view.id)) { + return ViewSectionPB.Public; + } else if (privateViews.contains(view.id)) { + return ViewSectionPB.Private; + } else { + return null; + } + } + + Future _getSectionViews() async { + try { + final publicViews = await _workspaceService.getPublicViews().getOrThrow(); + final privateViews = + await _workspaceService.getPrivateViews().getOrThrow(); + return SidebarSection( + publicViews: publicViews, + privateViews: privateViews, + ); + } catch (e) { + Log.error('Failed to get section views: $e'); + return null; + } + } + + void _initial(UserProfilePB userProfile, String workspaceId) { + _workspaceService = WorkspaceService(workspaceId: workspaceId); + + _listener = WorkspaceSectionsListener( + user: userProfile, + workspaceId: workspaceId, + )..start( + sectionChanged: (result) { + if (!isClosed) { + result.fold( + (s) => add(SidebarSectionsEvent.receiveSectionViewsUpdate(s)), + (f) => Log.error('Failed to receive section views: $f'), + ); + } + }, + ); + } + + void _reset(UserProfilePB userProfile, String workspaceId) { + _listener?.stop(); + _listener = null; + + _initial(userProfile, workspaceId); + } +} + +@freezed +class SidebarSectionsEvent with _$SidebarSectionsEvent { + const factory SidebarSectionsEvent.initial( + UserProfilePB userProfile, + String workspaceId, + ) = _Initial; + const factory SidebarSectionsEvent.reset( + UserProfilePB userProfile, + String workspaceId, + ) = _Reset; + const factory SidebarSectionsEvent.createRootViewInSection({ + required String name, + required ViewSectionPB viewSection, + String? desc, + int? index, + }) = _CreateRootViewInSection; + const factory SidebarSectionsEvent.moveRootView({ + required int fromIndex, + required int toIndex, + required ViewSectionPB fromSection, + required ViewSectionPB toSection, + }) = _MoveRootView; + const factory SidebarSectionsEvent.receiveSectionViewsUpdate( + SectionViewsPB sectionViews, + ) = _ReceiveSectionViewsUpdate; +} + +@freezed +class SidebarSectionsState with _$SidebarSectionsState { + const factory SidebarSectionsState({ + required SidebarSection section, + @Default(null) ViewPB? lastCreatedRootView, + FlowyResult? createRootViewResult, + }) = _SidebarSectionsState; + + factory SidebarSectionsState.initial() => const SidebarSectionsState( + section: SidebarSection.empty(), + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart index 51891a0be9..e82b54241a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -10,7 +11,19 @@ part 'folder_bloc.freezed.dart'; enum FolderCategoryType { favorite, - personal, + private, + public; + + ViewSectionPB get toViewSectionPB { + switch (this) { + case FolderCategoryType.private: + return ViewSectionPB.Private; + case FolderCategoryType.public: + return ViewSectionPB.Public; + case FolderCategoryType.favorite: + throw UnimplementedError(); + } + } } class FolderBloc extends Bloc { diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 3aa8f0ba88..80d7e86e59 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -2,7 +2,7 @@ import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -20,14 +20,20 @@ class UserWorkspaceBloc extends Bloc { (event, emit) async { await event.when( initial: () async { - // do nothing + add(const FetchWorkspaces()); }, workspacesReceived: (workspaceId) async {}, fetchWorkspaces: () async { final result = await _fetchWorkspaces(); if (result != null) { + final members = await _userService + .getWorkspaceMembers( + result.$1.workspaceId, + ) + .fold((s) => s.items.length, (f) => -1); emit( state.copyWith( + isCollaborativeWorkspace: members > 1, currentWorkspace: result.$1, workspaces: result.$2, ), @@ -258,7 +264,7 @@ class UserWorkspaceBloc extends Bloc { workspaces.firstWhere((e) => e.workspaceId == currentWorkspace.id); return (currentWorkspaceInList, workspaces); } catch (e) { - Log.error(e); + Log.error('fetch workspace error: $e'); return null; } } @@ -292,6 +298,7 @@ class UserWorkspaceState with _$UserWorkspaceState { const factory UserWorkspaceState({ required UserWorkspacePB? currentWorkspace, required List workspaces, + @Default(false) bool isCollaborativeWorkspace, @Default(null) FlowyResult? createWorkspaceResult, @Default(null) FlowyResult? deleteWorkspaceResult, @Default(null) FlowyResult? openWorkspaceResult, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index a6df92fa23..8cfa6a2014 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -165,6 +165,8 @@ class ViewBloc extends Bloc { viewId: value.from.id, newParentId: value.newParentId, prevViewId: value.prevId, + fromSection: value.fromSection, + toSection: value.toSection, ); emit( result.fold( @@ -184,8 +186,8 @@ class ViewBloc extends Bloc { layoutType: e.layoutType, ext: {}, openAfterCreate: e.openAfterCreated, + section: e.section, ); - emit( result.fold( (view) => state.copyWith( @@ -353,12 +355,15 @@ class ViewEvent with _$ViewEvent { ViewPB from, String newParentId, String? prevId, + ViewSectionPB? fromSection, + ViewSectionPB? toSection, ) = Move; const factory ViewEvent.createView( String name, ViewLayoutPB layoutType, { /// open the view after created @Default(true) bool openAfterCreated, + required ViewSectionPB section, }) = CreateView; const factory ViewEvent.viewDidUpdate( FlowyResult result, 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 893a11d9b1..a8ffc0516e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -37,6 +37,7 @@ class ViewBackendService { /// The [index] is the index of the view in the parent view. /// If the index is null, the view will be added to the end of the list. int? index, + ViewSectionPB? section, }) { final payload = CreateViewPayloadPB.create() ..parentViewId = parentViewId @@ -58,6 +59,10 @@ class ViewBackendService { payload.index = index; } + if (section != null) { + payload.section = section; + } + return FolderEventCreateView(payload).send(); } @@ -195,11 +200,15 @@ class ViewBackendService { required String viewId, required String newParentId, required String? prevViewId, + ViewSectionPB? fromSection, + ViewSectionPB? toSection, }) { final payload = MoveNestedViewPayloadPB( viewId: viewId, newParentId: newParentId, prevViewId: prevViewId, + fromSection: fromSection, + toSection: toSection, ); return FolderEventMoveNestedView(payload).send(); diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart index 4c5ba13ec0..fb3beb4dd5 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart @@ -11,23 +11,28 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/notifier.dart'; -typedef AppListNotifyValue = FlowyResult, FlowyError>; +typedef RootViewsNotifyValue = FlowyResult, FlowyError>; typedef WorkspaceNotifyValue = FlowyResult; +/// The [WorkspaceListener] listens to the changes including the below: +/// +/// - The root views of the workspace. (Not including the views are inside the root views) +/// - The workspace itself. class WorkspaceListener { WorkspaceListener({required this.user, required this.workspaceId}); final UserProfilePB user; final String workspaceId; - PublishNotifier? _appsChangedNotifier = PublishNotifier(); + PublishNotifier? _appsChangedNotifier = + PublishNotifier(); PublishNotifier? _workspaceUpdatedNotifier = PublishNotifier(); FolderNotificationListener? _listener; void start({ - void Function(AppListNotifyValue)? appsChanged, + void Function(RootViewsNotifyValue)? appsChanged, void Function(WorkspaceNotifyValue)? onWorkspaceUpdated, }) { if (appsChanged != null) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart new file mode 100644 index 0000000000..73c2a9045f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart @@ -0,0 +1,68 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/folder_notification.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flowy_infra/notifier.dart'; + +typedef SectionNotifyValue = FlowyResult; + +/// The [WorkspaceSectionsListener] listens to the changes including the below: +/// +/// - The root views inside different section of the workspace. (Not including the views are inside the root views) +/// depends on the section type(s). +class WorkspaceSectionsListener { + WorkspaceSectionsListener({ + required this.user, + required this.workspaceId, + }); + + final UserProfilePB user; + final String workspaceId; + + final _sectionNotifier = PublishNotifier(); + late final FolderNotificationListener _listener; + + void start({ + void Function(SectionNotifyValue)? sectionChanged, + }) { + if (sectionChanged != null) { + _sectionNotifier.addPublishListener(sectionChanged); + } + + _listener = FolderNotificationListener( + objectId: workspaceId, + handler: _handleObservableType, + ); + } + + void _handleObservableType( + FolderNotification ty, + FlowyResult result, + ) { + switch (ty) { + case FolderNotification.DidUpdateSectionViews: + final FlowyResult value = result.fold( + (s) => FlowyResult.success( + SectionViewsPB.fromBuffer(s), + ), + (f) => FlowyResult.failure(f), + ); + _sectionNotifier.value = value; + break; + default: + break; + } + } + + Future stop() async { + _sectionNotifier.dispose(); + + await _listener.stop(); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart index 3c29c9a4c1..6e42b744f6 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart' - show CreateViewPayloadPB, MoveViewPayloadPB, ViewLayoutPB, ViewPB; -import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; class WorkspaceService { @@ -12,15 +10,18 @@ class WorkspaceService { final String workspaceId; - Future> createApp({ + Future> createView({ required String name, + required ViewSectionPB viewSection, String? desc, int? index, }) { final payload = CreateViewPayloadPB.create() ..parentViewId = workspaceId ..name = name - ..layout = ViewLayoutPB.Document; + // only allow document layout for the top-level views + ..layout = ViewLayoutPB.Document + ..section = viewSection; if (desc != null) { payload.desc = desc; @@ -37,8 +38,8 @@ class WorkspaceService { return FolderEventReadCurrentWorkspace().send(); } - Future, FlowyError>> getViews() { - final payload = WorkspaceIdPB.create()..value = workspaceId; + Future, FlowyError>> getPublicViews() { + final payload = GetWorkspaceViewPB.create()..value = workspaceId; return FolderEventReadWorkspaceViews(payload).send().then((result) { return result.fold( (views) => FlowyResult.success(views.items), @@ -47,13 +48,23 @@ class WorkspaceService { }); } - Future> moveApp({ - required String appId, + Future, FlowyError>> getPrivateViews() { + final payload = GetWorkspaceViewPB.create()..value = workspaceId; + return FolderEventReadPrivateViews(payload).send().then((result) { + return result.fold( + (views) => FlowyResult.success(views.items), + (error) => FlowyResult.failure(error), + ); + }); + } + + Future> moveView({ + required String viewId, required int fromIndex, required int toIndex, }) { final payload = MoveViewPayloadPB.create() - ..viewId = appId + ..viewId = viewId ..from = fromIndex ..to = toIndex; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart similarity index 100% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart new file mode 100644 index 0000000000..00f88e153d --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart @@ -0,0 +1,63 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class FolderHeader extends StatefulWidget { + const FolderHeader({ + super.key, + required this.title, + required this.expandButtonTooltip, + required this.addButtonTooltip, + required this.onPressed, + required this.onAdded, + }); + + final String title; + final String expandButtonTooltip; + final String addButtonTooltip; + final VoidCallback onPressed; + final VoidCallback onAdded; + + @override + State createState() => _FolderHeaderState(); +} + +class _FolderHeaderState extends State { + bool onHover = false; + + @override + Widget build(BuildContext context) { + const iconSize = 26.0; + const textPadding = 4.0; + return MouseRegion( + onEnter: (event) => setState(() => onHover = true), + onExit: (event) => setState(() => onHover = false), + child: Row( + children: [ + FlowyTextButton( + widget.title, + tooltip: widget.expandButtonTooltip, + constraints: const BoxConstraints( + minHeight: iconSize + textPadding * 2, + ), + padding: const EdgeInsets.all(textPadding), + fillColor: Colors.transparent, + onPressed: widget.onPressed, + ), + if (onHover) ...[ + const Spacer(), + FlowyIconButton( + tooltipText: widget.addButtonTooltip, + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + iconPadding: const EdgeInsets.all(2), + height: iconSize, + width: iconSize, + icon: const FlowySvg(FlowySvgs.add_s), + onPressed: widget.onAdded, + ), + ], + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart new file mode 100644 index 0000000000..2cf57a6d08 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart @@ -0,0 +1,116 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SectionFolder extends StatelessWidget { + const SectionFolder({ + super.key, + required this.title, + required this.categoryType, + required this.views, + this.isHoverEnabled = true, + }); + + final String title; + final FolderCategoryType categoryType; + final List views; + final bool isHoverEnabled; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => FolderBloc(type: categoryType) + ..add( + const FolderEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + FolderHeader( + title: title, + expandButtonTooltip: expandButtonTooltip, + addButtonTooltip: addButtonTooltip, + onPressed: () => context + .read() + .add(const FolderEvent.expandOrUnExpand()), + onAdded: () { + createViewAndShowRenameDialogIfNeeded( + context, + LocaleKeys.newPageText.tr(), + (viewName, _) { + if (viewName.isNotEmpty) { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + index: 0, + viewSection: categoryType.toViewSectionPB, + ), + ); + + context.read().add( + const FolderEvent.expandOrUnExpand( + isExpanded: true, + ), + ); + } + }, + ); + }, + ), + if (state.isExpanded) + ...views.map( + (view) => ViewItem( + key: ValueKey( + '${categoryType.name} ${view.id}', + ), + categoryType: categoryType, + isFirstChild: view.id == views.first.id, + view: view, + level: 0, + leftPadding: 16, + isFeedback: false, + onSelected: (view) { + if (HardwareKeyboard.instance.isControlPressed) { + context.read().openTab(view); + } + + context.read().openPlugin(view); + }, + onTertiarySelected: (view) => + context.read().openTab(view), + isHoverEnabled: isHoverEnabled, + ), + ), + ], + ); + }, + ), + ); + } + + String get expandButtonTooltip { + return switch (categoryType) { + FolderCategoryType.public => LocaleKeys.sideBar_clickToHidePublic.tr(), + FolderCategoryType.private => LocaleKeys.sideBar_clickToHidePrivate.tr(), + _ => '', + }; + } + + String get addButtonTooltip { + return switch (categoryType) { + FolderCategoryType.public => LocaleKeys.sideBar_addAPageToPublic.tr(), + FolderCategoryType.private => LocaleKeys.sideBar_addAPageToPrivate.tr(), + _ => '', + }; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart deleted file mode 100644 index ec86203599..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class PersonalFolder extends StatelessWidget { - const PersonalFolder({ - super.key, - required this.views, - this.isHoverEnabled = true, - }); - - final List views; - final bool isHoverEnabled; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => FolderBloc(type: FolderCategoryType.personal) - ..add( - const FolderEvent.initial(), - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - PersonalFolderHeader( - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () => context - .read() - .add(const FolderEvent.expandOrUnExpand(isExpanded: true)), - ), - if (state.isExpanded) - ...views.map( - (view) => ViewItem( - key: ValueKey( - '${FolderCategoryType.personal.name} ${view.id}', - ), - categoryType: FolderCategoryType.personal, - isFirstChild: view.id == views.first.id, - view: view, - level: 0, - leftPadding: 16, - isFeedback: false, - onSelected: (view) { - if (HardwareKeyboard.instance.isControlPressed) { - context.read().openTab(view); - } - - context.read().openPlugin(view); - }, - onTertiarySelected: (view) => - context.read().openTab(view), - isHoverEnabled: isHoverEnabled, - ), - ), - ], - ); - }, - ), - ); - } -} - -class PersonalFolderHeader extends StatefulWidget { - const PersonalFolderHeader({ - super.key, - required this.onPressed, - required this.onAdded, - }); - - final VoidCallback onPressed; - final VoidCallback onAdded; - - @override - State createState() => _PersonalFolderHeaderState(); -} - -class _PersonalFolderHeaderState extends State { - bool onHover = false; - - @override - Widget build(BuildContext context) { - const iconSize = 26.0; - const textPadding = 4.0; - return MouseRegion( - onEnter: (event) => setState(() => onHover = true), - onExit: (event) => setState(() => onHover = false), - child: Row( - children: [ - FlowyTextButton( - LocaleKeys.sideBar_personal.tr(), - tooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(), - constraints: const BoxConstraints( - minHeight: iconSize + textPadding * 2, - ), - padding: const EdgeInsets.all(textPadding), - fillColor: Colors.transparent, - onPressed: widget.onPressed, - ), - if (onHover) ...[ - const Spacer(), - FlowyIconButton( - tooltipText: LocaleKeys.sideBar_addAPage.tr(), - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - iconPadding: const EdgeInsets.all(2), - height: iconSize, - width: iconSize, - icon: const FlowySvg(FlowySvgs.add_s), - onPressed: () { - createViewAndShowRenameDialogIfNeeded( - context, - LocaleKeys.newPageText.tr(), - (viewName, _) { - if (viewName.isNotEmpty) { - context.read().add( - SidebarRootViewsEvent.createRootView( - viewName, - index: 0, - ), - ); - - widget.onAdded(); - } - }, - ); - }, - ), - ], - ], - ), - ); - } -} 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 e02a5b0c74..8e845e1f8c 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 @@ -2,8 +2,7 @@ import 'dart:async'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.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'; @@ -15,8 +14,8 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_me import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_trash.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -31,7 +30,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; /// - settings /// - scrollable document list /// - trash -class HomeSideBar extends StatefulWidget { +class HomeSideBar extends StatelessWidget { const HomeSideBar({ super.key, required this.userProfile, @@ -42,49 +41,30 @@ class HomeSideBar extends StatefulWidget { final WorkspaceSettingPB workspaceSetting; - @override - State createState() => _HomeSideBarState(); -} - -class _HomeSideBarState extends State { - final _scrollController = ScrollController(); - Timer? _srollDebounce; - bool isScrolling = false; - - @override - void initState() { - super.initState(); - _scrollController.addListener(_onScrollChanged); - } - - void _onScrollChanged() { - setState(() => isScrolling = true); - - _srollDebounce?.cancel(); - _srollDebounce = - Timer(const Duration(milliseconds: 300), _setScrollStopped); - } - - void _setScrollStopped() { - if (mounted) { - setState(() => isScrolling = false); - } - } - - @override - void dispose() { - _srollDebounce?.cancel(); - _scrollController.removeListener(_onScrollChanged); - _scrollController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { + // Workspace Bloc: control the current workspace + // | + // +-- Workspace Menu + // | | + // | +-- Workspace List: control to switch workspace + // | | + // | +-- Workspace Settings + // | | + // | +-- Notification Center + // | + // +-- Favorite Section + // | + // +-- Public Or Private Section: control the sections of the workspace + // | + // +-- Trash Section return BlocProvider( - create: (_) => UserWorkspaceBloc(userProfile: widget.userProfile) - ..add(const UserWorkspaceEvent.fetchWorkspaces()), + create: (_) => UserWorkspaceBloc(userProfile: userProfile) + ..add( + const UserWorkspaceEvent.initial(), + ), child: BlocBuilder( + // Rebuild the whole sidebar when the current workspace changes buildWhen: (previous, current) => previous.currentWorkspace?.workspaceId != current.currentWorkspace?.workspaceId, @@ -95,19 +75,19 @@ class _HomeSideBarState extends State { create: (_) => getIt(), ), BlocProvider( - create: (_) => SidebarRootViewsBloc() + create: (_) => SidebarSectionsBloc() ..add( - SidebarRootViewsEvent.initial( - widget.userProfile, + SidebarSectionsEvent.initial( + userProfile, state.currentWorkspace?.workspaceId ?? - widget.workspaceSetting.workspaceId, + workspaceSetting.workspaceId, ), ), ), ], child: MultiBlocListener( listeners: [ - BlocListener( + BlocListener( listenWhen: (p, c) => p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, listener: (context, state) => context.read().add( @@ -122,28 +102,17 @@ class _HomeSideBarState extends State { ), BlocListener( listener: (context, state) { - context.read().add( - SidebarRootViewsEvent.reset( - widget.userProfile, + context.read().add( + SidebarSectionsEvent.initial( + userProfile, state.currentWorkspace?.workspaceId ?? - widget.workspaceSetting.workspaceId, + workspaceSetting.workspaceId, ), ); }, ), ], - child: Builder( - builder: (context) { - final menuState = context.watch().state; - final favoriteState = context.watch().state; - - return _buildSidebar( - context, - menuState.views, - favoriteState.views, - ); - }, - ), + child: _Sidebar(userProfile: userProfile), ), ); }, @@ -151,71 +120,6 @@ class _HomeSideBarState extends State { ); } - Widget _buildSidebar( - BuildContext context, - List views, - List favoriteViews, - ) { - const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 12); - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, - border: Border( - right: BorderSide(color: Theme.of(context).dividerColor), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // top menu - const Padding( - padding: menuHorizontalInset, - child: SidebarTopMenu(), - ), - // user or workspace, setting - Padding( - padding: menuHorizontalInset, - child: FeatureFlag.collaborativeWorkspace.isOn - ? SidebarWorkspace( - userProfile: widget.userProfile, - views: views, - ) - : SidebarUser( - userProfile: widget.userProfile, - views: views, - ), - ), - - const VSpace(20), - // scrollable document list - Expanded( - child: Padding( - padding: menuHorizontalInset, - child: SingleChildScrollView( - controller: _scrollController, - physics: const ClampingScrollPhysics(), - child: SidebarFolder( - views: views, - favoriteViews: favoriteViews, - isHoverEnabled: !isScrolling, - ), - ), - ), - ), - const VSpace(10), - // trash - const Padding( - padding: menuHorizontalInset, - child: SidebarTrashButton(), - ), - const VSpace(10), - // new page button - const SidebarNewPageButton(), - ], - ), - ); - } - void _onNotificationAction( BuildContext context, NotificationActionState state, @@ -224,9 +128,10 @@ class _HomeSideBarState extends State { if (action != null) { if (action.type == ActionType.openView) { final view = context - .read() + .read() .state - .views + .section + .publicViews .findView(action.objectId); if (view != null) { @@ -250,3 +155,108 @@ class _HomeSideBarState extends State { } } } + +class _Sidebar extends StatefulWidget { + const _Sidebar({ + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + State<_Sidebar> createState() => _SidebarState(); +} + +class _SidebarState extends State<_Sidebar> { + final _scrollController = ScrollController(); + Timer? _scrollDebounce; + bool isScrolling = false; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScrollChanged); + } + + @override + void dispose() { + _scrollDebounce?.cancel(); + _scrollController.removeListener(_onScrollChanged); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 12); + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + border: Border( + right: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // top menu + const Padding( + padding: menuHorizontalInset, + child: SidebarTopMenu(), + ), + // user or workspace, setting + Padding( + padding: menuHorizontalInset, + child: widget.userProfile.authenticator != AuthenticatorPB.Local && + FeatureFlag.collaborativeWorkspace.isOn + ? SidebarWorkspace( + userProfile: widget.userProfile, + ) + : SidebarUser( + userProfile: widget.userProfile, + ), + ), + + const VSpace(20), + // scrollable document list + Expanded( + child: Padding( + padding: menuHorizontalInset, + child: SingleChildScrollView( + controller: _scrollController, + physics: const ClampingScrollPhysics(), + child: SidebarFolder( + userProfile: widget.userProfile, + isHoverEnabled: !isScrolling, + ), + ), + ), + ), + const VSpace(10), + // trash + const Padding( + padding: menuHorizontalInset, + child: SidebarTrashButton(), + ), + const VSpace(10), + // new page button + const SidebarNewPageButton(), + ], + ), + ); + } + + void _onScrollChanged() { + setState(() => isScrolling = true); + + _scrollDebounce?.cancel(); + _scrollDebounce = + Timer(const Duration(milliseconds: 300), _setScrollStopped); + } + + void _setScrollStopped() { + if (mounted) { + setState(() => isScrolling = false); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart index 397a3e3d90..c6586f4b66 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart @@ -1,50 +1,118 @@ -import 'package:flutter/material.dart'; - +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarFolder extends StatelessWidget { const SidebarFolder({ super.key, - required this.views, - required this.favoriteViews, this.isHoverEnabled = true, + required this.userProfile, }); - final List views; - final List favoriteViews; final bool isHoverEnabled; + final UserProfilePB userProfile; @override Widget build(BuildContext context) { - // check if there is any duplicate views - final views = this.views.toSet().toList(); - final favoriteViews = this.favoriteViews.toSet().toList(); - assert(views.length == this.views.length); - assert(favoriteViews.length == favoriteViews.length); - return ValueListenableBuilder( valueListenable: getIt().notifier, builder: (context, value, child) { return Column( children: [ // favorite - if (favoriteViews.isNotEmpty) ...[ - FavoriteFolder( - // remove the duplicate views - views: favoriteViews, - ), - const VSpace(10), - ], - // personal - PersonalFolder(views: views, isHoverEnabled: isHoverEnabled), + BlocBuilder( + builder: (context, state) { + if (state.views.isEmpty) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: FavoriteFolder( + // remove the duplicate views + views: state.views, + ), + ); + }, + ), + // public or private + BlocBuilder( + builder: (context, state) { + // only show public and private section if the workspace is collaborative and not local + final isCollaborativeWorkspace = + userProfile.authenticator != AuthenticatorPB.Local && + FeatureFlag.collaborativeWorkspace.isOn; + + return Column( + children: + // only show public and private section if the workspace is collaborative + isCollaborativeWorkspace + ? [ + // public + const VSpace(10), + PublicSectionFolder( + views: state.section.publicViews, + ), + + // private + const VSpace(10), + PrivateSectionFolder( + views: state.section.privateViews, + ), + ] + : [ + // personal + const VSpace(10), + PersonalSectionFolder( + views: state.section.publicViews, + ), + ], + ); + }, + ), ], ); }, ); } } + +class PrivateSectionFolder extends SectionFolder { + PrivateSectionFolder({ + super.key, + required super.views, + }) : super( + title: LocaleKeys.sideBar_private.tr(), + categoryType: FolderCategoryType.private, + ); +} + +class PublicSectionFolder extends SectionFolder { + PublicSectionFolder({ + super.key, + required super.views, + }) : super( + title: LocaleKeys.sideBar_public.tr(), + categoryType: FolderCategoryType.public, + ); +} + +class PersonalSectionFolder extends SectionFolder { + PersonalSectionFolder({ + super.key, + required super.views, + }) : super( + title: LocaleKeys.sideBar_personal.tr(), + categoryType: FolderCategoryType.public, + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart index d5cd8a65ae..ea3633cdc4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart @@ -1,7 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.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/button.dart'; import 'package:flowy_infra_ui/style_widget/extension.dart'; @@ -25,9 +26,12 @@ class SidebarNewPageButton extends StatelessWidget { LocaleKeys.newPageText.tr(), (viewName, _) { if (viewName.isNotEmpty) { - context - .read() - .add(SidebarRootViewsEvent.createRootView(viewName)); + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + viewSection: ViewSectionPB.Public, + ), + ); } }, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart index 71c04cf048..e4d5f2fa3e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart @@ -4,7 +4,7 @@ import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; @@ -24,7 +24,7 @@ class SidebarTopMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { return SizedBox( height: HomeSizes.topBarHeight, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart index 473ca6f1d3..288bd76a74 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart @@ -3,7 +3,6 @@ import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:easy_localization/easy_localization.dart'; @@ -17,11 +16,9 @@ class SidebarUser extends StatelessWidget { const SidebarUser({ super.key, required this.userProfile, - required this.views, }); final UserProfilePB userProfile; - final List views; @override Widget build(BuildContext context) { @@ -37,13 +34,13 @@ class SidebarUser extends StatelessWidget { iconUrl: state.userProfile.iconUrl, name: state.userProfile.name, ), - const HSpace(4), + const HSpace(8), Expanded( child: _buildUserName(context, state), ), UserSettingButton(userProfile: state.userProfile), const HSpace(4), - NotificationButton(views: views), + const NotificationButton(), ], ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart index bf2bb16476..026d89ed66 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart @@ -6,7 +6,6 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sid import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -20,11 +19,9 @@ class SidebarWorkspace extends StatelessWidget { const SidebarWorkspace({ super.key, required this.userProfile, - required this.views, }); final UserProfilePB userProfile; - final List views; @override Widget build(BuildContext context) { @@ -46,7 +43,7 @@ class SidebarWorkspace extends StatelessWidget { ), UserSettingButton(userProfile: userProfile), const HSpace(4), - NotificationButton(views: views), + const NotificationButton(), ], ); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 5d44ec9df9..24a00bcc4b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -182,14 +182,14 @@ class WorkspaceMenuItem extends StatelessWidget { Widget _buildRightIcon(BuildContext context) { // only the owner can update or delete workspace. // only show the more action button when the workspace is selected. - if (!isSelected || - !context.read().state.myRole.isOwner) { + if (!isSelected) { return const SizedBox.shrink(); } return Row( children: [ - WorkspaceMoreActionList(workspace: workspace), + if (context.read().state.myRole.isOwner) + WorkspaceMoreActionList(workspace: workspace), const FlowySvg( FlowySvgs.blue_check_s, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart index 2fcf4ce098..910286f4b7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart'; @@ -188,6 +189,9 @@ class _DraggableViewItemState extends State { return; } + final fromSection = getViewSection(from); + final toSection = getViewSection(to); + switch (position) { case DraggableHoverPosition.top: context.read().add( @@ -195,6 +199,8 @@ class _DraggableViewItemState extends State { from, to.parentViewId, null, + fromSection, + toSection, ), ); break; @@ -204,6 +210,8 @@ class _DraggableViewItemState extends State { from, to.parentViewId, to.id, + fromSection, + toSection, ), ); break; @@ -213,6 +221,8 @@ class _DraggableViewItemState extends State { from, to.id, to.childViews.lastOrNull?.id, + fromSection, + toSection, ), ); break; @@ -251,6 +261,10 @@ class _DraggableViewItemState extends State { return true; } + + ViewSectionPB? getViewSection(ViewPB view) { + return context.read().getViewSection(view); + } } extension on ViewPB { 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 2cdc373181..19876b8eab 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 @@ -475,6 +475,7 @@ class _SingleInnerViewItemState extends State { viewName, pluginBuilder.layoutType!, openAfterCreated: openAfterCreated, + section: widget.categoryType.toViewSectionPB, ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart index 45ac056a4f..a7925dc3f7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart @@ -2,8 +2,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/notifications/notification_dialog.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/theme_extension.dart'; @@ -13,12 +13,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class NotificationButton extends StatelessWidget { - const NotificationButton({super.key, required this.views}); - - final List views; + const NotificationButton({ + super.key, + }); @override Widget build(BuildContext context) { + final views = context.watch().state.section.views; final mutex = PopoverMutex(); return BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart index aaa63c3b9b..742d90a5fd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart @@ -1,6 +1,8 @@ +import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -21,7 +23,8 @@ class WorkspaceMemberBloc WorkspaceMemberBloc({ required this.userProfile, this.workspace, - }) : super(WorkspaceMemberState.initial()) { + }) : _userBackendService = UserBackendService(userId: userProfile.id), + super(WorkspaceMemberState.initial()) { on((event, emit) async { await event.when( initial: () async { @@ -73,14 +76,16 @@ class WorkspaceMemberBloc final UserWorkspacePB? workspace; late final String workspaceId; + late final UserBackendService _userBackendService; Future> _getWorkspaceMembers() async { - final data = QueryWorkspacePB()..workspaceId = workspaceId; - final result = await UserEventGetWorkspaceMember(data).send(); - return result.fold((s) => s.items, (e) { - Log.error('Failed to read workspace members: $e'); - return []; - }); + return _userBackendService.getWorkspaceMembers(workspaceId).fold( + (s) => s.items, + (e) { + Log.error('Failed to read workspace members: $e'); + return []; + }, + ); } AFRolePB _getMyRole(List members) { @@ -97,40 +102,26 @@ class WorkspaceMemberBloc } Future _addWorkspaceMember(String email) async { - final data = AddWorkspaceMemberPB() - ..workspaceId = workspaceId - ..email = email; - final result = await UserEventAddWorkspaceMember(data).send(); - result.fold((s) { - Log.info('Added workspace member: $data'); - }, (e) { - Log.error('Failed to add workspace member: $e'); - }); + return _userBackendService.addWorkspaceMember(workspaceId, email).fold( + (s) => Log.debug('Added workspace member: $email'), + (e) => Log.error('Failed to add workspace member: $e'), + ); } Future _removeWorkspaceMember(String email) async { - final data = RemoveWorkspaceMemberPB() - ..workspaceId = workspaceId - ..email = email; - final result = await UserEventRemoveWorkspaceMember(data).send(); - result.fold((s) { - Log.info('Removed workspace member: $data'); - }, (e) { - Log.error('Failed to remove workspace member: $e'); - }); + return _userBackendService.removeWorkspaceMember(workspaceId, email).fold( + (s) => Log.debug('Removed workspace member: $email'), + (e) => Log.error('Failed to remove workspace member: $e'), + ); } Future _updateWorkspaceMember(String email, AFRolePB role) async { - final data = UpdateWorkspaceMemberPB() - ..workspaceId = workspaceId - ..email = email - ..role = role; - final result = await UserEventUpdateWorkspaceMember(data).send(); - result.fold((s) { - Log.info('Updated workspace member: $data'); - }, (e) { - Log.error('Failed to update workspace member: $e'); - }); + return _userBackendService + .updateWorkspaceMember(workspaceId, email, role) + .fold( + (s) => Log.debug('Updated workspace member: $email'), + (e) => Log.error('Failed to update workspace member: $e'), + ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart index 66ccbe01e0..bb2277bbf1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -50,7 +50,7 @@ class UserAvatar extends StatelessWidget { ), child: FlowyText.semibold( nameInitials, - color: Colors.white, + color: Colors.black, fontSize: isLarge ? nameInitials.length == initialsCount ? 20 diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart index 328aa03556..f226be66e2 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart @@ -8,6 +8,10 @@ extension FlowyAsyncResultExtension return then((result) => result.getOrElse(onFailure)); } + Future toNullable() { + return then((result) => result.toNullable()); + } + Future getOrThrow() { return then((result) => result.getOrThrow()); } diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart index ec7a357ee6..1027d2a719 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart @@ -38,8 +38,13 @@ void main() { final appBloc = ViewBloc(view: app)..add(const ViewEvent.initial()); assert(appBloc.state.lastCreatedView == null); - appBloc - .add(const ViewEvent.createView("New document", ViewLayoutPB.Document)); + appBloc.add( + const ViewEvent.createView( + "New document", + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), + ); await blocResponseFuture(); assert(appBloc.state.lastCreatedView != null); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart deleted file mode 100644 index 7c2e115524..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../util.dart'; - -void main() { - late AppFlowyUnitTest testContext; - setUpAll(() async { - testContext = await AppFlowyUnitTest.ensureInitialized(); - }); - - test('assert initial apps is the build-in app', () async { - final menuBloc = SidebarRootViewsBloc() - ..add( - SidebarRootViewsEvent.initial( - testContext.userProfile, - testContext.currentWorkspace.id, - ), - ); - - await blocResponseFuture(); - - assert(menuBloc.state.views.length == 1); - }); - - test('reorder apps', () async { - final menuBloc = SidebarRootViewsBloc() - ..add( - SidebarRootViewsEvent.initial( - testContext.userProfile, - testContext.currentWorkspace.id, - ), - ); - await blocResponseFuture(); - menuBloc.add(const SidebarRootViewsEvent.createRootView("App 1")); - await blocResponseFuture(); - menuBloc.add(const SidebarRootViewsEvent.createRootView("App 2")); - await blocResponseFuture(); - menuBloc.add(const SidebarRootViewsEvent.createRootView("App 3")); - await blocResponseFuture(); - - assert(menuBloc.state.views[1].name == 'App 1'); - assert(menuBloc.state.views[2].name == 'App 2'); - assert(menuBloc.state.views[3].name == 'App 3'); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart new file mode 100644 index 0000000000..75ade70a87 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart @@ -0,0 +1,57 @@ +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../util.dart'; + +void main() { + late AppFlowyUnitTest testContext; + setUpAll(() async { + testContext = await AppFlowyUnitTest.ensureInitialized(); + }); + + test('assert initial apps is the build-in app', () async { + final menuBloc = SidebarSectionsBloc() + ..add( + SidebarSectionsEvent.initial( + testContext.userProfile, + testContext.currentWorkspace.id, + ), + ); + + await blocResponseFuture(); + + assert(menuBloc.state.section.publicViews.length == 1); + assert(menuBloc.state.section.privateViews.isEmpty); + }); + + test('create views', () async { + final menuBloc = SidebarSectionsBloc() + ..add( + SidebarSectionsEvent.initial( + testContext.userProfile, + testContext.currentWorkspace.id, + ), + ); + await blocResponseFuture(); + + final names = ['View 1', 'View 2', 'View 3']; + for (final name in names) { + menuBloc.add( + SidebarSectionsEvent.createRootViewInSection( + name: name, + index: 0, + viewSection: ViewSectionPB.Public, + ), + ); + await blocResponseFuture(); + } + + final reversedNames = names.reversed.toList(); + for (var i = 0; i < names.length; i++) { + assert( + menuBloc.state.section.publicViews[i].name == reversedNames[i], + ); + } + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart index 0bd464e1b0..189a32cbac 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart @@ -22,6 +22,7 @@ class TrashTestContext { const ViewEvent.createView( "Document 1", ViewLayoutPB.Document, + section: ViewSectionPB.Public, ), ); await blocResponseFuture(); @@ -30,6 +31,7 @@ class TrashTestContext { const ViewEvent.createView( "Document 2", ViewLayoutPB.Document, + section: ViewSectionPB.Public, ), ); await blocResponseFuture(); @@ -38,6 +40,7 @@ class TrashTestContext { const ViewEvent.createView( "Document 3", ViewLayoutPB.Document, + section: ViewSectionPB.Public, ), ); await blocResponseFuture(); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart index f70a8e5ec1..868a003d5b 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart @@ -36,7 +36,11 @@ void main() { final viewBloc = await createTestViewBloc(); // create a nested view viewBloc.add( - const ViewEvent.createView(name, ViewLayoutPB.Document), + const ViewEvent.createView( + name, + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); expect(viewBloc.state.view.childViews.length, 1); @@ -52,7 +56,11 @@ void main() { test('delete view test', () async { final viewBloc = await createTestViewBloc(); viewBloc.add( - const ViewEvent.createView(name, ViewLayoutPB.Document), + const ViewEvent.createView( + name, + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); expect(viewBloc.state.view.childViews.length, 1); @@ -69,7 +77,11 @@ void main() { test('create nested view test', () async { final viewBloc = await createTestViewBloc(); viewBloc.add( - const ViewEvent.createView('Document 1', ViewLayoutPB.Document), + const ViewEvent.createView( + 'Document 1', + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); final document1Bloc = ViewBloc(view: viewBloc.state.view.childViews.first) @@ -79,7 +91,11 @@ void main() { await blocResponseFuture(); const name = 'Document 1 - 1'; document1Bloc.add( - const ViewEvent.createView('Document 1 - 1', ViewLayoutPB.Document), + const ViewEvent.createView( + 'Document 1 - 1', + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); expect(document1Bloc.state.view.childViews.length, 1); @@ -91,7 +107,11 @@ void main() { final names = ['1', '2', '3']; for (final name in names) { viewBloc.add( - ViewEvent.createView(name, ViewLayoutPB.Document), + ViewEvent.createView( + name, + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); } @@ -106,7 +126,13 @@ void main() { final viewBloc = await createTestViewBloc(); expect(viewBloc.state.lastCreatedView, isNull); - viewBloc.add(const ViewEvent.createView('1', ViewLayoutPB.Document)); + viewBloc.add( + const ViewEvent.createView( + '1', + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), + ); await blocResponseFuture(); expect( viewBloc.state.lastCreatedView!.id, @@ -117,7 +143,13 @@ void main() { '1', ); - viewBloc.add(const ViewEvent.createView('2', ViewLayoutPB.Document)); + viewBloc.add( + const ViewEvent.createView( + '2', + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), + ); await blocResponseFuture(); expect( viewBloc.state.lastCreatedView!.name, @@ -128,13 +160,25 @@ void main() { test('open latest document test', () async { const name1 = 'document'; final viewBloc = await createTestViewBloc(); - viewBloc.add(const ViewEvent.createView(name1, ViewLayoutPB.Document)); + viewBloc.add( + const ViewEvent.createView( + name1, + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), + ); await blocResponseFuture(); final document = viewBloc.state.lastCreatedView!; assert(document.name == name1); const gird = 'grid'; - viewBloc.add(const ViewEvent.createView(gird, ViewLayoutPB.Document)); + viewBloc.add( + const ViewEvent.createView( + gird, + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), + ); await blocResponseFuture(); assert(viewBloc.state.lastCreatedView!.name == gird); @@ -170,7 +214,11 @@ void main() { for (var i = 0; i < layouts.length; i++) { final layout = layouts[i]; viewBloc.add( - ViewEvent.createView('Test $layout', layout), + ViewEvent.createView( + 'Test $layout', + layout, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); expect(viewBloc.state.view.childViews.length, i + 1); diff --git a/frontend/appflowy_flutter/test/util.dart b/frontend/appflowy_flutter/test/util.dart index 2cf163688d..65303cb789 100644 --- a/frontend/appflowy_flutter/test/util.dart +++ b/frontend/appflowy_flutter/test/util.dart @@ -74,7 +74,10 @@ class AppFlowyUnitTest { } Future createWorkspace() async { - final result = await workspaceService.createApp(name: "Test App"); + final result = await workspaceService.createView( + name: "Test App", + viewSection: ViewSectionPB.Public, + ); return result.fold( (app) => app, (error) => throw Exception(error), @@ -82,7 +85,7 @@ class AppFlowyUnitTest { } Future> loadApps() async { - final result = await workspaceService.getViews(); + final result = await workspaceService.getPublicViews(); return result.fold( (apps) => apps, diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 5a43686f77..532628171c 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -162,7 +162,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "bincode", @@ -716,7 +716,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "again", "anyhow", @@ -764,7 +764,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "futures-channel", "futures-util", @@ -838,7 +838,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "async-trait", @@ -854,6 +854,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "unicode-segmentation", "web-sys", "yrs", ] @@ -861,7 +862,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "async-trait", @@ -891,7 +892,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "collab", @@ -910,7 +911,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "bytes", @@ -925,7 +926,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "chrono", @@ -962,7 +963,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "async-stream", @@ -1001,7 +1002,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "collab", @@ -1335,7 +1336,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "app-error", @@ -2637,7 +2638,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "futures-util", @@ -2654,7 +2655,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "app-error", @@ -3109,7 +3110,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "reqwest", @@ -4892,7 +4893,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "bincode", @@ -4916,7 +4917,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "bincode", @@ -5588,7 +5589,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "app-error", @@ -7551,7 +7552,7 @@ dependencies = [ [[package]] name = "workspace-template" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "async-trait", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index e7869b20c6..4a47b165a2 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -86,7 +86,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c5112cc761736ac91f0a518552e7bbe522bceae6" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ab9496c248b7c733d1aa160062abeb66c4e41325" } # Please use the following script to update collab. # Working directory: frontend # @@ -96,10 +96,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c51 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts index e6f28766f2..fe066b7377 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts @@ -1,16 +1,12 @@ +import { parserViewPBToPage } from '$app_reducers/pages/slice'; import { + ChangeWorkspaceIconPB, CreateViewPayloadPB, + GetWorkspaceViewPB, + RenameWorkspacePB, UserWorkspaceIdPB, WorkspaceIdPB, - RenameWorkspacePB, - ChangeWorkspaceIconPB, } from '@/services/backend'; -import { - UserEventOpenWorkspace, - UserEventRenameWorkspace, - UserEventChangeWorkspaceIcon, - UserEventGetAllWorkspace, -} from '@/services/backend/events/flowy-user'; import { FolderEventCreateView, FolderEventDeleteWorkspace, @@ -18,7 +14,12 @@ import { FolderEventReadCurrentWorkspace, FolderEventReadWorkspaceViews, } from '@/services/backend/events/flowy-folder'; -import { parserViewPBToPage } from '$app_reducers/pages/slice'; +import { + UserEventChangeWorkspaceIcon, + UserEventGetAllWorkspace, + UserEventOpenWorkspace, + UserEventRenameWorkspace, +} from '@/services/backend/events/flowy-user'; export async function openWorkspace(id: string) { const payload = new UserWorkspaceIdPB({ @@ -49,7 +50,7 @@ export async function deleteWorkspace(id: string) { } export async function getWorkspaceChildViews(id: string) { - const payload = new WorkspaceIdPB({ + const payload = new GetWorkspaceViewPB({ value: id, }); diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index ad1abdcbde..820a6e5274 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -221,7 +221,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "bincode", @@ -545,7 +545,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "again", "anyhow", @@ -592,7 +592,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "futures-channel", "futures-util", @@ -636,7 +636,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "async-trait", @@ -652,6 +652,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "unicode-segmentation", "web-sys", "yrs", ] @@ -659,7 +660,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "collab", @@ -678,7 +679,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "bytes", @@ -693,7 +694,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "chrono", @@ -730,7 +731,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "async-stream", @@ -768,7 +769,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "collab", @@ -965,7 +966,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "app-error", @@ -1720,7 +1721,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "futures-util", @@ -1737,7 +1738,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "app-error", @@ -2071,7 +2072,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "reqwest", @@ -3315,7 +3316,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "bincode", @@ -3339,7 +3340,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "bincode", @@ -3792,7 +3793,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "app-error", @@ -5024,4 +5025,4 @@ dependencies = [ [[patch.unused]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 296a387ecc..829840c6dd 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -55,7 +55,7 @@ codegen-units = 1 # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c5112cc761736ac91f0a518552e7bbe522bceae6" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ab9496c248b7c733d1aa160062abeb66c4e41325" } # Please use the following script to update collab. # Working directory: frontend # @@ -65,10 +65,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c51 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 8807a3a057..54076b1bff 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -205,10 +205,16 @@ "closeSidebar": "Close side bar", "openSidebar": "Open side bar", "personal": "Personal", + "private": "Private", + "public": "Public", "favorites": "Favorites", + "clickToHidePrivate": "Click to hide private section\nPages you created here are only visible to you", + "clickToHidePublic": "Click to hide public section\nPages you created here are visible to every member", "clickToHidePersonal": "Click to hide personal section", "clickToHideFavorites": "Click to hide favorite section", "addAPage": "Add a page", + "addAPageToPrivate": "Add a page to private section", + "addAPageToPublic": "Add a page to public section", "recent": "Recent" }, "notifications": { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 48c5d13101..d46547cd29 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "bincode", @@ -673,7 +673,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "again", "anyhow", @@ -721,7 +721,7 @@ dependencies = [ [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "futures-channel", "futures-util", @@ -764,7 +764,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "async-trait", @@ -780,6 +780,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "unicode-segmentation", "web-sys", "yrs", ] @@ -787,7 +788,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "async-trait", @@ -817,7 +818,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "collab", @@ -836,7 +837,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "bytes", @@ -851,7 +852,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "chrono", @@ -888,7 +889,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "async-stream", @@ -927,7 +928,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2d7b1838e463ce0348cf700ff43f33f5718203be#2d7b1838e463ce0348cf700ff43f33f5718203be" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0970b2e1440134af7c83bb8fc80cac5d2dedebb7#0970b2e1440134af7c83bb8fc80cac5d2dedebb7" dependencies = [ "anyhow", "collab", @@ -1257,7 +1258,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "app-error", @@ -2432,7 +2433,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "futures-util", @@ -2449,7 +2450,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "app-error", @@ -2843,7 +2844,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "reqwest", @@ -4325,7 +4326,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "bincode", @@ -4349,7 +4350,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "bincode", @@ -4942,7 +4943,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "app-error", @@ -6355,7 +6356,7 @@ dependencies = [ [[package]] name = "workspace-template" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c5112cc761736ac91f0a518552e7bbe522bceae6#c5112cc761736ac91f0a518552e7bbe522bceae6" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ab9496c248b7c733d1aa160062abeb66c4e41325#ab9496c248b7c733d1aa160062abeb66c4e41325" dependencies = [ "anyhow", "async-trait", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 7a4b22591a..5e030565da 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -110,7 +110,7 @@ rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec1 # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c5112cc761736ac91f0a518552e7bbe522bceae6" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ab9496c248b7c733d1aa160062abeb66c4e41325" } # Please use the following script to update collab. # Working directory: frontend # @@ -120,10 +120,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c51 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2d7b1838e463ce0348cf700ff43f33f5718203be" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0970b2e1440134af7c83bb8fc80cac5d2dedebb7" } diff --git a/frontend/rust-lib/event-integration/src/database_event.rs b/frontend/rust-lib/event-integration/src/database_event.rs index 3f91fd8441..424936b6a8 100644 --- a/frontend/rust-lib/event-integration/src/database_event.rs +++ b/frontend/rust-lib/event-integration/src/database_event.rs @@ -36,6 +36,7 @@ impl EventIntegrationTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -66,6 +67,7 @@ impl EventIntegrationTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -91,6 +93,7 @@ impl EventIntegrationTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) 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 2070b6adf9..49f0f62a9b 100644 --- a/frontend/rust-lib/event-integration/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration/src/document/document_event.rs @@ -64,6 +64,7 @@ impl DocumentEventTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; EventBuilder::new(core.clone()) .event(FolderEvent::CreateView) diff --git a/frontend/rust-lib/event-integration/src/document_event.rs b/frontend/rust-lib/event-integration/src/document_event.rs index 70fffac107..95103159fd 100644 --- a/frontend/rust-lib/event-integration/src/document_event.rs +++ b/frontend/rust-lib/event-integration/src/document_event.rs @@ -41,6 +41,7 @@ impl EventIntegrationTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; let view = EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) diff --git a/frontend/rust-lib/event-integration/src/folder_event.rs b/frontend/rust-lib/event-integration/src/folder_event.rs index c189e4fb26..604bd1475d 100644 --- a/frontend/rust-lib/event-integration/src/folder_event.rs +++ b/frontend/rust-lib/event-integration/src/folder_event.rs @@ -57,7 +57,7 @@ impl EventIntegrationTest { pub async fn get_all_workspace_views(&self) -> Vec { EventBuilder::new(self.clone()) - .event(FolderEvent::ReadWorkspaceViews) + .event(FolderEvent::ReadCurrentWorkspaceViews) .async_send() .await .parse::() @@ -115,6 +115,7 @@ impl EventIntegrationTest { meta: Default::default(), set_as_current: false, index: None, + section: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -165,6 +166,7 @@ impl ViewTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; let view = EventBuilder::new(sdk.clone()) diff --git a/frontend/rust-lib/event-integration/src/user_event.rs b/frontend/rust-lib/event-integration/src/user_event.rs index db09b5414c..07c8560a09 100644 --- a/frontend/rust-lib/event-integration/src/user_event.rs +++ b/frontend/rust-lib/event-integration/src/user_event.rs @@ -276,9 +276,9 @@ impl EventIntegrationTest { .parse() } - pub async fn folder_read_workspace_views(&self) -> RepeatedViewPB { + pub async fn folder_read_current_workspace_views(&self) -> RepeatedViewPB { EventBuilder::new(self.clone()) - .event(FolderEvent::ReadWorkspaceViews) + .event(FolderEvent::ReadCurrentWorkspaceViews) .async_send() .await .parse() 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 c240ce9844..b2a1ee98d3 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 @@ -246,6 +246,7 @@ pub async fn create_view( meta: Default::default(), set_as_current: true, index: None, + section: None, }; EventBuilder::new(sdk.clone()) .event(CreateView) @@ -275,6 +276,8 @@ pub async fn move_view( view_id, new_parent_id: parent_id, prev_view_id, + from_section: None, + to_section: None, }; let error = EventBuilder::new(sdk.clone()) .event(MoveNestedView) 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 327d4f5843..8e60baef3a 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 @@ -549,6 +549,8 @@ async fn move_folder_nested_view( view_id, new_parent_id, prev_view_id, + from_section: None, + to_section: None, }; EventBuilder::new(sdk) .event(flowy_folder::event_map::FolderEvent::MoveNestedView) diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs index 5a587bc368..8f1968cec3 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs @@ -77,7 +77,7 @@ async fn af_cloud_create_workspace_test() { // before opening new workspace let folder_ws = test.folder_read_current_workspace().await; assert_eq!(&folder_ws.id, first_workspace_id); - let views = test.folder_read_workspace_views().await; + let views = test.folder_read_current_workspace_views().await; assert_eq!(views.items[0].parent_view_id.as_str(), first_workspace_id); } { @@ -85,7 +85,7 @@ async fn af_cloud_create_workspace_test() { test.open_workspace(&created_workspace.workspace_id).await; let folder_ws = test.folder_read_current_workspace().await; assert_eq!(folder_ws.id, created_workspace.workspace_id); - let views = test.folder_read_workspace_views().await; + let views = test.folder_read_current_workspace_views().await; assert_eq!( views.items[0].parent_view_id.as_str(), created_workspace.workspace_id diff --git a/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs b/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs index 7a7c7ca030..26c5368398 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs @@ -59,6 +59,7 @@ impl ViewBuilder { layout: ViewLayout::Document, child_views: vec![], is_favorite: false, + icon: None, } } diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 508d68989c..9a7a667e32 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -118,6 +118,15 @@ impl std::convert::From for ViewLayoutPB { } } +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct SectionViewsPB { + #[pb(index = 1)] + pub section: ViewSectionPB, + + #[pb(index = 2)] + pub views: Vec, +} + #[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] pub struct RepeatedViewPB { #[pb(index = 1)] @@ -181,6 +190,20 @@ pub struct CreateViewPayloadPB { // If the index is None or the index is out of range, the view will be appended to the end of the parent view. #[pb(index = 9, one_of)] pub index: Option, + + // The section of the view. + // Only the view in public section will be shown in the shared workspace view list. + // The view in private section will only be shown in the user's private view list. + #[pb(index = 10, one_of)] + pub section: Option, +} + +#[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone, Default)] +pub enum ViewSectionPB { + #[default] + // only support public and private section now. + Private = 0, + Public = 1, } /// The orphan view is meant to be a view that is not attached to any parent view. By default, this @@ -218,6 +241,8 @@ pub struct CreateViewParams { // The index of the view in the parent view. // If the index is None or the index is out of range, the view will be appended to the end of the parent view. pub index: Option, + // The section of the view. + pub section: Option, } impl TryInto for CreateViewPayloadPB { @@ -238,6 +263,7 @@ impl TryInto for CreateViewPayloadPB { meta: self.meta, set_as_current: self.set_as_current, index: self.index, + section: self.section, }) } } @@ -259,6 +285,8 @@ impl TryInto for CreateOrphanViewPayloadPB { meta: Default::default(), set_as_current: false, index: None, + // TODO: lucas.xu add section to CreateOrphanViewPayloadPB + section: Some(ViewSectionPB::Public), }) } } @@ -384,6 +412,12 @@ pub struct MoveNestedViewPayloadPB { #[pb(index = 3, one_of)] pub prev_view_id: Option, + + #[pb(index = 4, one_of)] + pub from_section: Option, + + #[pb(index = 5, one_of)] + pub to_section: Option, } pub struct MoveViewParams { @@ -405,10 +439,13 @@ impl TryInto for MoveViewPayloadPB { } } +#[derive(Debug)] pub struct MoveNestedViewParams { pub view_id: String, pub new_parent_id: String, pub prev_view_id: Option, + pub from_section: Option, + pub to_section: Option, } impl TryInto for MoveNestedViewPayloadPB { @@ -422,6 +459,8 @@ impl TryInto for MoveNestedViewPayloadPB { view_id, new_parent_id, prev_view_id, + from_section: self.from_section, + to_section: self.to_section, }) } } diff --git a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs index 6ce3328da6..21ff046226 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs @@ -97,6 +97,42 @@ pub struct WorkspaceIdPB { pub value: String, } +#[derive(Clone, Debug)] +pub struct WorkspaceIdParams { + pub value: String, +} + +impl TryInto for WorkspaceIdPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + Ok(WorkspaceIdParams { + value: WorkspaceIdentify::parse(self.value)?.0, + }) + } +} + +#[derive(Clone, ProtoBuf, Default, Debug)] +pub struct GetWorkspaceViewPB { + #[pb(index = 1)] + pub value: String, +} + +#[derive(Clone, Debug)] +pub struct GetWorkspaceViewParams { + pub value: String, +} + +impl TryInto for GetWorkspaceViewPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + Ok(GetWorkspaceViewParams { + value: WorkspaceIdentify::parse(self.value)?.0, + }) + } +} + #[derive(Default, ProtoBuf, Debug, Clone)] pub struct WorkspaceSettingPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 58064018e1..c368cda3b2 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -48,6 +48,18 @@ pub(crate) async fn get_all_workspace_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn get_workspace_views_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> DataResult { + let folder = upgrade_folder(folder)?; + let params: GetWorkspaceViewParams = data.into_inner().try_into()?; + let child_views = folder.get_workspace_views(¶ms.value).await?; + let repeated_view: RepeatedViewPB = child_views.into(); + data_result_ok(repeated_view) +} + +#[tracing::instrument(level = "debug", skip(folder), err)] +pub(crate) async fn get_current_workspace_views_handler( folder: AFPluginState>, ) -> DataResult { let folder = upgrade_folder(folder)?; @@ -56,6 +68,18 @@ pub(crate) async fn get_workspace_views_handler( data_result_ok(repeated_view) } +#[tracing::instrument(level = "debug", skip(folder), err)] +pub(crate) async fn read_private_views_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> DataResult { + let folder = upgrade_folder(folder)?; + let params: GetWorkspaceViewParams = data.into_inner().try_into()?; + let child_views = folder.get_workspace_private_views(¶ms.value).await?; + let repeated_view: RepeatedViewPB = child_views.into(); + data_result_ok(repeated_view) +} + #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_current_workspace_setting_handler( folder: AFPluginState>, @@ -212,9 +236,7 @@ pub(crate) async fn move_nested_view_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let params: MoveNestedViewParams = data.into_inner().try_into()?; - folder - .move_nested_view(params.view_id, params.new_parent_id, params.prev_view_id) - .await?; + folder.move_nested_view(params).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index e81afbb656..2baf534993 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -38,6 +38,8 @@ pub fn init(folder: Weak) -> AFPlugin { .event(FolderEvent::ToggleFavorite, toggle_favorites_handler) .event(FolderEvent::UpdateRecentViews, update_recent_views_handler) .event(FolderEvent::ReloadWorkspace, reload_workspace_handler) + .event(FolderEvent::ReadPrivateViews, read_private_views_handler) + .event(FolderEvent::ReadCurrentWorkspaceViews, get_current_workspace_views_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -59,9 +61,9 @@ pub enum FolderEvent { #[event(input = "WorkspaceIdPB")] DeleteWorkspace = 3, - /// Return a list of views of the current workspace. + /// Return a list of views of the specified workspace. /// Only the first level of child views are included. - #[event(input = "WorkspaceIdPB", output = "RepeatedViewPB")] + #[event(input = "GetWorkspaceViewPB", output = "RepeatedViewPB")] ReadWorkspaceViews = 5, /// Create a new view in the corresponding app @@ -156,4 +158,12 @@ pub enum FolderEvent { #[event()] ReloadWorkspace = 38, + + #[event(input = "GetWorkspaceViewPB", output = "RepeatedViewPB")] + ReadPrivateViews = 39, + + /// Return a list of views of the current workspace. + /// Only the first level of child views are included. + #[event(output = "RepeatedViewPB")] + ReadCurrentWorkspaceViews = 40, } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index b4d480ea38..84b5ad8bb1 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -23,8 +23,8 @@ use lib_infra::conditional_send_sync_trait; use crate::entities::icon::UpdateViewIconParams; use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, CreateViewParams, CreateWorkspaceParams, - DeletedViewPB, FolderSnapshotPB, RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, - UpdateViewParams, ViewPB, WorkspacePB, WorkspaceSettingPB, + DeletedViewPB, FolderSnapshotPB, MoveNestedViewParams, RepeatedTrashPB, RepeatedViewIdPB, + RepeatedViewPB, UpdateViewParams, ViewPB, ViewSectionPB, WorkspacePB, WorkspaceSettingPB, }; use crate::manager_observer::{ notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change, @@ -113,7 +113,7 @@ impl FolderManager { }, |folder| { let workspace_pb_from_workspace = |workspace: Workspace, folder: &Folder| { - let views = get_workspace_view_pbs(&workspace.id, folder); + let views = get_workspace_public_view_pbs(&workspace.id, folder); let workspace: WorkspacePB = (workspace, views).into(); Ok::(workspace) }; @@ -145,7 +145,15 @@ impl FolderManager { pub async fn get_workspace_views(&self, workspace_id: &str) -> FlowyResult> { let views = self.with_folder(Vec::new, |folder| { - get_workspace_view_pbs(workspace_id, folder) + get_workspace_public_view_pbs(workspace_id, folder) + }); + + Ok(views) + } + + pub async fn get_workspace_private_views(&self, workspace_id: &str) -> FlowyResult> { + let views = self.with_folder(Vec::new, |folder| { + get_workspace_private_view_pbs(workspace_id, folder) }); Ok(views) @@ -452,11 +460,16 @@ impl FolderManager { } let index = params.index; + let section = params.section.clone().unwrap_or(ViewSectionPB::Public); + let is_private = section == ViewSectionPB::Private; let view = create_view(self.user.user_id()?, params, view_layout); self.with_folder( || (), |folder| { folder.insert_view(view.clone(), index); + if is_private { + folder.add_private_view_ids(vec![view.id.clone()]); + } }, ); @@ -609,18 +622,26 @@ impl FolderManager { /// * `prev_view_id` - An `Option` that holds the id of the view after which the `view_id` should be positioned. /// #[tracing::instrument(level = "trace", skip(self), err)] - pub async fn move_nested_view( - &self, - view_id: String, - new_parent_id: String, - prev_view_id: Option, - ) -> FlowyResult<()> { + pub async fn move_nested_view(&self, params: MoveNestedViewParams) -> FlowyResult<()> { + let view_id = params.view_id; + let new_parent_id = params.new_parent_id; + let prev_view_id = params.prev_view_id; + let from_section = params.from_section; + let to_section = params.to_section; let view = self.get_view_pb(&view_id).await?; let old_parent_id = view.parent_view_id; self.with_folder( || (), |folder| { folder.move_nested_view(&view_id, &new_parent_id, prev_view_id); + + if from_section != to_section { + if to_section == Some(ViewSectionPB::Private) { + folder.add_private_view_ids(vec![view_id.clone()]); + } else { + folder.delete_private_view_ids(vec![view_id.clone()]); + } + } }, ); notify_parent_view_did_change( @@ -743,6 +764,8 @@ impl FolderManager { meta: Default::default(), set_as_current: true, index, + // TODO: lucas.xu fetch the section from the view + section: Some(ViewSectionPB::Public), }; self.create_view_with_params(duplicate_params).await?; @@ -954,6 +977,8 @@ impl FolderManager { meta: Default::default(), set_as_current: false, index: None, + // TODO: Lucas.xu fetch the section from the view + section: Some(ViewSectionPB::Public), }; let view = create_view(self.user.user_id()?, params, import_data.view_layout); @@ -1110,16 +1135,61 @@ impl FolderManager { } } -/// Return the views that belong to the workspace. The views are filtered by the trash. -pub(crate) fn get_workspace_view_pbs(_workspace_id: &str, folder: &Folder) -> Vec { - let items = folder.get_all_trash(); - let trash_ids = items +/// Return the views that belong to the workspace. The views are filtered by the trash and all the private views. +pub(crate) fn get_workspace_public_view_pbs(_workspace_id: &str, folder: &Folder) -> Vec { + // get the trash ids + let trash_ids = folder + .get_all_trash() .into_iter() .map(|trash| trash.id) .collect::>(); + // get the private view ids + let private_view_ids = folder + .get_all_private_views() + .into_iter() + .map(|view| view.id) + .collect::>(); + let mut views = folder.get_workspace_views(); - views.retain(|view| !trash_ids.contains(&view.id)); + + // filter the views that are in the trash and all the private views + views.retain(|view| !trash_ids.contains(&view.id) && !private_view_ids.contains(&view.id)); + + views + .into_iter() + .map(|view| { + // Get child views + let child_views = folder + .views + .get_views_belong_to(&view.id) + .into_iter() + .collect(); + view_pb_with_child_views(view, child_views) + }) + .collect() +} + +/// Get the current private views of the user. +pub(crate) fn get_workspace_private_view_pbs(_workspace_id: &str, folder: &Folder) -> Vec { + // get the trash ids + let trash_ids = folder + .get_all_trash() + .into_iter() + .map(|trash| trash.id) + .collect::>(); + + // get the private view ids + let private_view_ids = folder + .get_my_private_views() + .into_iter() + .map(|view| view.id) + .collect::>(); + + let mut views = folder.get_workspace_views(); + + // filter the views that are in the trash and not in the private view ids + views.retain(|view| !trash_ids.contains(&view.id) && private_view_ids.contains(&view.id)); views .into_iter() diff --git a/frontend/rust-lib/flowy-folder/src/manager_observer.rs b/frontend/rust-lib/flowy-folder/src/manager_observer.rs index ab08010613..c3dfbae682 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_observer.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_observer.rs @@ -14,9 +14,9 @@ use lib_dispatch::prelude::af_spawn; use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, FolderSnapshotStatePB, - FolderSyncStatePB, RepeatedTrashPB, RepeatedViewPB, ViewPB, + FolderSyncStatePB, RepeatedTrashPB, RepeatedViewPB, SectionViewsPB, ViewPB, ViewSectionPB, }; -use crate::manager::{get_workspace_view_pbs, MutexFolder}; +use crate::manager::{get_workspace_private_view_pbs, get_workspace_public_view_pbs, MutexFolder}; use crate::notification::{send_notification, FolderNotification}; /// Listen on the [ViewChange] after create/delete/update events happened @@ -161,7 +161,8 @@ pub(crate) fn notify_parent_view_did_change>( // if the view's parent id equal to workspace id. Then it will fetch the current // workspace views. Because the the workspace is not a view stored in the views map. if parent_view_id == workspace_id { - notify_did_update_workspace(&workspace_id, folder) + notify_did_update_workspace(&workspace_id, folder); + notify_did_update_section_views(&workspace_id, folder); } else { // Parent view can contain a list of child views. Currently, only get the first level // child views. @@ -181,8 +182,35 @@ pub(crate) fn notify_parent_view_did_change>( None } +pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folder) { + let public_views = get_workspace_public_view_pbs(workspace_id, folder); + let private_views = get_workspace_private_view_pbs(workspace_id, folder); + tracing::trace!( + "Did update section views: public len = {}, private len = {}", + public_views.len(), + private_views.len() + ); + + // TODO(Lucas.xu) - Only notify the section changed, not the public/private both. + // Notify the public views + send_notification(workspace_id, FolderNotification::DidUpdateSectionViews) + .payload(SectionViewsPB { + section: ViewSectionPB::Public, + views: public_views, + }) + .send(); + + // Notify the private views + send_notification(workspace_id, FolderNotification::DidUpdateSectionViews) + .payload(SectionViewsPB { + section: ViewSectionPB::Private, + views: private_views, + }) + .send(); +} + pub(crate) fn notify_did_update_workspace(workspace_id: &str, folder: &Folder) { - let repeated_view: RepeatedViewPB = get_workspace_view_pbs(workspace_id, folder).into(); + let repeated_view: RepeatedViewPB = get_workspace_public_view_pbs(workspace_id, folder).into(); tracing::trace!("Did update workspace views: {:?}", repeated_view); send_notification(workspace_id, FolderNotification::DidUpdateWorkspaceViews) .payload(repeated_view) diff --git a/frontend/rust-lib/flowy-folder/src/notification.rs b/frontend/rust-lib/flowy-folder/src/notification.rs index df83edf46b..c57450a5d6 100644 --- a/frontend/rust-lib/flowy-folder/src/notification.rs +++ b/frontend/rust-lib/flowy-folder/src/notification.rs @@ -35,6 +35,9 @@ pub enum FolderNotification { DidUnfavoriteView = 37, DidUpdateRecentViews = 38, + + /// Trigger when the ROOT views (the first level) in section are updated + DidUpdateSectionViews = 39, } impl std::convert::From for i32 { @@ -60,6 +63,8 @@ impl std::convert::From for FolderNotification { 17 => FolderNotification::DidUpdateFolderSyncUpdate, 36 => FolderNotification::DidFavoriteView, 37 => FolderNotification::DidUnfavoriteView, + 38 => FolderNotification::DidUpdateRecentViews, + 39 => FolderNotification::DidUpdateSectionViews, _ => FolderNotification::Unknown, } } diff --git a/frontend/rust-lib/flowy-folder/src/test_helper.rs b/frontend/rust-lib/flowy-folder/src/test_helper.rs index b63448bc94..50e4b290ff 100644 --- a/frontend/rust-lib/flowy-folder/src/test_helper.rs +++ b/frontend/rust-lib/flowy-folder/src/test_helper.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use flowy_folder_pub::cloud::gen_view_id; -use crate::entities::{CreateViewParams, ViewLayoutPB}; +use crate::entities::{CreateViewParams, ViewLayoutPB, ViewSectionPB}; use crate::manager::FolderManager; #[cfg(feature = "test_helper")] @@ -47,6 +47,7 @@ impl FolderManager { meta: ext, set_as_current: true, index: None, + section: Some(ViewSectionPB::Public), }; self.create_view_with_params(params).await.unwrap(); view_id diff --git a/frontend/rust-lib/flowy-folder/src/user_default.rs b/frontend/rust-lib/flowy-folder/src/user_default.rs index be2e4c3cf4..0e2e3f4bc3 100644 --- a/frontend/rust-lib/flowy-folder/src/user_default.rs +++ b/frontend/rust-lib/flowy-folder/src/user_default.rs @@ -54,6 +54,7 @@ impl DefaultFolderBuilder { favorites: Default::default(), recent: Default::default(), trash: Default::default(), + private: Default::default(), } } }