feat: members settings (#4788)

* feat: add member settings

* feat: fetch workspace members from server

* feat: add translations

* feat: implement invite feature

* feat: support inviting people via email

* feat: support updating member role

* feat: add feature flag to control the visibilty of members settings
This commit is contained in:
Lucas.Xu 2024-03-03 08:36:12 +07:00 committed by GitHub
parent f7233f6949
commit 8732c3c28b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 808 additions and 75 deletions

View File

@ -7,6 +7,7 @@ import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/application/doc_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
@ -176,6 +177,7 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
DocumentHeaderBlockKeys.coverDetails: coverDetails,
DocumentHeaderBlockKeys.icon:
widget.node.attributes[DocumentHeaderBlockKeys.icon],
CustomImageBlockKeys.imageType: '1',
};
if (cover != null) {
attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString();

View File

@ -0,0 +1,25 @@
/// The [FeatureFlag] is used to control the front-end features of the app.
///
/// For example, if your feature is still under development,
/// you can set the value to `false` to hide the feature.
enum FeatureFlag {
// Feature flags
// used to control the visibility of the collaborative workspace feature
// if it's on, you can see the workspace list and the workspace settings
// in the top-left corner of the app
collaborativeWorkspace,
// used to control the visibility of the members settings
// if it's on, you can see the members settings in the settings page
membersSettings;
bool get isOn {
switch (this) {
case FeatureFlag.collaborativeWorkspace:
return false;
case FeatureFlag.membersSettings:
return false;
}
}
}

View File

@ -16,6 +16,7 @@ enum SettingsPage {
notifications,
cloud,
shortcuts,
member,
}
class SettingsDialogBloc

View File

@ -1,18 +1,20 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'widgets/setting_cloud.dart';
const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12);
@ -110,6 +112,8 @@ class SettingsDialog extends StatelessWidget {
);
case SettingsPage.shortcuts:
return const SettingsCustomizeShortcutsWrapper();
case SettingsPage.member:
return WorkspaceMembersPage(userProfile: user);
default:
return const SizedBox.shrink();
}

View File

@ -0,0 +1,158 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'workspace_member_bloc.freezed.dart';
// 1. get the workspace members
// 2. display the content based on the user role
// Owner:
// - invite member button
// - delete member button
// - member list
// Member:
// Guest:
// - member list
class WorkspaceMemberBloc
extends Bloc<WorkspaceMemberEvent, WorkspaceMemberState> {
WorkspaceMemberBloc({
required this.userProfile,
}) : super(WorkspaceMemberState.initial()) {
on<WorkspaceMemberEvent>((event, emit) async {
await event.map(
getWorkspaceMembers: (_) async {
final members = await _getWorkspaceMembers();
final myRole = _getMyRole(members);
emit(
state.copyWith(
members: members,
myRole: myRole,
),
);
},
addWorkspaceMember: (e) async {
await _addWorkspaceMember(e.email);
add(const WorkspaceMemberEvent.getWorkspaceMembers());
},
removeWorkspaceMember: (e) async {
await _removeWorkspaceMember(e.email);
add(const WorkspaceMemberEvent.getWorkspaceMembers());
},
updateWorkspaceMember: (e) async {
await _updateWorkspaceMember(e.email, e.role);
add(const WorkspaceMemberEvent.getWorkspaceMembers());
},
);
});
}
final UserProfilePB userProfile;
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');
return [];
});
}
AFRolePB _getMyRole(List<WorkspaceMemberPB> members) {
final role = members
.firstWhereOrNull(
(e) => e.email == userProfile.email,
)
?.role;
if (role == null) {
Log.error('Failed to get my role');
return AFRolePB.Guest;
}
return role;
}
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');
});
}, (e) {
Log.error('Failed to read current workspace: $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');
});
}, (e) {
Log.error('Failed to read current workspace: $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');
});
}, (e) {
Log.error('Failed to read current workspace: $e');
});
}
}
@freezed
class WorkspaceMemberEvent with _$WorkspaceMemberEvent {
const factory WorkspaceMemberEvent.getWorkspaceMembers() =
GetWorkspaceMembers;
const factory WorkspaceMemberEvent.addWorkspaceMember(String email) =
AddWorkspaceMember;
const factory WorkspaceMemberEvent.removeWorkspaceMember(String email) =
RemoveWorkspaceMember;
const factory WorkspaceMemberEvent.updateWorkspaceMember(
String email,
AFRolePB role,
) = UpdateWorkspaceMember;
}
@freezed
class WorkspaceMemberState with _$WorkspaceMemberState {
const factory WorkspaceMemberState({
@Default([]) List<WorkspaceMemberPB> members,
@Default(AFRolePB.Guest) AFRolePB myRole,
}) = _WorkspaceMemberState;
factory WorkspaceMemberState.initial() => const WorkspaceMemberState();
}

