mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
f7233f6949
commit
8732c3c28b
@ -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/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/plugins/document/application/doc_bloc.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/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/image_util.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||||
@ -176,6 +177,7 @@ class _DocumentHeaderNodeWidgetState extends State<DocumentHeaderNodeWidget> {
|
|||||||
DocumentHeaderBlockKeys.coverDetails: coverDetails,
|
DocumentHeaderBlockKeys.coverDetails: coverDetails,
|
||||||
DocumentHeaderBlockKeys.icon:
|
DocumentHeaderBlockKeys.icon:
|
||||||
widget.node.attributes[DocumentHeaderBlockKeys.icon],
|
widget.node.attributes[DocumentHeaderBlockKeys.icon],
|
||||||
|
CustomImageBlockKeys.imageType: '1',
|
||||||
};
|
};
|
||||||
if (cover != null) {
|
if (cover != null) {
|
||||||
attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString();
|
attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString();
|
||||||
|
25
frontend/appflowy_flutter/lib/shared/feature_flags.dart
Normal file
25
frontend/appflowy_flutter/lib/shared/feature_flags.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ enum SettingsPage {
|
|||||||
notifications,
|
notifications,
|
||||||
cloud,
|
cloud,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
|
member,
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsDialogBloc
|
class SettingsDialogBloc
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.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_appearance_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_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_file_system_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_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/presentation/settings/widgets/settings_menu.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'widgets/setting_cloud.dart';
|
import 'widgets/setting_cloud.dart';
|
||||||
|
|
||||||
const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12);
|
const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12);
|
||||||
@ -110,6 +112,8 @@ class SettingsDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
case SettingsPage.shortcuts:
|
case SettingsPage.shortcuts:
|
||||||
return const SettingsCustomizeShortcutsWrapper();
|
return const SettingsCustomizeShortcutsWrapper();
|
||||||
|
case SettingsPage.member:
|
||||||
|
return WorkspaceMembersPage(userProfile: user);
|
||||||
default:
|
default:
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
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/application/settings/settings_dialog_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||||
import 'package:easy_localization/easy_localization.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/material.dart';
|
||||||
|
|
||||||
class SettingsMenu extends StatelessWidget {
|
class SettingsMenu extends StatelessWidget {
|
||||||
@ -16,7 +18,9 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return SingleChildScrollView(
|
||||||
|
child: SeparatedColumn(
|
||||||
|
separatorBuilder: () => const SizedBox(height: 10),
|
||||||
children: [
|
children: [
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.appearance,
|
page: SettingsPage.appearance,
|
||||||
@ -25,7 +29,6 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.brightness_4,
|
icon: Icons.brightness_4,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.language,
|
page: SettingsPage.language,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -33,7 +36,6 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.translate,
|
icon: Icons.translate,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.files,
|
page: SettingsPage.files,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -41,7 +43,6 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.file_present_outlined,
|
icon: Icons.file_present_outlined,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.user,
|
page: SettingsPage.user,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -49,7 +50,6 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.account_box_outlined,
|
icon: Icons.account_box_outlined,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.notifications,
|
page: SettingsPage.notifications,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -57,7 +57,6 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.notifications_outlined,
|
icon: Icons.notifications_outlined,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.cloud,
|
page: SettingsPage.cloud,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -65,7 +64,6 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.sync,
|
icon: Icons.sync,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.shortcuts,
|
page: SettingsPage.shortcuts,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -73,7 +71,16 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.cut,
|
icon: Icons.cut,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
|
if (FeatureFlag.membersSettings.isOn)
|
||||||
|
SettingsMenuElement(
|
||||||
|
page: SettingsPage.member,
|
||||||
|
selectedPage: currentPage,
|
||||||
|
label: LocaleKeys.settings_appearance_members_label.tr(),
|
||||||
|
icon: Icons.people,
|
||||||
|
changeSelectedPage: changeSelectedPage,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,14 @@ abstract class FlowyResult<S, F> {
|
|||||||
bool isFailure();
|
bool isFailure();
|
||||||
|
|
||||||
S? toNullable();
|
S? toNullable();
|
||||||
|
|
||||||
|
void onSuccess(
|
||||||
|
void Function(S s) onSuccess,
|
||||||
|
);
|
||||||
|
|
||||||
|
void onFailure(
|
||||||
|
void Function(F f) onFailure,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class FlowySuccess<S, F> implements FlowyResult<S, F> {
|
class FlowySuccess<S, F> implements FlowyResult<S, F> {
|
||||||
@ -64,6 +72,14 @@ class FlowySuccess<S, F> implements FlowyResult<S, F> {
|
|||||||
S? toNullable() {
|
S? toNullable() {
|
||||||
return _value;
|
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> {
|
class FlowyFailure<S, F> implements FlowyResult<S, F> {
|
||||||
@ -114,4 +130,12 @@ class FlowyFailure<S, F> implements FlowyResult<S, F> {
|
|||||||
S? toNullable() {
|
S? toNullable() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSuccess(void Function(S success) onSuccess) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onFailure(void Function(F failure) onFailure) {
|
||||||
|
onFailure(_error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'base_styled_button.dart';
|
import 'base_styled_button.dart';
|
||||||
import 'secondary_button.dart';
|
import 'secondary_button.dart';
|
||||||
|
|
||||||
@ -25,15 +26,18 @@ class PrimaryTextButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PrimaryButton 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 Widget child;
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
final TextButtonMode mode;
|
final TextButtonMode mode;
|
||||||
|
final Color? backgroundColor;
|
||||||
const PrimaryButton(
|
|
||||||
{super.key,
|
|
||||||
required this.child,
|
|
||||||
this.onPressed,
|
|
||||||
this.mode = TextButtonMode.big});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -41,7 +45,7 @@ class PrimaryButton extends StatelessWidget {
|
|||||||
minWidth: mode.size.width,
|
minWidth: mode.size.width,
|
||||||
minHeight: mode.size.height,
|
minHeight: mode.size.height,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
bgColor: Theme.of(context).colorScheme.primary,
|
bgColor: backgroundColor ?? Theme.of(context).colorScheme.primary,
|
||||||
hoverColor: Theme.of(context).colorScheme.primaryContainer,
|
hoverColor: Theme.of(context).colorScheme.primaryContainer,
|
||||||
borderRadius: mode.borderRadius,
|
borderRadius: mode.borderRadius,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
|
@ -13,6 +13,7 @@ class RoundedTextButton extends StatelessWidget {
|
|||||||
final Color? hoverColor;
|
final Color? hoverColor;
|
||||||
final Color? textColor;
|
final Color? textColor;
|
||||||
final double? fontSize;
|
final double? fontSize;
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
const RoundedTextButton({
|
const RoundedTextButton({
|
||||||
super.key,
|
super.key,
|
||||||
@ -26,6 +27,7 @@ class RoundedTextButton extends StatelessWidget {
|
|||||||
this.hoverColor,
|
this.hoverColor,
|
||||||
this.textColor,
|
this.textColor,
|
||||||
this.fontSize,
|
this.fontSize,
|
||||||
|
this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -48,6 +50,7 @@ class RoundedTextButton extends StatelessWidget {
|
|||||||
fillColor: fillColor ?? Theme.of(context).colorScheme.primary,
|
fillColor: fillColor ?? Theme.of(context).colorScheme.primary,
|
||||||
hoverColor:
|
hoverColor:
|
||||||
hoverColor ?? Theme.of(context).colorScheme.primaryContainer,
|
hoverColor ?? Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
padding: padding,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
10
frontend/resources/flowy_icons/24x/invite_member_link.svg
Normal file
10
frontend/resources/flowy_icons/24x/invite_member_link.svg
Normal 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 |
@ -397,7 +397,24 @@
|
|||||||
"twentyFourHour": "Twenty four hour"
|
"twentyFourHour": "Twenty four hour"
|
||||||
},
|
},
|
||||||
"showNamingDialogWhenCreatingPage": "Show naming dialog when creating a page",
|
"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": {
|
"files": {
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
|
@ -180,16 +180,16 @@ pub enum UserEvent {
|
|||||||
#[event(output = "NotificationSettingsPB")]
|
#[event(output = "NotificationSettingsPB")]
|
||||||
GetNotificationSettings = 36,
|
GetNotificationSettings = 36,
|
||||||
|
|
||||||
#[event(output = "AddWorkspaceMemberPB")]
|
#[event(input = "AddWorkspaceMemberPB")]
|
||||||
AddWorkspaceMember = 37,
|
AddWorkspaceMember = 37,
|
||||||
|
|
||||||
#[event(output = "RemoveWorkspaceMemberPB")]
|
#[event(input = "RemoveWorkspaceMemberPB")]
|
||||||
RemoveWorkspaceMember = 38,
|
RemoveWorkspaceMember = 38,
|
||||||
|
|
||||||
#[event(output = "UpdateWorkspaceMemberPB")]
|
#[event(input = "UpdateWorkspaceMemberPB")]
|
||||||
UpdateWorkspaceMember = 39,
|
UpdateWorkspaceMember = 39,
|
||||||
|
|
||||||
#[event(output = "QueryWorkspacePB")]
|
#[event(input = "QueryWorkspacePB", output = "RepeatedWorkspaceMemberPB")]
|
||||||
GetWorkspaceMember = 40,
|
GetWorkspaceMember = 40,
|
||||||
|
|
||||||
#[event(input = "ImportAppFlowyDataPB")]
|
#[event(input = "ImportAppFlowyDataPB")]
|
||||||
|
Loading…
Reference in New Issue
Block a user