mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: show the workspace list on top-left corner (#4799)
This commit is contained in:
parent
63464cbf2e
commit
90146148b6
@ -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';
|
||||
|
@ -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<DatabaseTabBarBloc>().add(
|
||||
DatabaseTabBarEvent.renameView(view.id, newValue),
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
@ -79,15 +79,13 @@ class UserBackendService {
|
||||
return UserEventOpenAnonUser().send();
|
||||
}
|
||||
|
||||
Future<FlowyResult<List<WorkspacePB>, 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<FlowyResult<List<UserWorkspacePB>, FlowyError>> getWorkspaces() {
|
||||
return UserEventGetAllWorkspace().send().then((value) {
|
||||
return value.fold(
|
||||
(workspaces) => FlowyResult.success(workspaces.items),
|
||||
(error) => FlowyResult.failure(error),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> openWorkspace(String workspaceId) {
|
||||
@ -118,4 +116,18 @@ class UserBackendService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<FlowyResult<UserWorkspacePB, FlowyError>> createUserWorkspace(
|
||||
String name,
|
||||
) {
|
||||
final request = CreateWorkspacePB.create()..name = name;
|
||||
return UserEventCreateWorkspace(request).send();
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> deleteWorkspaceById(
|
||||
String workspaceId,
|
||||
) {
|
||||
final request = UserWorkspaceIdPB.create()..workspaceId = workspaceId;
|
||||
return UserEventDeleteWorkspace(request).send();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,2 @@
|
||||
export 'settings_user_bloc.dart';
|
||||
export '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<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
UserWorkspaceBloc({
|
||||
required this.userProfile,
|
||||
}) : _userService = UserBackendService(userId: userProfile.id),
|
||||
super(UserWorkspaceState.initial()) {
|
||||
on<UserWorkspaceEvent>(
|
||||
(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<UserWorkspacePB> 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<List<UserWorkspacePB>, FlowyError> workspacesOrFail,
|
||||
) = WorkspacesReceived;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class UserWorkspaceState with _$UserWorkspaceState {
|
||||
const factory UserWorkspaceState({
|
||||
required UserWorkspacePB? currentWorkspace,
|
||||
required List<UserWorkspacePB> workspaces,
|
||||
@Default(null) FlowyResult<void, FlowyError>? createWorkspaceResult,
|
||||
@Default(null) FlowyResult<void, FlowyError>? deleteWorkspaceResult,
|
||||
@Default(null) FlowyResult<void, FlowyError>? openWorkspaceResult,
|
||||
}) = _UserWorkspaceState;
|
||||
|
||||
factory UserWorkspaceState.initial() =>
|
||||
const UserWorkspaceState(currentWorkspace: null, workspaces: []);
|
||||
}
|
@ -48,7 +48,7 @@ class WorkspaceBloc extends Bloc<WorkspaceEvent, WorkspaceState> {
|
||||
emit(
|
||||
workspacesOrFailed.fold(
|
||||
(workspaces) => state.copyWith(
|
||||
workspaces: workspaces,
|
||||
workspaces: [],
|
||||
successOrFailure: FlowyResult.success(null),
|
||||
),
|
||||
(error) {
|
||||
|
@ -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));
|
||||
|
@ -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';
|
||||
|
||||
|
@ -119,7 +119,7 @@ class _PersonalFolderHeaderState extends State<PersonalFolderHeader> {
|
||||
createViewAndShowRenameDialogIfNeeded(
|
||||
context,
|
||||
LocaleKeys.newPageText.tr(),
|
||||
(viewName) {
|
||||
(viewName, _) {
|
||||
if (viewName.isNotEmpty) {
|
||||
context.read<MenuBloc>().add(
|
||||
MenuEvent.createApp(
|
||||
|
@ -15,7 +15,7 @@ import 'package:flutter/material.dart';
|
||||
Future<void> createViewAndShowRenameDialogIfNeeded(
|
||||
BuildContext context,
|
||||
String dialogTitle,
|
||||
void Function(String viewName) createView,
|
||||
void Function(String viewName, BuildContext context) createView,
|
||||
) async {
|
||||
final value = await getIt<KeyValueStorage>().getWithFormat(
|
||||
KVKeys.showRenameDialogWhenCreatingNewFile,
|
||||
@ -27,9 +27,9 @@ Future<void> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -23,7 +23,7 @@ class SidebarNewPageButton extends StatelessWidget {
|
||||
onPressed: () async => createViewAndShowRenameDialogIfNeeded(
|
||||
context,
|
||||
LocaleKeys.newPageText.tr(),
|
||||
(viewName) {
|
||||
(viewName, _) {
|
||||
if (viewName.isNotEmpty) {
|
||||
context.read<MenuBloc>().add(MenuEvent.createApp(viewName));
|
||||
}
|
||||
|
@ -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<HotKeyItem?> openSettingsHotKey(BuildContext context) async {
|
||||
final userProfileOrFailure = await getIt<AuthService>().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<DocumentAppearanceCubit>.value(
|
||||
key: _settingsDialogKey,
|
||||
value: BlocProvider.of<DocumentAppearanceCubit>(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();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
@ -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<HotKeyItem?> openSettingsHotKey(BuildContext context) async {
|
||||
final userProfileOrFailure = await getIt<AuthService>().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<ViewPB> views;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<MenuUserBloc>(
|
||||
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<DocumentAppearanceCubit>.value(
|
||||
key: _settingsDialogKey,
|
||||
value: BlocProvider.of<DocumentAppearanceCubit>(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();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -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<ViewPB> views;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<UserWorkspaceBloc>(
|
||||
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
|
||||
..add(const UserWorkspaceEvent.fetchWorkspaces()),
|
||||
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
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<UserWorkspaceBloc>.value(
|
||||
value: context.read<UserWorkspaceBloc>(),
|
||||
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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<UserWorkspaceBloc>().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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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<UserWorkspacePB> 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<void> _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<WorkspaceMemberBloc, WorkspaceMemberState>(
|
||||
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<UserWorkspaceBloc>().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<WorkspaceMemberBloc>().state.myRole.isOwner) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
WorkspaceMoreActionList(workspace: workspace),
|
||||
const FlowySvg(
|
||||
FlowySvgs.blue_check_s,
|
||||
blendMode: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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<SingleInnerViewItem> {
|
||||
createViewAndShowRenameDialogIfNeeded(
|
||||
context,
|
||||
_convertLayoutToHintText(pluginBuilder.layoutType!),
|
||||
(viewName) {
|
||||
(viewName, _) {
|
||||
if (viewName.isNotEmpty) {
|
||||
viewBloc.add(
|
||||
ViewEvent.createView(
|
||||
@ -499,7 +498,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
autoSelectAllText: true,
|
||||
value: widget.view.name,
|
||||
maxLength: 256,
|
||||
confirm: (newValue) {
|
||||
onConfirm: (newValue, _) {
|
||||
context.read<ViewBloc>().add(ViewEvent.rename(newValue));
|
||||
},
|
||||
).show(context);
|
||||
|
@ -20,10 +20,26 @@ class WorkspaceMemberBloc
|
||||
extends Bloc<WorkspaceMemberEvent, WorkspaceMemberState> {
|
||||
WorkspaceMemberBloc({
|
||||
required this.userProfile,
|
||||
this.workspace,
|
||||
}) : super(WorkspaceMemberState.initial()) {
|
||||
on<WorkspaceMemberEvent>((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<List<WorkspaceMemberPB>> _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<void> _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<void> _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<void> _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) =
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -217,7 +217,7 @@ class CloudTypeItem extends StatelessWidget {
|
||||
confirm: () async {
|
||||
onSelected(cloudType);
|
||||
},
|
||||
hideCancleButton: true,
|
||||
hideCancelButton: true,
|
||||
).show(context);
|
||||
}
|
||||
PopoverContainer.of(context).close();
|
||||
|
@ -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<NavigatorTextFieldDialog> createState() =>
|
||||
@ -69,7 +72,8 @@ class _NavigatorTextFieldDialogState extends State<NavigatorTextFieldDialog> {
|
||||
),
|
||||
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<NavigatorTextFieldDialog> {
|
||||
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<NavigatorAlertDialog> createState() => _CreateFlowyAlertDialog();
|
||||
@ -158,7 +160,7 @@ class _CreateFlowyAlertDialog extends State<NavigatorAlertDialog> {
|
||||
widget.confirm?.call();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onCancelPressed: widget.hideCancleButton
|
||||
onCancelPressed: widget.hideCancelButton
|
||||
? null
|
||||
: () {
|
||||
widget.cancel?.call();
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -88,7 +88,7 @@ class FlowyButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _render(BuildContext context) {
|
||||
List<Widget> children = List.empty(growable: true);
|
||||
final List<Widget> children = [];
|
||||
|
||||
if (leftIcon != null) {
|
||||
children.add(
|
||||
|
@ -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": {
|
||||
|
@ -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")]
|
||||
|
Loading…
Reference in New Issue
Block a user