View File

@ -0,0 +1,478 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.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';
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:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
class WorkspaceMembersPage extends StatelessWidget {
const WorkspaceMembersPage({
super.key,
required this.userProfile,
});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
return BlocProvider<WorkspaceMemberBloc>(
create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
..add(
const WorkspaceMemberEvent.getWorkspaceMembers(),
),
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
builder: (context, state) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// title
FlowyText.semibold(
LocaleKeys.settings_appearance_members_title.tr(),
fontSize: 20,
),
if (state.myRole.canInvite) const _InviteMember(),
if (state.members.isNotEmpty)
_MemberList(
members: state.members,
userProfile: userProfile,
myRole: state.myRole,
),
],
),
);
},
),
);
}
}
class _InviteMember extends StatefulWidget {
const _InviteMember();
@override
State<_InviteMember> createState() => _InviteMemberState();
}
class _InviteMemberState extends State<_InviteMember> {
final _emailController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(12.0),
FlowyText.semibold(
LocaleKeys.settings_appearance_members_inviteMembers.tr(),
fontSize: 16.0,
),
const VSpace(8.0),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(
height: 48.0,
),
child: FlowyTextField(
controller: _emailController,
onEditingComplete: _inviteMember,
),
),
),
const HSpace(10.0),
SizedBox(
height: 48.0,
child: IntrinsicWidth(
child: RoundedTextButton(
title: LocaleKeys.settings_appearance_members_sendInvite.tr(),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
onPressed: _inviteMember,
),
),
),
],
),
const VSpace(16.0),
PrimaryButton(
backgroundColor: const Color(0xFFE0E0E0),
child: Padding(
padding: const EdgeInsets.only(
left: 20,
right: 24,
top: 8,
bottom: 8,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FlowySvg(
FlowySvgs.invite_member_link_m,
color: Colors.black,
),
const HSpace(8.0),
FlowyText(
LocaleKeys.settings_appearance_members_copyInviteLink.tr(),
color: Colors.black,
),
],
),
),
onPressed: () {
showSnackBarMessage(context, 'not implemented');
},
),
const VSpace(16.0),
const Divider(
height: 1.0,
thickness: 1.0,
),
],
);
}
void _inviteMember() {
final email = _emailController.text;
if (!isEmail(email)) {
showSnackBarMessage(
context,
LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
);
return;
}
context
.read<WorkspaceMemberBloc>()
.add(WorkspaceMemberEvent.addWorkspaceMember(email));
showSnackBarMessage(
context,
LocaleKeys.settings_appearance_members_emailSent.tr(),
);
}
}
class _MemberList extends StatelessWidget {
const _MemberList({
required this.members,
required this.myRole,
required this.userProfile,
});
final List<WorkspaceMemberPB> members;
final AFRolePB myRole;
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
return Column(
children: [
const VSpace(16.0),
SeparatedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
separatorBuilder: () => const Divider(),
children: [
const _MemberListHeader(),
...members.map(
(member) => _MemberItem(
member: member,
myRole: myRole,
userProfile: userProfile,
),
),
],
),
],
);
}
}
class _MemberListHeader extends StatelessWidget {
const _MemberListHeader();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.semibold(
LocaleKeys.settings_appearance_members_label.tr(),
fontSize: 16.0,
),
const VSpace(16.0),
Row(
children: [
Expanded(
child: FlowyText.semibold(
LocaleKeys.settings_appearance_members_user.tr(),
fontSize: 14.0,
),
),
Expanded(
child: FlowyText.semibold(
LocaleKeys.settings_appearance_members_role.tr(),
fontSize: 14.0,
),
),
const HSpace(28.0),
],
),
],
);
}
}
class _MemberItem extends StatelessWidget {
const _MemberItem({
required this.member,
required this.myRole,
required this.userProfile,
});
final WorkspaceMemberPB member;
final AFRolePB myRole;
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
final textColor = member.role.isOwner ? Theme.of(context).hintColor : null;
return Row(
children: [
Expanded(
child: FlowyText.medium(
member.name,
color: textColor,
fontSize: 14.0,
),
),
Expanded(
child: member.role.isOwner || !myRole.canUpdate
? FlowyText.medium(
member.role.description,
color: textColor,
fontSize: 14.0,
)
: _MemberRoleActionList(
member: member,
),
),
myRole.canDelete &&
member.email != userProfile.email // can't delete self
? _MemberMoreActionList(member: member)
: const HSpace(28.0),
],
);
}
}
enum _MemberMoreAction {
delete,
}
class _MemberMoreActionList extends StatelessWidget {
const _MemberMoreActionList({
required this.member,
});
final WorkspaceMemberPB member;
@override
Widget build(BuildContext context) {
return PopoverActionList<_MemberMoreActionWrapper>(
asBarrier: true,
direction: PopoverDirection.bottomWithCenterAligned,
actions: _MemberMoreAction.values
.map((e) => _MemberMoreActionWrapper(e, member))
.toList(),
buildChild: (controller) {
return FlowyButton(
useIntrinsicWidth: true,
text: const FlowySvg(
FlowySvgs.three_dots_vertical_s,
),
onTap: () {
controller.show();
},
);
},
onSelected: (action, controller) async {
switch (action.inner) {
case _MemberMoreAction.delete:
context.read<WorkspaceMemberBloc>().add(
WorkspaceMemberEvent.removeWorkspaceMember(
action.member.email,
),
);
break;
}
controller.close();
},
);
}
}
class _MemberMoreActionWrapper extends ActionCell {
_MemberMoreActionWrapper(this.inner, this.member);
final _MemberMoreAction inner;
final WorkspaceMemberPB member;
@override
String get name {
switch (inner) {
case _MemberMoreAction.delete:
return LocaleKeys.settings_appearance_members_removeFromWorkspace.tr();
}
}
}
class _MemberRoleActionList extends StatelessWidget {
const _MemberRoleActionList({
required this.member,
});
final WorkspaceMemberPB member;
@override
Widget build(BuildContext context) {
return PopoverActionList<_MemberRoleActionWrapper>(
asBarrier: true,
direction: PopoverDirection.bottomWithLeftAligned,
actions: [AFRolePB.Member, AFRolePB.Guest]
.map((e) => _MemberRoleActionWrapper(e, member))
.toList(),
offset: const Offset(0, 10),
buildChild: (controller) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => controller.show(),
child: Row(
children: [
FlowyText.medium(
member.role.description,
fontSize: 14.0,
),
const HSpace(8.0),
const FlowySvg(
FlowySvgs.drop_menu_show_s,
),
],
),
),
);
},
onSelected: (action, controller) async {
switch (action.inner) {
case AFRolePB.Member:
case AFRolePB.Guest:
context.read<WorkspaceMemberBloc>().add(
WorkspaceMemberEvent.updateWorkspaceMember(
action.member.email,
action.inner,
),
);
break;
case AFRolePB.Owner:
break;
}
controller.close();
},
);
}
}
class _MemberRoleActionWrapper extends ActionCell {
_MemberRoleActionWrapper(this.inner, this.member);
final AFRolePB inner;
final WorkspaceMemberPB member;
@override
Widget? rightIcon(Color iconColor) {
return SizedBox(
width: 58.0,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
FlowyTooltip(
message: tooltip,
child: const FlowySvg(
FlowySvgs.information_s,
// color: iconColor,
),
),
const Spacer(),
if (member.role == inner)
const FlowySvg(
FlowySvgs.checkmark_tiny_s,
),
],
),
);
}
@override
String get name {
switch (inner) {
case AFRolePB.Guest:
return LocaleKeys.settings_appearance_members_guest.tr();
case AFRolePB.Member:
return LocaleKeys.settings_appearance_members_member.tr();
case AFRolePB.Owner:
return LocaleKeys.settings_appearance_members_owner.tr();
}
throw UnimplementedError('Unknown role: $inner');
}
String get tooltip {
switch (inner) {
case AFRolePB.Guest:
return LocaleKeys.settings_appearance_members_guestHintText.tr();
case AFRolePB.Member:
return LocaleKeys.settings_appearance_members_memberHintText.tr();
case AFRolePB.Owner:
return '';
}
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');
}
}

