diff --git a/frontend/appflowy_flutter/lib/features/share_tab/data/models/share_access_level.dart b/frontend/appflowy_flutter/lib/features/share_tab/data/models/share_access_level.dart index 38b7012430..7180c5e7f4 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/data/models/share_access_level.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/data/models/share_access_level.dart @@ -32,13 +32,13 @@ enum ShareAccessLevel { String get subtitle { switch (this) { case ShareAccessLevel.readOnly: - return 'Can\'t make changes'; + return LocaleKeys.shareTab_cantMakeChanges.tr(); case ShareAccessLevel.readAndComment: - return 'Can make any changes'; + return LocaleKeys.shareTab_canMakeAnyChanges.tr(); case ShareAccessLevel.readAndWrite: - return 'Can make any changes'; + return LocaleKeys.shareTab_canMakeAnyChanges.tr(); case ShareAccessLevel.fullAccess: - return 'Can make any changes'; + return LocaleKeys.shareTab_canMakeAnyChanges.tr(); } } diff --git a/frontend/appflowy_flutter/lib/features/share_tab/data/models/share_section_type.dart b/frontend/appflowy_flutter/lib/features/share_tab/data/models/share_section_type.dart index 61abd440e4..8fb9156b89 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/data/models/share_section_type.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/data/models/share_section_type.dart @@ -4,6 +4,7 @@ /// - shared: the shared section is shared, anyone in the shared section can view/edit it. /// - private: the shared section is private, only the users in the shared section can view/edit it. enum SharedSectionType { + unknown, public, shared, private; diff --git a/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/local_share_with_user_repository_impl.dart b/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/local_share_with_user_repository_impl.dart index 221f516988..1e14aa2cfc 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/local_share_with_user_repository_impl.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/local_share_with_user_repository_impl.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:appflowy/features/share_tab/data/models/models.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 'share_with_user_repository.dart'; @@ -11,41 +12,47 @@ class LocalShareWithUserRepositoryImpl extends ShareWithUserRepository { LocalShareWithUserRepositoryImpl(); final SharedUsers _sharedUsers = [ + // current user has full access SharedUser( email: 'lucas.xu@appflowy.io', name: 'Lucas Xu - Long long long long long name', - accessLevel: ShareAccessLevel.fullAccess, + accessLevel: ShareAccessLevel.readOnly, role: ShareRole.guest, avatarUrl: 'https://avatar.iran.liara.run/public', ), + // member user has read and write access SharedUser( email: 'vivian@appflowy.io', name: 'Vivian Wang', accessLevel: ShareAccessLevel.readAndWrite, - role: ShareRole.guest, + role: ShareRole.member, avatarUrl: 'https://avatar.iran.liara.run/public/girl', ), + // member user has read access SharedUser( email: 'shuheng@appflowy.io', name: 'Shuheng', - accessLevel: ShareAccessLevel.fullAccess, - role: ShareRole.owner, + accessLevel: ShareAccessLevel.readOnly, + role: ShareRole.member, avatarUrl: 'https://avatar.iran.liara.run/public/boy', ), + // guest user has read access SharedUser( email: 'guest_user_1@appflowy.io', - name: 'Guest User 1 - Long long long long long name', + name: 'Read Only Guest', accessLevel: ShareAccessLevel.readOnly, role: ShareRole.guest, avatarUrl: 'https://avatar.iran.liara.run/public/boy/10', ), + // guest user has read and write access SharedUser( email: 'guest_user_2@appflowy.io', - name: 'Guest User 2', - accessLevel: ShareAccessLevel.readOnly, - role: ShareRole.owner, + name: 'Read And Write Guest', + accessLevel: ShareAccessLevel.readAndWrite, + role: ShareRole.guest, avatarUrl: 'https://avatar.iran.liara.run/public/boy/11', ), + // Others SharedUser( email: 'member_user_1@appflowy.io', name: 'Member User 1', @@ -157,4 +164,21 @@ class LocalShareWithUserRepositoryImpl extends ShareWithUserRepository { return FlowySuccess(null); } + + @override + Future> getCurrentUserProfile() async { + // Simulate fetching current user profile + return FlowySuccess( + UserProfilePB() + ..email = 'lucas.xu@appflowy.io' + ..name = 'Lucas Xu', + ); + } + + @override + Future> getCurrentPageSectionType({ + required String pageId, + }) async { + return FlowySuccess(SharedSectionType.private); + } } diff --git a/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/rust_share_with_user_repository_impl.dart b/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/rust_share_with_user_repository_impl.dart index c70f210819..b32ebc9851 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/rust_share_with_user_repository_impl.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/rust_share_with_user_repository_impl.dart @@ -1,11 +1,15 @@ import 'package:appflowy/features/share_tab/data/models/models.dart'; import 'package:appflowy/features/util/extensions.dart'; +import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:collection/collection.dart'; import 'share_with_user_repository.dart'; @@ -70,6 +74,7 @@ class RustShareWithUserRepositoryImpl extends ShareWithUserRepository { viewId: pageId, emails: emails, accessLevel: accessLevel.accessLevel, + autoConfirm: true, // TODO: remove this after the backend is ready ); final result = await FolderEventSharePageWithUser(request).send(); @@ -82,7 +87,9 @@ class RustShareWithUserRepositoryImpl extends ShareWithUserRepository { return FlowySuccess(success); }, (failure) { - Log.error('sharePageWithUser: $failure'); + Log.error( + 'share page($pageId) with users($emails) with access level($accessLevel): $failure', + ); return FlowyFailure(failure); }, @@ -124,4 +131,34 @@ class RustShareWithUserRepositoryImpl extends ShareWithUserRepository { }, ); } + + @override + Future> getCurrentUserProfile() async { + final result = await UserEventGetUserProfile().send(); + return result; + } + + @override + Future> getCurrentPageSectionType({ + required String pageId, + }) async { + final request = ViewIdPB.create()..value = pageId; + final result = await FolderEventGetViewAncestors(request).send(); + final ancestors = result.fold( + (s) => s.items, + (f) => [], + ); + final space = ancestors.firstWhereOrNull((e) => e.isSpace); + + if (space == null) { + return FlowySuccess(SharedSectionType.unknown); + } + + final sectionType = switch (space.spacePermission) { + SpacePermission.publicToAll => SharedSectionType.public, + SpacePermission.private => SharedSectionType.private, + }; + + return FlowySuccess(sectionType); + } } diff --git a/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/share_with_user_repository.dart b/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/share_with_user_repository.dart index d8a1e88a3e..9251bfec2a 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/share_with_user_repository.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/data/repositories/share_with_user_repository.dart @@ -1,5 +1,6 @@ import 'package:appflowy/features/share_tab/data/models/models.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'; /// Abstract repository for sharing with users. @@ -38,4 +39,12 @@ abstract class ShareWithUserRepository { required String email, required ShareRole role, }); + + /// Get current user profile. + Future> getCurrentUserProfile(); + + /// Get current page is in public section or private section. + Future> getCurrentPageSectionType({ + required String pageId, + }); } diff --git a/frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_bloc.dart b/frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_bloc.dart index 4dc60d9004..1f6637ba3a 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_bloc.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_bloc.dart @@ -6,7 +6,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p import 'package:appflowy/plugins/shared/share/constants.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; @@ -50,24 +49,33 @@ class ShareTabBloc extends Bloc { return; } - final result = await UserBackendService.getCurrentUserProfile(); + final result = await repository.getCurrentUserProfile(); final currentUser = result.fold( (user) => user, (error) => null, ); + final sectionTypeResult = await repository.getCurrentPageSectionType( + pageId: pageId, + ); + final sectionType = sectionTypeResult.fold( + (type) => type, + (error) => SharedSectionType.unknown, + ); + final shareLink = ShareConstants.buildShareUrl( workspaceId: workspaceId, viewId: pageId, ); - final users = await _getLatestSharedUsersOrCurrentUsers(); + final users = await _getSharedUsers(); emit( state.copyWith( currentUser: currentUser, shareLink: shareLink, users: users, + sectionType: sectionType, ), ); } @@ -124,7 +132,7 @@ class ShareTabBloc extends Bloc { await result.fold( (_) async { - final users = await _getLatestSharedUsersOrCurrentUsers(); + final users = await _getSharedUsers(); emit( state.copyWith( @@ -161,7 +169,7 @@ class ShareTabBloc extends Bloc { await result.fold( (_) async { - final users = await _getLatestSharedUsersOrCurrentUsers(); + final users = await _getSharedUsers(); emit( state.copyWith( removeResult: FlowySuccess(null), @@ -196,7 +204,7 @@ class ShareTabBloc extends Bloc { await result.fold( (_) async { - final users = await _getLatestSharedUsersOrCurrentUsers(); + final users = await _getSharedUsers(); emit( state.copyWith( updateAccessLevelResult: FlowySuccess(null), @@ -296,7 +304,7 @@ class ShareTabBloc extends Bloc { await result.fold( (_) async { - final users = await _getLatestSharedUsersOrCurrentUsers(); + final users = await _getSharedUsers(); emit( state.copyWith( turnIntoMemberResult: FlowySuccess(null), @@ -315,7 +323,7 @@ class ShareTabBloc extends Bloc { ); } - Future _getLatestSharedUsersOrCurrentUsers() async { + Future _getSharedUsers() async { final shareResult = await repository.getSharedUsersInPage( pageId: pageId, ); diff --git a/frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_state.dart b/frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_state.dart index 88e8ae5ddb..43a501865d 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_state.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/logic/share_tab_state.dart @@ -15,6 +15,7 @@ class ShareTabState { this.shareLink = '', this.generalAccessRole, this.linkCopied = false, + this.sectionType = SharedSectionType.private, this.initialResult, this.shareResult, this.removeResult, @@ -30,6 +31,7 @@ class ShareTabState { final String shareLink; final ShareAccessLevel? generalAccessRole; final bool linkCopied; + final SharedSectionType sectionType; final FlowyResult? initialResult; final FlowyResult? shareResult; final FlowyResult? removeResult; @@ -45,6 +47,7 @@ class ShareTabState { String? shareLink, ShareAccessLevel? generalAccessRole, bool? linkCopied, + SharedSectionType? sectionType, FlowyResult? initialResult, FlowyResult? shareResult, FlowyResult? removeResult, @@ -60,6 +63,7 @@ class ShareTabState { shareLink: shareLink ?? this.shareLink, generalAccessRole: generalAccessRole ?? this.generalAccessRole, linkCopied: linkCopied ?? this.linkCopied, + sectionType: sectionType ?? this.sectionType, initialResult: initialResult, shareResult: shareResult, removeResult: removeResult, @@ -80,6 +84,7 @@ class ShareTabState { other.shareLink == shareLink && other.generalAccessRole == generalAccessRole && other.linkCopied == linkCopied && + other.sectionType == sectionType && other.initialResult == initialResult && other.shareResult == shareResult && other.removeResult == removeResult && @@ -98,6 +103,7 @@ class ShareTabState { shareLink, generalAccessRole, linkCopied, + sectionType, initialResult, shareResult, removeResult, @@ -108,6 +114,6 @@ class ShareTabState { @override String toString() { - return 'ShareTabState(currentUser: $currentUser, users: $users, availableUsers: $availableUsers, isLoading: $isLoading, errorMessage: $errorMessage, shareLink: $shareLink, generalAccessRole: $generalAccessRole, linkCopied: $linkCopied, initialResult: $initialResult, shareResult: $shareResult, removeResult: $removeResult, updateAccessLevelResult: $updateAccessLevelResult, turnIntoMemberResult: $turnIntoMemberResult)'; + return 'ShareTabState(currentUser: $currentUser, users: $users, availableUsers: $availableUsers, isLoading: $isLoading, errorMessage: $errorMessage, shareLink: $shareLink, generalAccessRole: $generalAccessRole, shareSectionType: $SharedSectionType, linkCopied: $linkCopied, initialResult: $initialResult, shareResult: $shareResult, removeResult: $removeResult, updateAccessLevelResult: $updateAccessLevelResult, turnIntoMemberResult: $turnIntoMemberResult)'; } } diff --git a/frontend/appflowy_flutter/lib/features/share_tab/presentation/share_tab.dart b/frontend/appflowy_flutter/lib/features/share_tab/presentation/share_tab.dart index 258cc416e4..f703a6c185 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/presentation/share_tab.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/presentation/share_tab.dart @@ -5,11 +5,13 @@ import 'package:appflowy/features/share_tab/presentation/widgets/copy_link_widge import 'package:appflowy/features/share_tab/presentation/widgets/general_access_section.dart'; import 'package:appflowy/features/share_tab/presentation/widgets/people_with_access_section.dart'; import 'package:appflowy/features/share_tab/presentation/widgets/share_with_user_widget.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:collection/collection.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'; @@ -66,35 +68,38 @@ class _ShareTabState extends State { return const SizedBox.shrink(); } - final currentUserRole = state.users + final currentUser = state.currentUser; + final accessLevel = state.users .firstWhereOrNull( - (user) => user.email == state.currentUser?.email, + (user) => user.email == currentUser?.email, ) - ?.role; + ?.accessLevel; + final isFullAccess = accessLevel == ShareAccessLevel.fullAccess; return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // share page with user by email - // hide this when the user is guest - if (currentUserRole != ShareRole.guest) ...[ - VSpace(theme.spacing.l), - ShareWithUserWidget( - controller: controller, - onInvite: (emails) => _onSharePageWithUser( - context, - emails: emails, - accessLevel: ShareAccessLevel.readOnly, - ), + // only user with full access can invite others + + VSpace(theme.spacing.l), + ShareWithUserWidget( + controller: controller, + disabled: !isFullAccess, + onInvite: (emails) => _onSharePageWithUser( + context, + emails: emails, + accessLevel: ShareAccessLevel.readOnly, ), - ], + ), // shared users if (state.users.isNotEmpty) ...[ VSpace(theme.spacing.l), PeopleWithAccessSection( + isInPublicPage: state.sectionType == SharedSectionType.public, currentUserEmail: state.currentUser?.email ?? '', users: state.users, callbacks: _buildPeopleWithAccessSectionCallbacks(context), @@ -102,14 +107,16 @@ class _ShareTabState extends State { ], // general access - VSpace(theme.spacing.m), - GeneralAccessSection( - group: SharedGroup( - id: widget.workspaceId, - name: widget.workspaceName, - icon: widget.workspaceIcon, + if (state.sectionType == SharedSectionType.public) ...[ + VSpace(theme.spacing.m), + GeneralAccessSection( + group: SharedGroup( + id: widget.workspaceId, + name: widget.workspaceName, + icon: widget.workspaceIcon, + ), ), - ), + ], // copy link VSpace(theme.spacing.xl), @@ -150,6 +157,7 @@ class _ShareTabState extends State { }, onRemoveAccess: (user) { // show a dialog to confirm the action when removing self access + final theme = AppFlowyTheme.of(context); final shareTabBloc = context.read(); final removingSelf = user.email == shareTabBloc.state.currentUser?.email; @@ -157,6 +165,9 @@ class _ShareTabState extends State { showConfirmDialog( context: context, title: 'Remove your own access', + titleStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), description: '', style: ConfirmPopupStyle.cancelAndOk, confirmLabel: 'Remove', @@ -186,19 +197,19 @@ class _ShareTabState extends State { controller.clear(); showToastNotification( - message: 'Invitation sent', + message: LocaleKeys.shareTab_invitationSent.tr(), ); }, (error) { String message; switch (error.code) { case ErrorCode.InvalidGuest: - message = 'The email is already in the list'; + message = LocaleKeys.shareTab_emailAlreadyInList.tr(); break; case ErrorCode.FreePlanGuestLimitExceeded: - message = 'Please upgrade to a Pro plan to invite more guests'; + message = LocaleKeys.shareTab_upgradeToProToInviteGuests.tr(); break; case ErrorCode.PaidPlanGuestLimitExceeded: - message = 'You have reached the maximum number of guests'; + message = LocaleKeys.shareTab_maxGuestsReached.tr(); break; default: message = error.msg; @@ -214,7 +225,7 @@ class _ShareTabState extends State { if (removeResult != null) { removeResult.fold((success) { showToastNotification( - message: 'Removed guest successfully', + message: LocaleKeys.shareTab_removedGuestSuccessfully.tr(), ); }, (error) { showToastNotification( @@ -228,7 +239,21 @@ class _ShareTabState extends State { if (updateAccessLevelResult != null) { updateAccessLevelResult.fold((success) { showToastNotification( - message: 'Updated access level successfully', + message: LocaleKeys.shareTab_updatedAccessLevelSuccessfully.tr(), + ); + }, (error) { + showToastNotification( + message: error.msg, + type: ToastificationType.error, + ); + }); + } + + final turnIntoMemberResult = state.turnIntoMemberResult; + if (turnIntoMemberResult != null) { + turnIntoMemberResult.fold((success) { + showToastNotification( + message: LocaleKeys.shareTab_turnedIntoMemberSuccessfully.tr(), ); }, (error) { showToastNotification( diff --git a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/access_level_list_widget.dart b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/access_level_list_widget.dart index fe1572436b..3b5ae66bb1 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/access_level_list_widget.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/access_level_list_widget.dart @@ -122,7 +122,6 @@ class AccessLevelListWidget extends StatelessWidget { selected: selectedAccessLevel == accessLevel, leading: FlowySvg( accessLevel.icon, - blendMode: null, ), // Show a checkmark icon for the currently selected access level trailing: selectedAccessLevel == accessLevel diff --git a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/copy_link_widget.dart b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/copy_link_widget.dart index cbc3f80ccd..c02f6e03db 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/copy_link_widget.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/copy_link_widget.dart @@ -39,7 +39,7 @@ class CopyLinkWidget extends StatelessWidget { HSpace(theme.spacing.m), Expanded( child: Text( - 'People above can access with the link', + LocaleKeys.shareTab_peopleAboveCanAccessWithTheLink.tr(), style: theme.textStyle.caption.standard( color: theme.textColorScheme.primary, ), diff --git a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/general_access_section.dart b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/general_access_section.dart index 320908f45b..16935befa9 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/general_access_section.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/general_access_section.dart @@ -1,6 +1,8 @@ import 'package:appflowy/features/share_tab/data/models/shared_group.dart'; import 'package:appflowy/features/share_tab/presentation/widgets/shared_group_widget.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; class GeneralAccessSection extends StatelessWidget { @@ -15,7 +17,7 @@ class GeneralAccessSection extends StatelessWidget { Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); return AFMenuSection( - title: 'General access', + title: LocaleKeys.shareTab_generalAccess.tr(), padding: EdgeInsets.symmetric( vertical: theme.spacing.xs, horizontal: theme.spacing.m, diff --git a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/people_with_access_section.dart b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/people_with_access_section.dart index 6416bcee57..643743ef8c 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/people_with_access_section.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/people_with_access_section.dart @@ -36,11 +36,13 @@ class PeopleWithAccessSection extends StatelessWidget { super.key, required this.currentUserEmail, required this.users, + required this.isInPublicPage, this.callbacks, }); final String currentUserEmail; final SharedUsers users; + final bool isInPublicPage; final PeopleWithAccessSectionCallbacks? callbacks; @override @@ -67,6 +69,7 @@ class PeopleWithAccessSection extends StatelessWidget { return SharedUserWidget( user: user, currentUser: currentUser, + isInPublicPage: isInPublicPage, callbacks: AccessLevelListCallbacks( onRemoveAccess: () => callbacks?.onRemoveAccess.call(user), onTurnIntoMember: () => callbacks?.onTurnIntoMember.call(user), diff --git a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/share_with_user_widget.dart b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/share_with_user_widget.dart index 2b9c90c726..326a296fa1 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/share_with_user_widget.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/share_with_user_widget.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.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:string_validator/string_validator.dart'; @@ -10,10 +11,14 @@ class ShareWithUserWidget extends StatefulWidget { super.key, required this.onInvite, this.controller, + this.disabled = false, + this.tooltip, }); - final void Function(List emails) onInvite; final TextEditingController? controller; + final void Function(List emails) onInvite; + final bool disabled; + final String? tooltip; @override State createState() => _ShareWithUserWidgetState(); @@ -44,7 +49,7 @@ class _ShareWithUserWidgetState extends State { Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); - return Row( + final Widget child = Row( children: [ Expanded( child: AFTextField( @@ -63,6 +68,18 @@ class _ShareWithUserWidgetState extends State { ), ], ); + + if (widget.disabled) { + return FlowyTooltip( + message: + widget.tooltip ?? LocaleKeys.shareTab_onlyFullAccessCanInvite.tr(), + child: IgnorePointer( + child: child, + ), + ); + } + + return child; } void _onTextChanged() { diff --git a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_group_widget.dart b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_group_widget.dart index c475167fa9..bbc568996b 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_group_widget.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_group_widget.dart @@ -34,22 +34,17 @@ class SharedGroupWidget extends StatelessWidget { } Widget _buildLeading(BuildContext context) { - return AFAvatar( - child: Padding( - padding: const EdgeInsets.all(3.0), - child: WorkspaceIcon( - isEditable: false, - workspaceIcon: group.icon, - workspaceName: group.name, - iconSize: 32.0, - emojiSize: 24.0, - fontSize: 16.0, - onSelected: (r) {}, - borderRadius: 8.0, - showBorder: false, - figmaLineHeight: 24.0, - ), - ), + return WorkspaceIcon( + isEditable: false, + workspaceIcon: group.icon, + workspaceName: group.name, + iconSize: 32.0, + emojiSize: 24.0, + fontSize: 16.0, + onSelected: (r) {}, + borderRadius: 8.0, + showBorder: false, + figmaLineHeight: 24.0, ); } diff --git a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_user_widget.dart b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_user_widget.dart index 4a3f3e1917..77736f5499 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_user_widget.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_user_widget.dart @@ -15,12 +15,14 @@ class SharedUserWidget extends StatelessWidget { super.key, required this.user, required this.currentUser, + required this.isInPublicPage, this.callbacks, }); final SharedUser user; final SharedUser currentUser; final AccessLevelListCallbacks? callbacks; + final bool isInPublicPage; @override Widget build(BuildContext context) { @@ -96,68 +98,74 @@ class SharedUserWidget extends StatelessWidget { ); } - Widget _buildTrailing( - BuildContext context, - ) { + Widget _buildTrailing(BuildContext context) { final isCurrentUser = user.email == currentUser.email; final theme = AppFlowyTheme.of(context); - // Guest: read and write, read only - // Member: full access <- only have full access until the backend supports more access levels - // Owner: all access levels - final supportedAccessLevels = switch (user.role) { - ShareRole.guest => [ - ShareAccessLevel.readOnly, - ShareAccessLevel.readAndWrite, - ], - ShareRole.member => [ShareAccessLevel.fullAccess], - ShareRole.owner => [ShareAccessLevel.fullAccess], - }; + final currentAccessLevel = currentUser.accessLevel; - Widget editAccessWidget; + Widget disabledAccessButton() => AFGhostTextButton.disabled( + text: user.accessLevel.title, + textStyle: theme.textStyle.body.standard( + color: theme.textColorScheme.secondary, + ), + ); - // The current guest user can't edit the access level of the other user - if (isCurrentUser && currentUser.role == ShareRole.guest) { - editAccessWidget = EditAccessLevelWidget( - selectedAccessLevel: user.accessLevel, - supportedAccessLevels: [], - additionalUserManagementOptions: [ - AdditionalUserManagementOptions.removeAccess, - ], - callbacks: callbacks ?? AccessLevelListCallbacks.none(), - ); - } else if (currentUser.role == ShareRole.guest || - user.role == ShareRole.member || - user.role == ShareRole.owner) { - editAccessWidget = AFGhostTextButton.disabled( - text: user.accessLevel.title, - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, - ), - ); - } else { - editAccessWidget = EditAccessLevelWidget( - selectedAccessLevel: user.accessLevel, - supportedAccessLevels: supportedAccessLevels, - additionalUserManagementOptions: [ - AdditionalUserManagementOptions.removeAccess, - ], - callbacks: callbacks ?? AccessLevelListCallbacks.none(), - ); + Widget editAccessWidget(List supported) => + EditAccessLevelWidget( + selectedAccessLevel: user.accessLevel, + supportedAccessLevels: supported, + additionalUserManagementOptions: [ + AdditionalUserManagementOptions.removeAccess, + ], + callbacks: callbacks ?? AccessLevelListCallbacks.none(), + ); + + // In public page, member/owner permissions are fixed + if (isInPublicPage && + (user.role == ShareRole.member || user.role == ShareRole.owner)) { + return disabledAccessButton(); } - if (user.role == ShareRole.guest) { + // Full access user can turn a guest into a member + if (user.role == ShareRole.guest && + currentAccessLevel == ShareAccessLevel.fullAccess) { return Row( children: [ TurnIntoMemberWidget( - onTap: () { - callbacks?.onTurnIntoMember.call(); - }, + onTap: () => callbacks?.onTurnIntoMember.call(), ), - editAccessWidget, + editAccessWidget([ + ShareAccessLevel.readOnly, + ShareAccessLevel.readAndWrite, + ]), ], ); } - return editAccessWidget; + // Self-management + if (isCurrentUser) { + if (currentAccessLevel == ShareAccessLevel.readOnly || + currentAccessLevel == ShareAccessLevel.readAndWrite) { + // Can only remove self + return editAccessWidget([]); + } else if (currentAccessLevel == ShareAccessLevel.fullAccess) { + // Full access user cannot change own access + return disabledAccessButton(); + } + } + + // Managing others + if (currentAccessLevel == ShareAccessLevel.readOnly || + currentAccessLevel == ShareAccessLevel.readAndWrite) { + // Cannot change others' access + return disabledAccessButton(); + } else { + // Full access user can manage others + final supportedAccessLevels = [ + ShareAccessLevel.readOnly, + ShareAccessLevel.readAndWrite, + ]; + return editAccessWidget(supportedAccessLevels); + } } } diff --git a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/turn_into_member_widget.dart b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/turn_into_member_widget.dart index ec41e846a2..bd9a503508 100644 --- a/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/turn_into_member_widget.dart +++ b/frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/turn_into_member_widget.dart @@ -1,5 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; class TurnIntoMemberWidget extends StatelessWidget { @@ -13,12 +16,15 @@ class TurnIntoMemberWidget extends StatelessWidget { @override Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); - return AFGhostButton.normal( - onTap: onTap, - padding: EdgeInsets.all(theme.spacing.s), - builder: (context, isHovering, disabled) { - return FlowySvg(FlowySvgs.turn_into_member_m); - }, + return FlowyTooltip( + message: LocaleKeys.shareTab_turnIntoMember.tr(), + child: AFGhostButton.normal( + onTap: onTap, + padding: EdgeInsets.all(theme.spacing.s), + builder: (context, isHovering, disabled) { + return FlowySvg(FlowySvgs.turn_into_member_m); + }, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart index 76aba27dc0..c8b3ad4c71 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart'; @@ -54,6 +55,7 @@ class AIChatPagePlugin extends Plugin { }) : notifier = ViewPluginNotifier(view: view); late final ViewInfoBloc _viewInfoBloc; + late final PageAccessLevelBloc _pageAccessLevelBloc; late final _chatMessageSelectorBloc = ChatSelectMessageBloc(viewNotifier: notifier); @@ -63,6 +65,7 @@ class AIChatPagePlugin extends Plugin { @override PluginWidgetBuilder get widgetBuilder => AIChatPagePluginWidgetBuilder( viewInfoBloc: _viewInfoBloc, + pageAccessLevelBloc: _pageAccessLevelBloc, chatMessageSelectorBloc: _chatMessageSelectorBloc, notifier: notifier, ); @@ -77,11 +80,14 @@ class AIChatPagePlugin extends Plugin { void init() { _viewInfoBloc = ViewInfoBloc(view: notifier.view) ..add(const ViewInfoEvent.started()); + _pageAccessLevelBloc = PageAccessLevelBloc(view: notifier.view) + ..add(const PageAccessLevelEvent.initial()); } @override void dispose() { _viewInfoBloc.close(); + _pageAccessLevelBloc.close(); _chatMessageSelectorBloc.close(); notifier.dispose(); } @@ -91,11 +97,13 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder with NavigationItem { AIChatPagePluginWidgetBuilder({ required this.viewInfoBloc, + required this.pageAccessLevelBloc, required this.chatMessageSelectorBloc, required this.notifier, }); final ViewInfoBloc viewInfoBloc; + final PageAccessLevelBloc pageAccessLevelBloc; final ChatSelectMessageBloc chatMessageSelectorBloc; final ViewPluginNotifier notifier; int? deletedViewIndex; @@ -104,8 +112,12 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder String? get viewName => notifier.view.nameOrDefault; @override - Widget get leftBarItem => - ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view); + Widget get leftBarItem { + return BlocProvider.value( + value: pageAccessLevelBloc, + child: ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view), + ); + } @override Widget tabBarItem(String pluginId, [bool shortForm = false]) => @@ -128,6 +140,7 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder providers: [ BlocProvider.value(value: chatMessageSelectorBloc), BlocProvider.value(value: viewInfoBloc), + BlocProvider.value(value: pageAccessLevelBloc), ], child: AIChatPage( userProfile: context.userProfile!, diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index bdebfb1412..007e866ab6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; @@ -345,6 +346,7 @@ class DatabaseTabBarViewPlugin extends Plugin { final PluginType _pluginType; late final ViewInfoBloc _viewInfoBloc; + late final PageAccessLevelBloc _pageAccessLevelBloc; /// Used to open a Row on plugin load /// @@ -353,6 +355,7 @@ class DatabaseTabBarViewPlugin extends Plugin { @override PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder( bloc: _viewInfoBloc, + pageAccessLevelBloc: _pageAccessLevelBloc, notifier: notifier, initialRowId: initialRowId, ); @@ -367,11 +370,14 @@ class DatabaseTabBarViewPlugin extends Plugin { void init() { _viewInfoBloc = ViewInfoBloc(view: notifier.view) ..add(const ViewInfoEvent.started()); + _pageAccessLevelBloc = PageAccessLevelBloc(view: notifier.view) + ..add(const PageAccessLevelEvent.initial()); } @override void dispose() { _viewInfoBloc.close(); + _pageAccessLevelBloc.close(); notifier.dispose(); } } @@ -398,11 +404,13 @@ class DatabasePluginWidgetBuilderSize { class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { DatabasePluginWidgetBuilder({ required this.bloc, + required this.pageAccessLevelBloc, required this.notifier, this.initialRowId, }); final ViewInfoBloc bloc; + final PageAccessLevelBloc pageAccessLevelBloc; final ViewPluginNotifier notifier; /// Used to open a Row on plugin load @@ -413,8 +421,12 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { String? get viewName => notifier.view.nameOrDefault; @override - Widget get leftBarItem => - ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view); + Widget get leftBarItem { + return BlocProvider.value( + value: pageAccessLevelBloc, + child: ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view), + ); + } @override Widget tabBarItem(String pluginId, [bool shortForm = false]) => @@ -464,8 +476,15 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { @override Widget? get rightBarItem { final view = notifier.view; - return BlocProvider.value( - value: bloc, + return MultiBlocProvider( + providers: [ + BlocProvider.value( + value: bloc, + ), + BlocProvider.value( + value: pageAccessLevelBloc, + ), + ], child: Row( children: [ ShareButton(key: ValueKey(view.id), view: view), diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 4ebc6f1b47..b8703cdee6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -1,5 +1,6 @@ library; +import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; @@ -60,6 +61,7 @@ class DocumentPlugin extends Plugin { late PluginType _pluginType; late final ViewInfoBloc _viewInfoBloc; + late final PageAccessLevelBloc _pageAccessLevelBloc; @override final ViewPluginNotifier notifier; @@ -73,6 +75,7 @@ class DocumentPlugin extends Plugin { @override PluginWidgetBuilder get widgetBuilder => DocumentPluginWidgetBuilder( bloc: _viewInfoBloc, + pageAccessLevelBloc: _pageAccessLevelBloc, notifier: notifier, initialSelection: initialSelection, initialBlockId: initialBlockId, @@ -88,11 +91,14 @@ class DocumentPlugin extends Plugin { void init() { _viewInfoBloc = ViewInfoBloc(view: notifier.view) ..add(const ViewInfoEvent.started()); + _pageAccessLevelBloc = PageAccessLevelBloc(view: notifier.view) + ..add(const PageAccessLevelEvent.initial()); } @override void dispose() { _viewInfoBloc.close(); + _pageAccessLevelBloc.close(); notifier.dispose(); } } @@ -104,10 +110,12 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder required this.notifier, this.initialSelection, this.initialBlockId, + required this.pageAccessLevelBloc, }); final ViewInfoBloc bloc; final ViewPluginNotifier notifier; + final PageAccessLevelBloc pageAccessLevelBloc; ViewPB get view => notifier.view; int? deletedViewIndex; @@ -139,8 +147,15 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder PickerTabType.custom, ]; - return BlocProvider.value( - value: bloc, + return MultiBlocProvider( + providers: [ + BlocProvider.value( + value: bloc, + ), + BlocProvider.value( + value: pageAccessLevelBloc, + ), + ], child: BlocBuilder( builder: (_, state) => DocumentPage( key: ValueKey(view.id), @@ -159,7 +174,12 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder String? get viewName => notifier.view.nameOrDefault; @override - Widget get leftBarItem => ViewTitleBar(key: ValueKey(view.id), view: view); + Widget get leftBarItem { + return BlocProvider.value( + value: pageAccessLevelBloc, + child: ViewTitleBar(key: ValueKey(view.id), view: view), + ); + } @override Widget tabBarItem(String pluginId, [bool shortForm = false]) => @@ -167,8 +187,15 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder @override Widget? get rightBarItem { - return BlocProvider.value( - value: bloc, + return MultiBlocProvider( + providers: [ + BlocProvider.value( + value: bloc, + ), + BlocProvider.value( + value: pageAccessLevelBloc, + ), + ], child: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index cc28324756..69cc523123 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -87,10 +87,6 @@ class _DocumentPageState extends State providers: [ BlocProvider.value(value: getIt()), BlocProvider.value(value: documentBloc), - BlocProvider.value( - value: PageAccessLevelBloc(view: widget.view) - ..add(PageAccessLevelEvent.initial()), - ), BlocProvider( create: (context) => ViewBloc(view: widget.view)..add(const ViewEvent.initial()), diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart index fec15eb219..edf2968313 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/features/share_tab/data/repositories/rust_share_with_user_repository_impl.dart'; +import 'package:appflowy/features/share_tab/data/repositories/local_share_with_user_repository_impl.dart'; import 'package:appflowy/features/share_tab/logic/share_tab_bloc.dart'; import 'package:appflowy/features/workspace/logic/workspace_bloc.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -44,7 +44,8 @@ class ShareButton extends StatelessWidget { ), BlocProvider( create: (context) => ShareTabBloc( - repository: RustShareWithUserRepositoryImpl(), + // repository: RustShareWithUserRepositoryImpl(), + repository: LocalShareWithUserRepositoryImpl(), pageId: view.id, workspaceId: workspaceId, )..add(ShareTabEvent.initialize()), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart index 143955f8fd..a8935e01f9 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart @@ -25,15 +25,13 @@ class DebugTask extends LaunchTask { Bloc.observer = TalkerBlocObserver( talker: talker, settings: TalkerBlocLoggerSettings( - // Disabled by default to prevent mixing with AppFlowy logs - // Enable to observe all bloc events enabled: false, printEventFullData: false, printStateFullData: false, printChanges: true, printClosings: true, printCreations: true, - transitionFilter: (_, transition) { + transitionFilter: (bloc, transition) { // By default, observe all transitions // You can add your own filter here if needed // when you want to observer a specific bloc diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index 31fcf6c277..ff87b15090 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -280,6 +280,8 @@ class ConfirmPopup extends StatefulWidget { required this.onConfirm, this.onCancel, this.confirmLabel, + this.titleStyle, + this.descriptionStyle, this.confirmButtonColor, this.confirmButtonBuilder, this.child, @@ -289,7 +291,9 @@ class ConfirmPopup extends StatefulWidget { }); final String title; + final TextStyle? titleStyle; final String description; + final TextStyle? descriptionStyle; final VoidCallback onConfirm; final VoidCallback? onCancel; final Color? confirmButtonColor; @@ -392,9 +396,10 @@ class _ConfirmPopupState extends State { Expanded( child: Text( widget.title, - style: theme.textStyle.heading4.prominent( - color: ConfirmPopupColor.titleColor(context), - ), + style: widget.titleStyle ?? + theme.textStyle.heading4.prominent( + color: ConfirmPopupColor.titleColor(context), + ), overflow: TextOverflow.ellipsis, ), ), @@ -423,9 +428,10 @@ class _ConfirmPopupState extends State { return Text( widget.description, - style: theme.textStyle.body.standard( - color: ConfirmPopupColor.descriptionColor(context), - ), + style: widget.descriptionStyle ?? + theme.textStyle.body.standard( + color: ConfirmPopupColor.descriptionColor(context), + ), maxLines: 5, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 8d65ee23bb..7c14fb0f14 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -602,6 +602,8 @@ Future showConfirmDialog({ required BuildContext context, required String title, required String description, + TextStyle? titleStyle, + TextStyle? descriptionStyle, VoidCallback? onConfirm, VoidCallback? onCancel, String? confirmLabel, @@ -620,6 +622,8 @@ Future showConfirmDialog({ child: ConfirmPopup( title: title, description: description, + titleStyle: titleStyle, + descriptionStyle: descriptionStyle, confirmButtonBuilder: confirmButtonBuilder, onConfirm: () => onConfirm?.call(), onCancel: () => onCancel?.call(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index ba6fc360cf..eefc40ef2f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -1,10 +1,10 @@ import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart'; import 'package:appflowy/features/share_tab/data/models/share_access_level.dart'; +import 'package:appflowy/features/workspace/logic/workspace_bloc.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:appflowy/features/workspace/logic/workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; @@ -79,10 +79,6 @@ class _MoreViewActionsState extends State { const ViewEvent.initial(), ), ), - BlocProvider( - create: (_) => PageAccessLevelBloc(view: widget.view) - ..add(PageAccessLevelEvent.initial()), - ), BlocProvider( create: (context) => SpaceBloc( userProfile: userProfile, @@ -91,6 +87,9 @@ class _MoreViewActionsState extends State { const SpaceEvent.initial(openFirstPage: false), ), ), + BlocProvider.value( + value: context.read(), + ), ], child: BlocBuilder( builder: (context, viewState) { @@ -124,6 +123,7 @@ class _MoreViewActionsState extends State { final pageAccessLevelBloc = context.watch(); final pageAccessLevelState = pageAccessLevelBloc.state; final view = pageAccessLevelState.view; + final appearanceSettings = context.watch().state; final dateFormat = appearanceSettings.dateFormat; final timeFormat = appearanceSettings.timeFormat; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart index 77c0439b38..d286dcac41 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart @@ -26,16 +26,10 @@ class LockPageAction extends StatefulWidget { class _LockPageActionState extends State { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => PageAccessLevelBloc(view: widget.view) - ..add( - PageAccessLevelEvent.initial(), - ), - child: BlocBuilder( - builder: (context, state) { - return _buildTextButton(context); - }, - ), + return BlocBuilder( + builder: (context, state) { + return _buildTextButton(context); + }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 51be86d0a7..dc99d7048d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -1,10 +1,13 @@ import 'package:appflowy/features/page_access_level/logic/page_access_level_bloc.dart'; import 'package:appflowy/features/share_tab/data/models/share_section_type.dart'; +import 'package:appflowy/features/workspace/logic/workspace_bloc.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -14,7 +17,9 @@ import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart' + hide AFRolePB; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbenum.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -42,10 +47,6 @@ class ViewTitleBar extends StatelessWidget { view: view, ), ), - BlocProvider( - create: (_) => PageAccessLevelBloc(view: view) - ..add(const PageAccessLevelEvent.initial()), - ), ], child: BlocBuilder( buildWhen: (previous, current) => @@ -66,10 +67,11 @@ class ViewTitleBar extends StatelessWidget { SpacePermission.publicToAll => SharedSectionType.public, SpacePermission.private => SharedSectionType.private, }; - if (!context.read().state.isShared) { - context.read().add( - PageAccessLevelEvent.updateSectionType(sectionType), - ); + final bloc = context.read(); + if (!bloc.isClosed && !bloc.state.isShared) { + bloc.add( + PageAccessLevelEvent.updateSectionType(sectionType), + ); } }, builder: (context, state) { @@ -153,6 +155,13 @@ class ViewTitleBar extends StatelessWidget { return []; } + // remove the space from views if the current user role is a guest + final myRole = + context.read().state.currentWorkspace?.role; + if (myRole == AFRolePB.Guest) { + views = views.where((view) => !view.isSpace).toList(); + } + // ignore the workspace name, use section name instead in the future // skip the workspace view for (var i = 1; i < views.length; i++) { @@ -237,6 +246,8 @@ class ViewTitleBar extends StatelessWidget { SharedSectionType.public => FlowySvgs.public_section_icon_m, SharedSectionType.private => FlowySvgs.private_section_icon_m, SharedSectionType.shared => FlowySvgs.shared_section_icon_m, + SharedSectionType.unknown => + throw UnsupportedError('Unknown section type'), }; final icon = FlowySvg( @@ -249,22 +260,37 @@ class ViewTitleBar extends StatelessWidget { SharedSectionType.public => 'Team space', SharedSectionType.private => 'Private', SharedSectionType.shared => 'Shared', + SharedSectionType.unknown => + throw UnsupportedError('Unknown section type'), }; - return Row( - textBaseline: TextBaseline.alphabetic, - crossAxisAlignment: CrossAxisAlignment.baseline, - children: [ - HSpace(theme.spacing.xs), - icon, - const HSpace(4.0), // ask designer to provide the spacing - Text( - text, - style: theme.textStyle.caption - .enhanced(color: theme.textColorScheme.tertiary), - ), - HSpace(theme.spacing.xs), - ], + final workspaceName = + context.read().state.currentWorkspace?.name; + final tooltipText = switch (pageAccessLevelState.sectionType) { + SharedSectionType.public => 'Everyone at $workspaceName has access', + SharedSectionType.private => 'Only you have access', + SharedSectionType.shared => '', + SharedSectionType.unknown => + throw UnsupportedError('Unknown section type'), + }; + + return FlowyTooltip( + message: tooltipText, + child: Row( + textBaseline: TextBaseline.alphabetic, + crossAxisAlignment: CrossAxisAlignment.baseline, + children: [ + HSpace(theme.spacing.xs), + icon, + const HSpace(4.0), // ask designer to provide the spacing + Text( + text, + style: theme.textStyle.caption + .enhanced(color: theme.textColorScheme.tertiary), + ), + HSpace(theme.spacing.xs), + ], + ), ); } } @@ -342,8 +368,10 @@ class _ViewTitleState extends State { final isEditable = widget.behavior == ViewTitleBehavior.editable; return BlocProvider( - create: (_) => - ViewTitleBloc(view: widget.view)..add(const ViewTitleEvent.initial()), + create: (_) => ViewTitleBloc(view: widget.view) + ..add( + const ViewTitleEvent.initial(), + ), child: BlocConsumer( listenWhen: (previous, current) { if (previous.view == null || current.view == null) { @@ -440,19 +468,23 @@ class _ViewTitleState extends State { ViewTitleState state, bool isEditable, ) { - final spaceIcon = state.view?.buildSpaceIconSvg(context); + final view = state.view ?? widget.view; + final spaceIcon = view.buildSpaceIconSvg(context); + final icon = + state.icon.isNotEmpty ? state.icon : view.icon.toEmojiIconData(); + final name = state.name.isEmpty ? widget.view.name : state.name; return SingleChildScrollView( child: Row( children: [ - if (state.icon.isNotEmpty) ...[ - RawEmojiIconWidget(emoji: state.icon, emojiSize: 14.0), + if (icon.isNotEmpty) ...[ + RawEmojiIconWidget(emoji: icon, emojiSize: 14.0), const HSpace(4.0), ], - if (state.view?.isSpace == true && spaceIcon != null) ...[ + if (view.isSpace && spaceIcon != null) ...[ SpaceIcon( dimension: 14, svgSize: 8.5, - space: state.view!, + space: view, cornerRadius: 4, ), const HSpace(6.0), @@ -460,9 +492,7 @@ class _ViewTitleState extends State { Opacity( opacity: isEditable ? 1.0 : 0.5, child: FlowyText.regular( - state.name.isEmpty - ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() - : state.name, + name.orDefault(LocaleKeys.menuAppHeader_defaultNewPageName.tr()), fontSize: 14.0, overflow: TextOverflow.ellipsis, figmaLineHeight: 18.0, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 1e4db084eb..481cc1482b 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -2181,26 +2181,26 @@ packages: dependency: "direct main" description: name: talker - sha256: "45abef5b92f9b9bd42c3f20133ad4b20ab12e1da2aa206fc0a40ea874bed7c5d" + sha256: f4b3f6110b03f78ef314f897322e47c6be2b0fbe6fafa62e4041ac5321e88620 url: "https://pub.dev" source: hosted - version: "4.7.1" + version: "4.8.1" talker_bloc_logger: dependency: "direct main" description: name: talker_bloc_logger - sha256: "2214a5f6ef9ff33494dc6149321c270356962725cc8fc1a485d44b1d9b812ddd" + sha256: "2f3ccf88c473105b7fecc4a81289f4345ee5aa652dd2f43108a60dded08afc4a" url: "https://pub.dev" source: hosted - version: "4.7.1" + version: "4.8.1" talker_logger: dependency: transitive description: name: talker_logger - sha256: ed9b20b8c09efff9f6b7c63fc6630ee2f84aa92661ae09e5ba04e77272bf2ad2 + sha256: "4e526350aa917d8c68eeded19604ce82ffe68ceeb9fd803225d30a12924ca506" url: "https://pub.dev" source: hosted - version: "4.7.1" + version: "4.8.1" term_glyph: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 2b8f7dfac9..cfe9ffe56d 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -147,7 +147,7 @@ dependencies: xml: ^6.5.0 window_manager: ^0.4.3 saver_gallery: ^4.0.1 - talker_bloc_logger: ^4.7.1 + talker_bloc_logger: ^4.8.1 talker: ^4.7.1 analyzer: 6.11.0 diff --git a/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/general_access_section_test.dart b/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/general_access_section_test.dart index a61b70cb64..6c2c6c0384 100644 --- a/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/general_access_section_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/general_access_section_test.dart @@ -1,5 +1,7 @@ import 'package:appflowy/features/share_tab/data/models/shared_group.dart'; import 'package:appflowy/features/share_tab/presentation/widgets/general_access_section.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../../widget_test_wrapper.dart'; @@ -19,7 +21,7 @@ void main() { ), ), ); - expect(find.text('General access'), findsOneWidget); + expect(find.text(LocaleKeys.shareTab_generalAccess.tr()), findsOneWidget); expect(find.byType(GeneralAccessSection), findsOneWidget); }); }); diff --git a/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/people_with_access_section_test.dart b/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/people_with_access_section_test.dart index db5577590a..02a5a1b184 100644 --- a/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/people_with_access_section_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/people_with_access_section_test.dart @@ -18,6 +18,7 @@ void main() { await tester.pumpWidget( WidgetTestWrapper( child: PeopleWithAccessSection( + isInPublicPage: true, currentUserEmail: user.email, users: [user], callbacks: PeopleWithAccessSectionCallbacks( diff --git a/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/shared_user_widget_test.dart b/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/shared_user_widget_test.dart index a79a66edfa..db401ca187 100644 --- a/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/shared_user_widget_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/lib/features/share_tab/shared_user_widget_test.dart @@ -1,6 +1,10 @@ import 'package:appflowy/features/share_tab/data/models/models.dart'; +import 'package:appflowy/features/share_tab/presentation/widgets/access_level_list_widget.dart'; +import 'package:appflowy/features/share_tab/presentation/widgets/edit_access_level_widget.dart'; import 'package:appflowy/features/share_tab/presentation/widgets/shared_user_widget.dart'; +import 'package:appflowy/features/share_tab/presentation/widgets/turn_into_member_widget.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -20,6 +24,7 @@ void main() { await tester.pumpWidget( WidgetTestWrapper( child: SharedUserWidget( + isInPublicPage: true, user: user, currentUser: user, ), @@ -42,6 +47,7 @@ void main() { WidgetTestWrapper( child: IntrinsicWidth( child: SharedUserWidget( + isInPublicPage: true, user: user, currentUser: user, ), @@ -50,5 +56,248 @@ void main() { ); expect(find.text(LocaleKeys.shareTab_guest.tr()), findsOneWidget); }); + + testWidgets('readonly user can only see remove self action in menu', + (WidgetTester tester) async { + final user = SharedUser( + name: 'Readonly User', + email: 'readonly@user.com', + accessLevel: ShareAccessLevel.readOnly, + role: ShareRole.member, + ); + await tester.pumpWidget( + WidgetTestWrapper( + child: SharedUserWidget( + isInPublicPage: false, + user: user, + currentUser: user.copyWith(accessLevel: ShareAccessLevel.readOnly), + ), + ), + ); + // Tap the EditAccessLevelWidget to open the menu + await tester.tap(find.byType(EditAccessLevelWidget)); + await tester.pumpAndSettle(); + // Only remove access should be visible as an actionable item + expect(find.text(LocaleKeys.shareTab_removeAccess.tr()), findsOneWidget); + }); + + testWidgets('edit user can only see remove self action in menu', + (WidgetTester tester) async { + final user = SharedUser( + name: 'Edit User', + email: 'edit@user.com', + accessLevel: ShareAccessLevel.readAndWrite, + role: ShareRole.member, + ); + await tester.pumpWidget( + WidgetTestWrapper( + child: SharedUserWidget( + isInPublicPage: false, + user: user, + currentUser: user.copyWith( + accessLevel: ShareAccessLevel.readAndWrite, + ), + ), + ), + ); + // Tap the EditAccessLevelWidget to open the menu + await tester.tap(find.byType(EditAccessLevelWidget)); + await tester.pumpAndSettle(); + // Only remove access should be visible as an actionable item + expect(find.text(LocaleKeys.shareTab_removeAccess.tr()), findsOneWidget); + }); + + testWidgets('full access user can change another people permission', + (WidgetTester tester) async { + final user = SharedUser( + name: 'Other User', + email: 'other@user.com', + accessLevel: ShareAccessLevel.readOnly, + role: ShareRole.member, + ); + final currentUser = SharedUser( + name: 'Full Access User', + email: 'full@user.com', + accessLevel: ShareAccessLevel.fullAccess, + role: ShareRole.member, + ); + await tester.pumpWidget( + WidgetTestWrapper( + child: SharedUserWidget( + isInPublicPage: false, + user: user, + currentUser: currentUser, + ), + ), + ); + // Tap the EditAccessLevelWidget to open the menu + await tester.tap(find.byType(EditAccessLevelWidget)); + await tester.pumpAndSettle(); + // Permission change options should be visible + expect(find.text(ShareAccessLevel.readOnly.title), findsWidgets); + expect(find.text(ShareAccessLevel.readAndWrite.title), findsWidgets); + expect(find.text(LocaleKeys.shareTab_removeAccess.tr()), findsOneWidget); + }); + + testWidgets('full access user can turn a guest into member', + (WidgetTester tester) async { + bool turnedIntoMember = false; + final guestUser = SharedUser( + name: 'Guest User', + email: 'guest@user.com', + accessLevel: ShareAccessLevel.readOnly, + role: ShareRole.guest, + ); + final currentUser = SharedUser( + name: 'Full Access User', + email: 'full@user.com', + accessLevel: ShareAccessLevel.fullAccess, + role: ShareRole.member, + ); + await tester.pumpWidget( + WidgetTestWrapper( + child: SharedUserWidget( + isInPublicPage: true, + user: guestUser, + currentUser: currentUser, + callbacks: AccessLevelListCallbacks( + onSelectAccessLevel: (_) {}, + onTurnIntoMember: () { + turnedIntoMember = true; + }, + onRemoveAccess: () {}, + ), + ), + ), + ); + // The TurnIntoMemberWidget should be present + expect(find.byType(TurnIntoMemberWidget), findsOneWidget); + // Tap the button (AFGhostButton inside TurnIntoMemberWidget) + await tester.tap(find.byType(TurnIntoMemberWidget)); + await tester.pumpAndSettle(); + expect(turnedIntoMember, isTrue); + }); + + // Additional tests for more coverage + testWidgets('public page: member/owner always gets disabled button', + (WidgetTester tester) async { + final user = SharedUser( + name: 'Member User', + email: 'member@user.com', + accessLevel: ShareAccessLevel.readAndWrite, + role: ShareRole.member, + ); + final currentUser = + user.copyWith(accessLevel: ShareAccessLevel.fullAccess); + await tester.pumpWidget( + WidgetTestWrapper( + child: SharedUserWidget( + isInPublicPage: true, + user: user, + currentUser: currentUser, + ), + ), + ); + expect(find.byType(AFGhostTextButton), findsOneWidget); + expect(find.byType(EditAccessLevelWidget), findsNothing); + }); + + testWidgets('private page: full access user can manage others', + (WidgetTester tester) async { + final user = SharedUser( + name: 'Other User', + email: 'other@user.com', + accessLevel: ShareAccessLevel.readOnly, + role: ShareRole.member, + ); + final currentUser = SharedUser( + name: 'Full Access User', + email: 'full@user.com', + accessLevel: ShareAccessLevel.fullAccess, + role: ShareRole.member, + ); + await tester.pumpWidget( + WidgetTestWrapper( + child: SharedUserWidget( + isInPublicPage: false, + user: user, + currentUser: currentUser, + ), + ), + ); + expect(find.byType(EditAccessLevelWidget), findsOneWidget); + }); + + testWidgets('private page: readonly user sees disabled button for others', + (WidgetTester tester) async { + final user = SharedUser( + name: 'Other User', + email: 'other@user.com', + accessLevel: ShareAccessLevel.readOnly, + role: ShareRole.member, + ); + final currentUser = SharedUser( + name: 'Readonly User', + email: 'readonly@user.com', + accessLevel: ShareAccessLevel.readOnly, + role: ShareRole.member, + ); + await tester.pumpWidget( + WidgetTestWrapper( + child: SharedUserWidget( + isInPublicPage: false, + user: user, + currentUser: currentUser, + ), + ), + ); + expect(find.byType(AFGhostTextButton), findsOneWidget); + expect(find.byType(EditAccessLevelWidget), findsNothing); + }); + + testWidgets('self: full access user cannot change own access', + (WidgetTester tester) async { + final user = SharedUser( + name: 'Full Access User', + email: 'full@user.com', + accessLevel: ShareAccessLevel.fullAccess, + role: ShareRole.member, + ); + await tester.pumpWidget( + WidgetTestWrapper( + child: SharedUserWidget( + isInPublicPage: false, + user: user, + currentUser: user, + ), + ), + ); + expect(find.byType(AFGhostTextButton), findsOneWidget); + expect(find.byType(EditAccessLevelWidget), findsNothing); + }); + + testWidgets('self: readonly user can only remove self', + (WidgetTester tester) async { + final user = SharedUser( + name: 'Readonly User', + email: 'readonly@user.com', + accessLevel: ShareAccessLevel.readOnly, + role: ShareRole.member, + ); + await tester.pumpWidget( + WidgetTestWrapper( + child: SharedUserWidget( + isInPublicPage: false, + user: user, + currentUser: user, + ), + ), + ); + expect(find.byType(EditAccessLevelWidget), findsOneWidget); + // Open the menu and check only remove access is present + await tester.tap(find.byType(EditAccessLevelWidget)); + await tester.pumpAndSettle(); + expect(find.text(LocaleKeys.shareTab_removeAccess.tr()), findsOneWidget); + }); }); } diff --git a/frontend/resources/translations/en-US.json b/frontend/resources/translations/en-US.json index c72c710853..cdc0eb9e8c 100644 --- a/frontend/resources/translations/en-US.json +++ b/frontend/resources/translations/en-US.json @@ -2367,7 +2367,7 @@ "seeMore": "See more", "showMore": "Show more", "somethingWentWrong": "Something went wrong", - "pageNotExist": "This page doesn’t exist", + "pageNotExist": "This page doesn't exist", "tryAgainOrLater": "Please try again later", "placeholder": { "actions": "Search actions..." @@ -3099,7 +3099,7 @@ "many": "{count} members", "other": "{count} members" }, - "tip": "You’ve been invited to Join this workspace with the contact information below. If this is incorrect, contact your administrator to resend the invite.", + "tip": "You've been invited to Join this workspace with the contact information below. If this is incorrect, contact your administrator to resend the invite.", "joinWorkspace": "Join workspace", "success": "You've successfully joined the workspace", "successMessage": "You can now access all the pages and workspaces within it.", @@ -3430,7 +3430,21 @@ "removeAccess": "Remove access", "turnIntoMember": "Turn into Member", "you": "(You)", - "guest": "Guest" + "guest": "Guest", + "onlyFullAccessCanInvite": "Only user with full access can invite others", + "invitationSent": "Invitation sent", + "emailAlreadyInList": "The email is already in the list", + "upgradeToProToInviteGuests": "Please upgrade to a Pro plan to invite more guests", + "maxGuestsReached": "You have reached the maximum number of guests", + "removedGuestSuccessfully": "Removed guest successfully", + "updatedAccessLevelSuccessfully": "Updated access level successfully", + "turnedIntoMemberSuccessfully": "Turned into member successfully", + "peopleAboveCanAccessWithLink": "People above can access with the link", + "cantMakeChanges": "Can't make changes", + "canMakeAnyChanges": "Can make any changes", + "generalAccess": "General access", + "peopleWithAccess": "People with access", + "peopleAboveCanAccessWithTheLink": "People above can access with the link" }, "shareSection": { "shared": "Shared with me", diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index cc3d231f0d..469d16f676 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -808,6 +808,9 @@ pub struct SharePageWithUserPayloadPB { #[pb(index = 3)] pub access_level: AFAccessLevelPB, + + #[pb(index = 4)] + pub auto_confirm: bool, } impl TryInto for SharePageWithUserPayloadPB {