diff --git a/frontend/appflowy_flutter/integration_test/util/settings.dart b/frontend/appflowy_flutter/integration_test/util/settings.dart index 4c666645de..8f48aa3694 100644 --- a/frontend/appflowy_flutter/integration_test/util/settings.dart +++ b/frontend/appflowy_flutter/integration_test/util/settings.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart index 711b132a4d..7fec980112 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart @@ -1,9 +1,7 @@ -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; @@ -13,6 +11,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'tab_bar_add_button.dart'; @@ -235,7 +234,7 @@ class TabBarItemButton extends StatelessWidget { NavigatorTextFieldDialog( title: LocaleKeys.menuAppHeader_renameDialog.tr(), value: view.name, - confirm: (newValue) { + onConfirm: (newValue, _) { context.read().add( DatabaseTabBarEvent.renameView(view.id, newValue), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart index 47e06be639..09906e1429 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -360,9 +360,7 @@ class DepthOptionAction extends PopoverActionCell { (e) => HoverButton( onTap: () => onTap(e.inner), itemHeight: ActionListSizes.itemHeight, - leftIcon: null, name: e.name, - rightIcon: null, ), ) .toList(); diff --git a/frontend/appflowy_flutter/lib/shared/af_role_pb_extension.dart b/frontend/appflowy_flutter/lib/shared/af_role_pb_extension.dart new file mode 100644 index 0000000000..32b993938a --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/af_role_pb_extension.dart @@ -0,0 +1,25 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; + +extension AFRolePBExtension on AFRolePB { + bool get isOwner => this == AFRolePB.Owner; + + bool get canInvite => isOwner; + + bool get canDelete => isOwner; + + bool get canUpdate => isOwner; + + String get description { + switch (this) { + case AFRolePB.Owner: + return LocaleKeys.settings_appearance_members_owner.tr(); + case AFRolePB.Member: + return LocaleKeys.settings_appearance_members_member.tr(); + case AFRolePB.Guest: + return LocaleKeys.settings_appearance_members_guest.tr(); + } + throw UnimplementedError('Unknown role: $this'); + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 44b067475b..1ef6d132e0 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -79,15 +79,13 @@ class UserBackendService { return UserEventOpenAnonUser().send(); } - Future, FlowyError>> getWorkspaces() { - // final request = WorkspaceIdPB.create(); - // return FolderEventReadAllWorkspaces(request).send().then((result) { - // return result.fold( - // (workspaces) => FlowyResult.success(workspaces.items), - // (error) => FlowyResult.failure(error), - // ); - // }); - return Future.value(FlowyResult.success([])); + Future, FlowyError>> getWorkspaces() { + return UserEventGetAllWorkspace().send().then((value) { + return value.fold( + (workspaces) => FlowyResult.success(workspaces.items), + (error) => FlowyResult.failure(error), + ); + }); } Future> openWorkspace(String workspaceId) { @@ -118,4 +116,18 @@ class UserBackendService { ); }); } + + Future> createUserWorkspace( + String name, + ) { + final request = CreateWorkspacePB.create()..name = name; + return UserEventCreateWorkspace(request).send(); + } + + Future> deleteWorkspaceById( + String workspaceId, + ) { + final request = UserWorkspaceIdPB.create()..workspaceId = workspaceId; + return UserEventDeleteWorkspace(request).send(); + } } diff --git a/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart b/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart index f6d42101b6..c77650443e 100644 --- a/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart +++ b/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart @@ -1,11 +1,10 @@ -import 'dart:ui'; +import 'package:flutter/material.dart'; class ColorGenerator { - Color generateColorFromString(String string) { - final hash = string.hashCode; - final int r = (hash & 0xFF0000) >> 16; - final int g = (hash & 0x00FF00) >> 8; - final int b = hash & 0x0000FF; - return Color.fromRGBO(r, g, b, 0.5); + static Color generateColorFromString(String string) { + final int hash = + string.codeUnits.fold(0, (int acc, int unit) => acc + unit); + final double hue = (hash % 360).toDouble(); + return HSLColor.fromAHSL(1.0, hue, 0.5, 0.8).toColor(); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/user/prelude.dart index f698497db9..b9bbb0ff08 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/prelude.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/prelude.dart @@ -1 +1,2 @@ export 'settings_user_bloc.dart'; +export 'user_workspace_bloc.dart'; 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 new file mode 100644 index 0000000000..20d9d0ba41 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -0,0 +1,134 @@ +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_backend/log.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_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user_workspace_bloc.freezed.dart'; + +class UserWorkspaceBloc extends Bloc { + UserWorkspaceBloc({ + required this.userProfile, + }) : _userService = UserBackendService(userId: userProfile.id), + super(UserWorkspaceState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + // do nothing + }, + workspacesReceived: (workspaceId) async {}, + fetchWorkspaces: () async { + final result = await _fetchWorkspaces(); + if (result != null) { + emit( + state.copyWith( + currentWorkspace: result.$1, + workspaces: result.$2, + ), + ); + } + }, + createWorkspace: (name, desc) async { + final result = await _userService.createUserWorkspace(name); + emit( + state.copyWith( + openWorkspaceResult: null, + deleteWorkspaceResult: null, + createWorkspaceResult: + result.fold((s) => FlowyResult.success(null), (e) { + Log.error(e); + return FlowyResult.failure(e); + }), + ), + ); + }, + deleteWorkspace: (workspaceId) async { + final result = await _userService.deleteWorkspaceById(workspaceId); + emit( + state.copyWith( + openWorkspaceResult: null, + createWorkspaceResult: null, + deleteWorkspaceResult: + result.fold((s) => FlowyResult.success(null), (e) { + Log.error(e); + return FlowyResult.failure(e); + }), + ), + ); + }, + openWorkspace: (workspaceId) async { + final result = await _userService.openWorkspace(workspaceId); + emit( + state.copyWith( + createWorkspaceResult: null, + deleteWorkspaceResult: null, + openWorkspaceResult: + result.fold((s) => FlowyResult.success(null), (e) { + Log.error(e); + return FlowyResult.failure(e); + }), + ), + ); + }, + ); + }, + ); + } + + final UserProfilePB userProfile; + final UserBackendService _userService; + + Future<(UserWorkspacePB currentWorkspace, List workspaces)?> + _fetchWorkspaces() async { + final result = await _userService.getCurrentWorkspace(); + return result.fold((currentWorkspace) async { + final result = await _userService.getWorkspaces(); + return result.fold((workspaces) { + return ( + workspaces.firstWhere( + (e) => e.workspaceId == currentWorkspace.id, + ), + workspaces + ); + }, (e) { + Log.error(e); + return null; + }); + }, (e) { + Log.error(e); + return null; + }); + } +} + +@freezed +class UserWorkspaceEvent with _$UserWorkspaceEvent { + const factory UserWorkspaceEvent.initial() = Initial; + const factory UserWorkspaceEvent.createWorkspace(String name, String desc) = + CreateWorkspace; + const factory UserWorkspaceEvent.fetchWorkspaces() = FetchWorkspaces; + const factory UserWorkspaceEvent.deleteWorkspace(String workspaceId) = + DeleteWorkspace; + const factory UserWorkspaceEvent.openWorkspace(String workspaceId) = + OpenWorkspace; + const factory UserWorkspaceEvent.workspacesReceived( + FlowyResult, FlowyError> workspacesOrFail, + ) = WorkspacesReceived; +} + +@freezed +class UserWorkspaceState with _$UserWorkspaceState { + const factory UserWorkspaceState({ + required UserWorkspacePB? currentWorkspace, + required List workspaces, + @Default(null) FlowyResult? createWorkspaceResult, + @Default(null) FlowyResult? deleteWorkspaceResult, + @Default(null) FlowyResult? openWorkspaceResult, + }) = _UserWorkspaceState; + + factory UserWorkspaceState.initial() => + const UserWorkspaceState(currentWorkspace: null, workspaces: []); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart index f74b435ad7..d8d5db45b4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart @@ -48,7 +48,7 @@ class WorkspaceBloc extends Bloc { emit( workspacesOrFailed.fold( (workspaces) => state.copyWith( - workspaces: workspaces, + workspaces: [], successOrFailure: FlowyResult.success(null), ), (error) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart index 93cc5a81d5..1add004e82 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart @@ -182,7 +182,7 @@ class DesktopHomeScreen extends StatelessWidget { required WorkspaceSettingPB workspaceSetting, }) { final homeMenu = HomeSideBar( - user: userProfile, + userProfile: userProfile, workspaceSetting: workspaceSetting, ); return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu)); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart index e49c2786b3..356e18bded 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart @@ -1,13 +1,12 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:flutter/material.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:provider/provider.dart'; 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 index e6352f5c38..4aa2d41355 100644 --- 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 @@ -119,7 +119,7 @@ class _PersonalFolderHeaderState extends State { createViewAndShowRenameDialogIfNeeded( context, LocaleKeys.newPageText.tr(), - (viewName) { + (viewName, _) { if (viewName.isNotEmpty) { context.read().add( MenuEvent.createApp( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart index 964229e4cf..bf18df1a98 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart @@ -15,7 +15,7 @@ import 'package:flutter/material.dart'; Future createViewAndShowRenameDialogIfNeeded( BuildContext context, String dialogTitle, - void Function(String viewName) createView, + void Function(String viewName, BuildContext context) createView, ) async { final value = await getIt().getWithFormat( KVKeys.showRenameDialogWhenCreatingNewFile, @@ -27,9 +27,9 @@ Future createViewAndShowRenameDialogIfNeeded( title: dialogTitle, value: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), autoSelectAllText: true, - confirm: createView, + onConfirm: createView, ).show(context); - } else { - createView(LocaleKeys.menuAppHeader_defaultNewPageName.tr()); + } else if (context.mounted) { + createView(LocaleKeys.menuAppHeader_defaultNewPageName.tr(), context); } } 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 af6f475698..80cee5be5e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -1,5 +1,4 @@ -import 'package:flutter/material.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/menu_bloc.dart'; @@ -12,12 +11,14 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_pa import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart'; 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/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Home Sidebar is the left side bar of the home page. @@ -30,11 +31,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class HomeSideBar extends StatelessWidget { const HomeSideBar({ super.key, - required this.user, + required this.userProfile, required this.workspaceSetting, }); - final UserProfilePB user; + final UserProfilePB userProfile; final WorkspaceSettingPB workspaceSetting; @@ -47,7 +48,7 @@ class HomeSideBar extends StatelessWidget { ), BlocProvider( create: (_) => MenuBloc( - user: user, + user: userProfile, workspaceId: workspaceSetting.workspaceId, )..add(const MenuEvent.initial()), ), @@ -103,11 +104,14 @@ class HomeSideBar extends StatelessWidget { padding: menuHorizontalInset, child: SidebarTopMenu(), ), - // user, setting + // user or workspace, setting Padding( padding: menuHorizontalInset, - child: SidebarUser(user: user, views: views), + child: FeatureFlag.collaborativeWorkspace.isOn + ? SidebarWorkspace(userProfile: userProfile, views: views) + : SidebarUser(userProfile: userProfile, views: views), ), + const VSpace(20), // scrollable document list Expanded( 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 46cd3dfbb1..909124f70a 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 @@ -23,7 +23,7 @@ class SidebarNewPageButton extends StatelessWidget { onPressed: () async => createViewAndShowRenameDialogIfNeeded( context, LocaleKeys.newPageText.tr(), - (viewName) { + (viewName, _) { if (viewName.isNotEmpty) { context.read().add(MenuEvent.createApp(viewName)); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart new file mode 100644 index 0000000000..24d4dd312f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart @@ -0,0 +1,104 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; +import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hotkey_manager/hotkey_manager.dart'; + +final GlobalKey _settingsDialogKey = GlobalKey(); + +Future openSettingsHotKey(BuildContext context) async { + final userProfileOrFailure = await getIt().getUser(); + + return userProfileOrFailure.fold( + (userProfile) => HotKeyItem( + hotKey: HotKey( + KeyCode.comma, + scope: HotKeyScope.inapp, + modifiers: [ + PlatformExtension.isMacOS ? KeyModifier.meta : KeyModifier.control, + ], + ), + keyDownHandler: (_) { + if (_settingsDialogKey.currentContext == null) { + showSettingsDialog(context, userProfile); + } else { + Navigator.of(context, rootNavigator: true) + .popUntil((route) => route.isFirst); + } + }, + ), + (e) { + Log.error('Failed to get user $e'); + return null; + }, + ); +} + +class UserSettingButton extends StatelessWidget { + const UserSettingButton({required this.userProfile, super.key}); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.settings_menu_open.tr(), + child: IconButton( + onPressed: () => showSettingsDialog(context, userProfile), + icon: SizedBox.square( + dimension: 20, + child: FlowySvg( + FlowySvgs.settings_m, + color: Theme.of(context).colorScheme.tertiary, + ), + ), + ), + ); + } +} + +void showSettingsDialog( + BuildContext context, + UserProfilePB userProfile, +) { + showDialog( + context: context, + builder: (dialogContext) { + return BlocProvider.value( + key: _settingsDialogKey, + value: BlocProvider.of(dialogContext), + child: SettingsDialog( + userProfile, + didLogout: () async { + // Pop the dialog using the dialog context + Navigator.of(dialogContext).pop(); + await runAppFlowy(); + }, + dismissDialog: () { + if (Navigator.of(dialogContext).canPop()) { + Navigator.of(dialogContext).pop(); + } else { + Log.warn("Can't pop dialog context"); + } + }, + restartApp: () async { + // Pop the dialog using the dialog context + Navigator.of(dialogContext).pop(); + await runAppFlowy(); + }, + ), + ); + }, + ); +} 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 0308dce706..473ca6f1d3 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 @@ -1,70 +1,32 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/hotkeys.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/settings/settings_dialog.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; -import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:hotkey_manager/hotkey_manager.dart'; - -final GlobalKey _settingsDialogKey = GlobalKey(); - -Future openSettingsHotKey(BuildContext context) async { - final userProfileOrFailure = await getIt().getUser(); - - return userProfileOrFailure.fold( - (userProfile) => HotKeyItem( - hotKey: HotKey( - KeyCode.comma, - scope: HotKeyScope.inapp, - modifiers: [ - PlatformExtension.isMacOS ? KeyModifier.meta : KeyModifier.control, - ], - ), - keyDownHandler: (_) { - if (_settingsDialogKey.currentContext == null) { - _showSettingsDialog(context, userProfile); - } else { - Navigator.of(context, rootNavigator: true) - .popUntil((route) => route.isFirst); - } - }, - ), - (e) { - Log.error('Failed to get user $e'); - return null; - }, - ); -} +// keep this widget in case we need to roll back (lucas.xu) class SidebarUser extends StatelessWidget { const SidebarUser({ super.key, - required this.user, + required this.userProfile, required this.views, }); - final UserProfilePB user; + final UserProfilePB userProfile; final List views; @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => MenuUserBloc(user) + create: (context) => MenuUserBloc(userProfile) ..add( const MenuUserEvent.initial(), ), @@ -106,61 +68,3 @@ class SidebarUser extends StatelessWidget { return name; } } - -class UserSettingButton extends StatelessWidget { - const UserSettingButton({required this.userProfile, super.key}); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.settings_menu_open.tr(), - child: IconButton( - onPressed: () => _showSettingsDialog(context, userProfile), - icon: SizedBox.square( - dimension: 20, - child: FlowySvg( - FlowySvgs.settings_m, - color: Theme.of(context).colorScheme.tertiary, - ), - ), - ), - ); - } -} - -void _showSettingsDialog( - BuildContext context, - UserProfilePB userProfile, -) { - showDialog( - context: context, - builder: (dialogContext) { - return BlocProvider.value( - key: _settingsDialogKey, - value: BlocProvider.of(dialogContext), - child: SettingsDialog( - userProfile, - didLogout: () async { - // Pop the dialog using the dialog context - Navigator.of(dialogContext).pop(); - await runAppFlowy(); - }, - dismissDialog: () { - if (Navigator.of(dialogContext).canPop()) { - Navigator.of(dialogContext).pop(); - } else { - Log.warn("Can't pop dialog context"); - } - }, - restartApp: () async { - // Pop the dialog using the dialog context - Navigator.of(dialogContext).pop(); - await runAppFlowy(); - }, - ), - ); - }, - ); -} 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 new file mode 100644 index 0000000000..c6fe4b4d5a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart @@ -0,0 +1,183 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_item_list.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'; +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 SidebarWorkspace extends StatelessWidget { + const SidebarWorkspace({ + super.key, + required this.userProfile, + required this.views, + }); + + final UserProfilePB userProfile; + final List views; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => UserWorkspaceBloc(userProfile: userProfile) + ..add(const UserWorkspaceEvent.fetchWorkspaces()), + child: BlocConsumer( + listener: _showResultDialog, + builder: (context, state) { + final currentWorkspace = state.currentWorkspace; + // todo: show something if there is no workspace + if (currentWorkspace == null) { + return const SizedBox.shrink(); + } + return Row( + children: [ + Expanded( + child: _WorkspaceWrapper( + userProfile: userProfile, + currentWorkspace: currentWorkspace, + ), + ), + UserSettingButton(userProfile: userProfile), + const HSpace(4), + NotificationButton(views: views), + ], + ); + }, + ), + ); + } + + void _showResultDialog(BuildContext context, UserWorkspaceState state) { + var result = state.createWorkspaceResult; + + if (result != null) { + final message = result.fold( + (s) => LocaleKeys.workspace_createSuccess.tr(), + (e) => '${LocaleKeys.workspace_createFailed.tr()}: ${e.msg}', + ); + return showSnackBarMessage(context, message); + } + + result = state.deleteWorkspaceResult; + if (result != null) { + final message = result.fold( + (s) => LocaleKeys.workspace_deleteSuccess.tr(), + (e) => '${LocaleKeys.workspace_deleteFailed.tr()}: ${e.msg}', + ); + showSnackBarMessage(context, message); + return; + } + + result = state.openWorkspaceResult; + if (result != null) { + final message = result.fold( + (s) => LocaleKeys.workspace_openSuccess.tr(), + (e) => '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}', + ); + showSnackBarMessage(context, message); + return; + } + } +} + +class _WorkspaceWrapper extends StatefulWidget { + const _WorkspaceWrapper({ + required this.userProfile, + required this.currentWorkspace, + }); + + final UserWorkspacePB currentWorkspace; + final UserProfilePB userProfile; + + @override + State<_WorkspaceWrapper> createState() => _WorkspaceWrapperState(); +} + +class _WorkspaceWrapperState extends State<_WorkspaceWrapper> { + @override + Widget build(BuildContext context) { + if (PlatformExtension.isDesktopOrWeb) { + return _DesktopWorkspaceWrapper( + userProfile: widget.userProfile, + currentWorkspace: widget.currentWorkspace, + ); + } else { + // TODO(Lucas) mobile workspace menu + return const Placeholder(); + } + } +} + +class _DesktopWorkspaceWrapper extends StatefulWidget { + const _DesktopWorkspaceWrapper({ + required this.userProfile, + required this.currentWorkspace, + }); + + final UserWorkspacePB currentWorkspace; + final UserProfilePB userProfile; + + @override + State<_DesktopWorkspaceWrapper> createState() => + _DesktopWorkspaceWrapperState(); +} + +class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> { + final controller = PopoverController(); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 10), + constraints: const BoxConstraints(maxWidth: 260, maxHeight: 600), + popupBuilder: (_) { + return BlocProvider.value( + value: context.read(), + child: BlocBuilder( + builder: (context, state) { + final currentWorkspace = state.currentWorkspace; + final workspaces = state.workspaces; + if (currentWorkspace == null || workspaces.isEmpty) { + return const SizedBox.shrink(); + } + return WorkspacesMenu( + userProfile: widget.userProfile, + currentWorkspace: currentWorkspace, + workspaces: workspaces, + ); + }, + ), + ); + }, + child: FlowyButton( + onTap: () => controller.show(), + margin: const EdgeInsets.symmetric(vertical: 8), + text: Row( + children: [ + const HSpace(4.0), + SizedBox( + width: 24.0, + child: WorkspaceIcon(workspace: widget.currentWorkspace), + ), + const HSpace(8), + FlowyText.medium( + widget.currentWorkspace.name, + overflow: TextOverflow.ellipsis, + ), + const FlowySvg(FlowySvgs.drop_menu_show_m), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart new file mode 100644 index 0000000000..84dd7abe52 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart @@ -0,0 +1,97 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +enum WorkspaceMoreAction { + rename, + delete, +} + +class WorkspaceMoreActionList extends StatelessWidget { + const WorkspaceMoreActionList({ + super.key, + required this.workspace, + }); + + final UserWorkspacePB workspace; + + @override + Widget build(BuildContext context) { + return PopoverActionList<_WorkspaceMoreActionWrapper>( + direction: PopoverDirection.bottomWithCenterAligned, + actions: WorkspaceMoreAction.values + .map((e) => _WorkspaceMoreActionWrapper(e, workspace)) + .toList(), + buildChild: (controller) { + return FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + useIntrinsicWidth: true, + text: const FlowySvg( + FlowySvgs.three_dots_vertical_s, + ), + onTap: () { + controller.show(); + }, + ); + }, + onSelected: (action, controller) {}, + ); + } +} + +class _WorkspaceMoreActionWrapper extends CustomActionCell { + _WorkspaceMoreActionWrapper(this.inner, this.workspace); + + final WorkspaceMoreAction inner; + final UserWorkspacePB workspace; + + @override + Widget buildWithContext(BuildContext context) { + return FlowyButton( + text: FlowyText( + name, + color: inner == WorkspaceMoreAction.delete + ? Theme.of(context).colorScheme.error + : null, + ), + margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + onTap: () async { + switch (inner) { + case WorkspaceMoreAction.delete: + await NavigatorAlertDialog( + title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), + confirm: () { + context.read().add( + UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId), + ); + }, + ).show(context); + case WorkspaceMoreAction.rename: + + // TODO(Lucas): integrate with the backend + } + + if (context.mounted) { + PopoverContainer.of(context).closeAll(); + } + }, + ); + } + + String get name { + switch (inner) { + case WorkspaceMoreAction.delete: + return LocaleKeys.button_delete.tr(); + case WorkspaceMoreAction.rename: + return LocaleKeys.button_rename.tr(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart new file mode 100644 index 0000000000..fdf7935482 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -0,0 +1,30 @@ +import 'package:appflowy/util/color_generator/color_generator.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class WorkspaceIcon extends StatelessWidget { + const WorkspaceIcon({ + super.key, + required this.workspace, + }); + + final UserWorkspacePB workspace; + + @override + Widget build(BuildContext context) { + // TODO(Lucas): support icon later + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: ColorGenerator.generateColorFromString(workspace.name), + borderRadius: BorderRadius.circular(4), + ), + child: FlowyText( + workspace.name.isEmpty ? '' : workspace.name.substring(0, 1), + fontSize: 16, + color: Colors.black, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_item_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_item_list.dart new file mode 100644 index 0000000000..359933970a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_item_list.dart @@ -0,0 +1,195 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class WorkspacesMenu extends StatelessWidget { + const WorkspacesMenu({ + super.key, + required this.userProfile, + required this.currentWorkspace, + required this.workspaces, + }); + + final UserProfilePB userProfile; + final UserWorkspacePB currentWorkspace; + final List workspaces; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // user email + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + child: Row( + children: [ + FlowyText.medium( + _getUserInfo(), + fontSize: 12.0, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).hintColor, + ), + const Spacer(), + FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg(FlowySvgs.add_m), + onTap: () { + _showCreateWorkspaceDialog(context); + PopoverContainer.of(context).closeAll(); + }, + ), + ], + ), + ), + for (final workspace in workspaces) ...[ + _WorkspaceMenuItem( + workspace: workspace, + userProfile: userProfile, + isSelected: workspace.workspaceId == currentWorkspace.workspaceId, + ), + const VSpace(4.0), + ], + ], + ); + } + + String _getUserInfo() { + if (userProfile.email.isNotEmpty) { + return userProfile.email; + } + + if (userProfile.name.isNotEmpty) { + return userProfile.name; + } + + return LocaleKeys.defaultUsername.tr(); + } + + Future _showCreateWorkspaceDialog(BuildContext context) async { + if (context.mounted) { + await NavigatorTextFieldDialog( + title: LocaleKeys.workspace_create.tr(), + value: '', + hintText: '', + autoSelectAllText: true, + onConfirm: (name, context) async { + final request = CreateWorkspacePB.create()..name = name; + final result = await UserEventCreateWorkspace(request).send(); + final message = result.fold( + (s) => LocaleKeys.workspace_createSuccess.tr(), + (e) => '${LocaleKeys.workspace_createFailed.tr()}: ${e.msg}', + ); + if (context.mounted) { + showSnackBarMessage(context, message); + } + }, + ).show(context); + } + } +} + +class _WorkspaceMenuItem extends StatelessWidget { + const _WorkspaceMenuItem({ + required this.workspace, + required this.userProfile, + required this.isSelected, + }); + + final UserProfilePB userProfile; + final UserWorkspacePB workspace; + final bool isSelected; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => + WorkspaceMemberBloc(userProfile: userProfile, workspace: workspace) + ..add(const WorkspaceMemberEvent.initial()) + ..add(const WorkspaceMemberEvent.getWorkspaceMembers()), + child: BlocBuilder( + builder: (context, state) { + final members = state.members; + // settings right icon inside the flowy button will + // cause the popover dismiss intermediately when click the right icon. + // so using the stack to put the right icon on the flowy button. + return Stack( + alignment: Alignment.center, + children: [ + FlowyButton( + onTap: () { + if (!isSelected) { + context.read().add( + UserWorkspaceEvent.openWorkspace( + workspace.workspaceId, + ), + ); + } + }, + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + iconPadding: 10.0, + leftIconSize: const Size.square(32), + leftIcon: WorkspaceIcon( + workspace: workspace, + ), + text: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.medium( + workspace.name, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), + if (members.length > 1) + FlowyText( + '${members.length} ${LocaleKeys.settings_appearance_members_members.tr()}', + fontSize: 10.0, + color: Theme.of(context).hintColor, + ), + ], + ), + ), + Positioned( + right: 12.0, + child: Align(child: _buildRightIcon(context)), + ), + ], + ); + }, + ), + ); + } + + 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) { + return const SizedBox.shrink(); + } + + return Row( + children: [ + WorkspaceMoreActionList(workspace: workspace), + const FlowySvg( + FlowySvgs.blue_check_s, + blendMode: null, + ), + ], + ); + } +} 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 5ce8d54288..3a2afacbfe 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; @@ -25,6 +23,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; typedef ViewItemOnSelected = void Function(ViewPB); @@ -456,7 +455,7 @@ class _SingleInnerViewItemState extends State { createViewAndShowRenameDialogIfNeeded( context, _convertLayoutToHintText(pluginBuilder.layoutType!), - (viewName) { + (viewName, _) { if (viewName.isNotEmpty) { viewBloc.add( ViewEvent.createView( @@ -499,7 +498,7 @@ class _SingleInnerViewItemState extends State { autoSelectAllText: true, value: widget.view.name, maxLength: 256, - confirm: (newValue) { + onConfirm: (newValue, _) { context.read().add(ViewEvent.rename(newValue)); }, ).show(context); 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 8bc317390e..2d9397d7cf 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 @@ -20,10 +20,26 @@ class WorkspaceMemberBloc extends Bloc { WorkspaceMemberBloc({ required this.userProfile, + this.workspace, }) : super(WorkspaceMemberState.initial()) { on((event, emit) async { - await event.map( - getWorkspaceMembers: (_) async { + await event.when( + initial: () async { + if (workspace != null) { + workspaceId = workspace!.workspaceId; + } else { + final currentWorkspace = + await FolderEventReadCurrentWorkspace().send(); + currentWorkspace.fold((s) { + workspaceId = s.id; + }, (e) { + assert(false, 'Failed to read current workspace: $e'); + Log.error('Failed to read current workspace: $e'); + workspaceId = ''; + }); + } + }, + getWorkspaceMembers: () async { final members = await _getWorkspaceMembers(); final myRole = _getMyRole(members); emit( @@ -33,16 +49,16 @@ class WorkspaceMemberBloc ), ); }, - addWorkspaceMember: (e) async { - await _addWorkspaceMember(e.email); + addWorkspaceMember: (email) async { + await _addWorkspaceMember(email); add(const WorkspaceMemberEvent.getWorkspaceMembers()); }, - removeWorkspaceMember: (e) async { - await _removeWorkspaceMember(e.email); + removeWorkspaceMember: (email) async { + await _removeWorkspaceMember(email); add(const WorkspaceMemberEvent.getWorkspaceMembers()); }, - updateWorkspaceMember: (e) async { - await _updateWorkspaceMember(e.email, e.role); + updateWorkspaceMember: (email, role) async { + await _updateWorkspaceMember(email, role); add(const WorkspaceMemberEvent.getWorkspaceMembers()); }, ); @@ -51,18 +67,16 @@ class WorkspaceMemberBloc final UserProfilePB userProfile; + // if the workspace is null, use the current workspace + final UserWorkspacePB? workspace; + + late final String workspaceId; + Future> _getWorkspaceMembers() async { - // will the current workspace be synced across the app? - final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); - return currentWorkspace.fold((s) async { - final data = QueryWorkspacePB()..workspaceId = s.id; - final result = await UserEventGetWorkspaceMember(data).send(); - return result.fold((s) => s.items, (e) { - Log.error('Failed to read workspace members: $e'); - return []; - }); - }, (e) { - Log.error('Failed to read current workspace: $e'); + 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 []; }); } @@ -81,60 +95,46 @@ class WorkspaceMemberBloc } Future _addWorkspaceMember(String email) async { - final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); - return currentWorkspace.fold((s) async { - final data = AddWorkspaceMemberPB() - ..workspaceId = s.id - ..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'); - }); + 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 read current workspace: $e'); + Log.error('Failed to add workspace member: $e'); }); } Future _removeWorkspaceMember(String email) async { - final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); - return currentWorkspace.fold((s) async { - final data = RemoveWorkspaceMemberPB() - ..workspaceId = s.id - ..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'); - }); + 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 read current workspace: $e'); + Log.error('Failed to remove workspace member: $e'); }); } Future _updateWorkspaceMember(String email, AFRolePB role) async { - final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); - return currentWorkspace.fold((s) async { - final data = UpdateWorkspaceMemberPB() - ..workspaceId = s.id - ..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'); - }); + 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 read current workspace: $e'); + Log.error('Failed to update workspace member: $e'); }); } } @freezed class WorkspaceMemberEvent with _$WorkspaceMemberEvent { + const factory WorkspaceMemberEvent.initial() = Initial; const factory WorkspaceMemberEvent.getWorkspaceMembers() = GetWorkspaceMembers; const factory WorkspaceMemberEvent.addWorkspaceMember(String email) = diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index f43e376340..724c9ad2be 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; @@ -454,25 +455,3 @@ class _MemberRoleActionWrapper extends ActionCell { throw UnimplementedError('Unknown role: $inner'); } } - -extension on AFRolePB { - bool get isOwner => this == AFRolePB.Owner; - - bool get canInvite => isOwner; - - bool get canDelete => isOwner; - - bool get canUpdate => isOwner; - - String get description { - switch (this) { - case AFRolePB.Owner: - return LocaleKeys.settings_appearance_members_owner.tr(); - case AFRolePB.Member: - return LocaleKeys.settings_appearance_members_member.tr(); - case AFRolePB.Guest: - return LocaleKeys.settings_appearance_members_guest.tr(); - } - throw UnimplementedError('Unknown role: $this'); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart index 78f21c4485..3e3e376df4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart @@ -217,7 +217,7 @@ class CloudTypeItem extends StatelessWidget { confirm: () async { onSelected(cloudType); }, - hideCancleButton: true, + hideCancelButton: true, ).show(context); } PopoverContainer.of(context).close(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 1b431b58d6..1458bcad5b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -1,15 +1,16 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; +import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy/startup/tasks/app_widget.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/style_widget/text_input.dart'; -import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; + export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; class NavigatorTextFieldDialog extends StatefulWidget { const NavigatorTextFieldDialog({ @@ -17,17 +18,19 @@ class NavigatorTextFieldDialog extends StatefulWidget { required this.title, this.autoSelectAllText = false, required this.value, - required this.confirm, - this.cancel, + required this.onConfirm, + this.onCancel, this.maxLength, + this.hintText, }); final String value; final String title; - final void Function()? cancel; - final void Function(String) confirm; + final VoidCallback? onCancel; + final void Function(String, BuildContext) onConfirm; final bool autoSelectAllText; final int? maxLength; + final String? hintText; @override State createState() => @@ -69,7 +72,8 @@ class _NavigatorTextFieldDialogState extends State { ), VSpace(Insets.m), FlowyFormTextInput( - hintText: LocaleKeys.dialogCreatePageNameHint.tr(), + hintText: + widget.hintText ?? LocaleKeys.dialogCreatePageNameHint.tr(), controller: controller, textStyle: Theme.of(context) .textTheme @@ -82,20 +86,18 @@ class _NavigatorTextFieldDialogState extends State { newValue = text; }, onEditingComplete: () { - widget.confirm(newValue); + widget.onConfirm(newValue, context); AppGlobals.nav.pop(); }, ), VSpace(Insets.xl), OkCancelButton( onOkPressed: () { - widget.confirm(newValue); + widget.onConfirm(newValue, context); Navigator.of(context).pop(); }, onCancelPressed: () { - if (widget.cancel != null) { - widget.cancel!(); - } + widget.onCancel?.call(); Navigator.of(context).pop(); }, ), @@ -111,13 +113,13 @@ class NavigatorAlertDialog extends StatefulWidget { required this.title, this.cancel, this.confirm, - this.hideCancleButton = false, + this.hideCancelButton = false, }); final String title; final void Function()? cancel; final void Function()? confirm; - final bool hideCancleButton; + final bool hideCancelButton; @override State createState() => _CreateFlowyAlertDialog(); @@ -158,7 +160,7 @@ class _CreateFlowyAlertDialog extends State { widget.confirm?.call(); Navigator.of(context).pop(); }, - onCancelPressed: widget.hideCancleButton + onCancelPressed: widget.hideCancelButton ? null : () { widget.cancel?.call(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart index 83a42249d5..bb285a7917 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart @@ -219,9 +219,9 @@ class HoverButton extends StatelessWidget { super.key, required this.onTap, required this.itemHeight, - required this.leftIcon, + this.leftIcon, required this.name, - required this.rightIcon, + this.rightIcon, }); final VoidCallback onTap; 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 766f012af8..66ccbe01e0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -29,7 +29,7 @@ class UserAvatar extends StatelessWidget { if (iconUrl.isEmpty) { final String nameOrDefault = _userName(name); - final Color color = ColorGenerator().generateColorFromString(name); + final Color color = ColorGenerator.generateColorFromString(name); const initialsCount = 2; // Taking the first letters of the name components and limiting to 2 elements diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index e2cd3bc48e..4f029c3699 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -88,7 +88,7 @@ class FlowyButton extends StatelessWidget { } Widget _render(BuildContext context) { - List children = List.empty(growable: true); + final List children = []; if (leftIcon != null) { children.add( diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index f07c4972a9..da6e80825f 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -64,7 +64,14 @@ "reportIssueOnGithub": "Report an issue on Github", "exportLogFiles": "Export log files", "reachOut": "Reach out on Discord" - } + }, + "deleteWorkspaceHintText": "Are you sure you want to delete the workspace? This action cannot be undone.", + "createSuccess": "Workspace created successfully", + "createFailed": "Failed to create workspace", + "deleteSuccess": "Workspace deleted successfully", + "deleteFailed": "Failed to delete workspace", + "openSuccess": "Open workspace successfully", + "openFailed": "Failed to open workspace" }, "shareAction": { "buttonText": "Share", @@ -414,7 +421,8 @@ "memberHintText": "A member can read, comment, and edit pages. Invite members and guests.", "guestHintText": "A Guest can read, react, comment, and can edit certain pages with permission.", "emailInvalidError": "Invalid email, please check and try again", - "emailSent": "Email sent, please check the inbox" + "emailSent": "Email sent, please check the inbox", + "members": "members" } }, "files": { diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 2bcd60bbf7..3ac2c8a7b8 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -195,7 +195,7 @@ pub enum UserEvent { #[event(input = "ImportAppFlowyDataPB")] ImportAppFlowyDataFolder = 41, - #[event(output = "CreateWorkspacePB")] + #[event(input = "CreateWorkspacePB", output = "UserWorkspacePB")] CreateWorkspace = 42, #[event(input = "UserWorkspaceIdPB")]