View File

@ -1,7 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class SettingsMenu extends StatelessWidget {
@ -16,64 +18,69 @@ class SettingsMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
SettingsMenuElement(
page: SettingsPage.appearance,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_appearance.tr(),
icon: Icons.brightness_4,
changeSelectedPage: changeSelectedPage,
),
const SizedBox(height: 10),
SettingsMenuElement(
page: SettingsPage.language,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_language.tr(),
icon: Icons.translate,
changeSelectedPage: changeSelectedPage,
),
const SizedBox(height: 10),
SettingsMenuElement(
page: SettingsPage.files,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_files.tr(),
icon: Icons.file_present_outlined,
changeSelectedPage: changeSelectedPage,
),
const SizedBox(height: 10),
SettingsMenuElement(
page: SettingsPage.user,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_user.tr(),
icon: Icons.account_box_outlined,
changeSelectedPage: changeSelectedPage,
),
const SizedBox(height: 10),
SettingsMenuElement(
page: SettingsPage.notifications,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_notifications.tr(),
icon: Icons.notifications_outlined,
changeSelectedPage: changeSelectedPage,
),
const SizedBox(height: 10),
SettingsMenuElement(
page: SettingsPage.cloud,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_cloudSettings.tr(),
icon: Icons.sync,
changeSelectedPage: changeSelectedPage,
),
const SizedBox(height: 10),
SettingsMenuElement(
page: SettingsPage.shortcuts,
selectedPage: currentPage,
label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
icon: Icons.cut,
changeSelectedPage: changeSelectedPage,
),
],
return SingleChildScrollView(
child: SeparatedColumn(
separatorBuilder: () => const SizedBox(height: 10),
children: [
SettingsMenuElement(
page: SettingsPage.appearance,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_appearance.tr(),
icon: Icons.brightness_4,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.language,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_language.tr(),
icon: Icons.translate,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.files,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_files.tr(),
icon: Icons.file_present_outlined,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.user,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_user.tr(),
icon: Icons.account_box_outlined,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.notifications,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_notifications.tr(),
icon: Icons.notifications_outlined,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.cloud,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_cloudSettings.tr(),
icon: Icons.sync,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.shortcuts,
selectedPage: currentPage,
label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
icon: Icons.cut,
changeSelectedPage: changeSelectedPage,
),
if (FeatureFlag.membersSettings.isOn)
SettingsMenuElement(
page: SettingsPage.member,
selectedPage: currentPage,
label: LocaleKeys.settings_appearance_members_label.tr(),
icon: Icons.people,
changeSelectedPage: changeSelectedPage,
),
],
),
);
}
}

View File

@ -14,6 +14,14 @@ abstract class FlowyResult<S, F> {
bool isFailure();
S? toNullable();
void onSuccess(
void Function(S s) onSuccess,
);
void onFailure(
void Function(F f) onFailure,
);
}
class FlowySuccess<S, F> implements FlowyResult<S, F> {
@ -64,6 +72,14 @@ class FlowySuccess<S, F> implements FlowyResult<S, F> {
S? toNullable() {
return _value;
}
@override
void onSuccess(void Function(S success) onSuccess) {
onSuccess(_value);
}
@override
void onFailure(void Function(F failure) onFailure) {}
}
class FlowyFailure<S, F> implements FlowyResult<S, F> {
@ -114,4 +130,12 @@ class FlowyFailure<S, F> implements FlowyResult<S, F> {
S? toNullable() {
return null;
}
@override
void onSuccess(void Function(S success) onSuccess) {}
@override
void onFailure(void Function(F failure) onFailure) {
onFailure(_error);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'base_styled_button.dart';
import 'secondary_button.dart';
@ -25,15 +26,18 @@ class PrimaryTextButton extends StatelessWidget {
}
class PrimaryButton extends StatelessWidget {
const PrimaryButton({
super.key,
required this.child,
this.onPressed,
this.mode = TextButtonMode.big,
this.backgroundColor,
});
final Widget child;
final VoidCallback? onPressed;
final TextButtonMode mode;
const PrimaryButton(
{super.key,
required this.child,
this.onPressed,
this.mode = TextButtonMode.big});
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
@ -41,7 +45,7 @@ class PrimaryButton extends StatelessWidget {
minWidth: mode.size.width,
minHeight: mode.size.height,
contentPadding: EdgeInsets.zero,
bgColor: Theme.of(context).colorScheme.primary,
bgColor: backgroundColor ?? Theme.of(context).colorScheme.primary,
hoverColor: Theme.of(context).colorScheme.primaryContainer,
borderRadius: mode.borderRadius,
onPressed: onPressed,

View File

@ -13,6 +13,7 @@ class RoundedTextButton extends StatelessWidget {
final Color? hoverColor;
final Color? textColor;
final double? fontSize;
final EdgeInsets padding;
const RoundedTextButton({
super.key,
@ -26,6 +27,7 @@ class RoundedTextButton extends StatelessWidget {
this.hoverColor,
this.textColor,
this.fontSize,
this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
});
@override
@ -48,6 +50,7 @@ class RoundedTextButton extends StatelessWidget {
fillColor: fillColor ?? Theme.of(context).colorScheme.primary,
hoverColor:
hoverColor ?? Theme.of(context).colorScheme.primaryContainer,
padding: padding,
),
),
);

View File

@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="link">
<mask id="mask0_1032_7497" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect id="Bounding box" width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_1032_7497)">
<path id="link_2" d="M11 17H7C5.61667 17 4.4375 16.5125 3.4625 15.5375C2.4875 14.5625 2 13.3833 2 12C2 10.6167 2.4875 9.4375 3.4625 8.4625C4.4375 7.4875 5.61667 7 7 7H11V9H7C6.16667 9 5.45833 9.29167 4.875 9.875C4.29167 10.4583 4 11.1667 4 12C4 12.8333 4.29167 13.5417 4.875 14.125C5.45833 14.7083 6.16667 15 7 15H11V17ZM8 13V11H16V13H8ZM13 17V15H17C17.8333 15 18.5417 14.7083 19.125 14.125C19.7083 13.5417 20 12.8333 20 12C20 11.1667 19.7083 10.4583 19.125 9.875C18.5417 9.29167 17.8333 9 17 9H13V7H17C18.3833 7 19.5625 7.4875 20.5375 8.4625C21.5125 9.4375 22 10.6167 22 12C22 13.3833 21.5125 14.5625 20.5375 15.5375C19.5625 16.5125 18.3833 17 17 17H13Z" fill="#1C1B1F"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1019 B

View File

@ -397,7 +397,24 @@
"twentyFourHour": "Twenty four hour"
},
"showNamingDialogWhenCreatingPage": "Show naming dialog when creating a page",
"enableRTLToolbarItems": "Enable RTL toolbar items"
"enableRTLToolbarItems": "Enable RTL toolbar items",
"members": {
"title": "Members Settings",
"inviteMembers": "Invite Members",
"sendInvite": "Send Invite",
"copyInviteLink": "Copy Invite Link",
"label": "Members",
"user": "User",
"role": "Role",
"removeFromWorkspace": "Remove from Workspace",
"owner": "Owner",
"guest": "Guest",
"member": "Member",
"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"
}
},
"files": {
"copy": "Copy",

View File

@ -180,16 +180,16 @@ pub enum UserEvent {
#[event(output = "NotificationSettingsPB")]
GetNotificationSettings = 36,
#[event(output = "AddWorkspaceMemberPB")]
#[event(input = "AddWorkspaceMemberPB")]
AddWorkspaceMember = 37,
#[event(output = "RemoveWorkspaceMemberPB")]
#[event(input = "RemoveWorkspaceMemberPB")]
RemoveWorkspaceMember = 38,
#[event(output = "UpdateWorkspaceMemberPB")]
#[event(input = "UpdateWorkspaceMemberPB")]
UpdateWorkspaceMember = 39,
#[event(output = "QueryWorkspacePB")]
#[event(input = "QueryWorkspacePB", output = "RepeatedWorkspaceMemberPB")]
GetWorkspaceMember = 40,
#[event(input = "ImportAppFlowyDataPB")]