mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-28 05:34:41 +00:00
feat: control access level in share menu (#8017)
* feat: use auto_confirm in share with user api * feat: reduce share api calls * feat: add team space section in title bar * feat: only user with full access can invite guest/member * feat: customize popup style * feat: support section type * feat: update access level based on public or private space * test: add access level tests * chore: update i18n * chore: disable bloc talker * fix: tests * fix: general access test * fix: view title bar test * chore: i18n
This commit is contained in:
parent
88a3e9eddd
commit
5598689dba
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<FlowyResult<UserProfilePB, FlowyError>> getCurrentUserProfile() async {
|
||||
// Simulate fetching current user profile
|
||||
return FlowySuccess(
|
||||
UserProfilePB()
|
||||
..email = 'lucas.xu@appflowy.io'
|
||||
..name = 'Lucas Xu',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<SharedSectionType, FlowyError>> getCurrentPageSectionType({
|
||||
required String pageId,
|
||||
}) async {
|
||||
return FlowySuccess(SharedSectionType.private);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<FlowyResult<UserProfilePB, FlowyError>> getCurrentUserProfile() async {
|
||||
final result = await UserEventGetUserProfile().send();
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<SharedSectionType, FlowyError>> 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) => <ViewPB>[],
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<FlowyResult<UserProfilePB, FlowyError>> getCurrentUserProfile();
|
||||
|
||||
/// Get current page is in public section or private section.
|
||||
Future<FlowyResult<SharedSectionType, FlowyError>> getCurrentPageSectionType({
|
||||
required String pageId,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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<ShareTabEvent, ShareTabState> {
|
||||
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<ShareTabEvent, ShareTabState> {
|
||||
|
||||
await result.fold(
|
||||
(_) async {
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
final users = await _getSharedUsers();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -161,7 +169,7 @@ class ShareTabBloc extends Bloc<ShareTabEvent, ShareTabState> {
|
||||
|
||||
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<ShareTabEvent, ShareTabState> {
|
||||
|
||||
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<ShareTabEvent, ShareTabState> {
|
||||
|
||||
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<ShareTabEvent, ShareTabState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<SharedUsers> _getLatestSharedUsersOrCurrentUsers() async {
|
||||
Future<SharedUsers> _getSharedUsers() async {
|
||||
final shareResult = await repository.getSharedUsersInPage(
|
||||
pageId: pageId,
|
||||
);
|
||||
|
||||
@ -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<void, FlowyError>? initialResult;
|
||||
final FlowyResult<void, FlowyError>? shareResult;
|
||||
final FlowyResult<void, FlowyError>? removeResult;
|
||||
@ -45,6 +47,7 @@ class ShareTabState {
|
||||
String? shareLink,
|
||||
ShareAccessLevel? generalAccessRole,
|
||||
bool? linkCopied,
|
||||
SharedSectionType? sectionType,
|
||||
FlowyResult<void, FlowyError>? initialResult,
|
||||
FlowyResult<void, FlowyError>? shareResult,
|
||||
FlowyResult<void, FlowyError>? 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)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<ShareTab> {
|
||||
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<ShareTab> {
|
||||
],
|
||||
|
||||
// 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<ShareTab> {
|
||||
},
|
||||
onRemoveAccess: (user) {
|
||||
// show a dialog to confirm the action when removing self access
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
final shareTabBloc = context.read<ShareTabBloc>();
|
||||
final removingSelf =
|
||||
user.email == shareTabBloc.state.currentUser?.email;
|
||||
@ -157,6 +165,9 @@ class _ShareTabState extends State<ShareTab> {
|
||||
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<ShareTab> {
|
||||
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<ShareTab> {
|
||||
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<ShareTab> {
|
||||
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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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<String> emails) onInvite;
|
||||
final TextEditingController? controller;
|
||||
final void Function(List<String> emails) onInvite;
|
||||
final bool disabled;
|
||||
final String? tooltip;
|
||||
|
||||
@override
|
||||
State<ShareWithUserWidget> createState() => _ShareWithUserWidgetState();
|
||||
@ -44,7 +49,7 @@ class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
||||
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<ShareWithUserWidget> {
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (widget.disabled) {
|
||||
return FlowyTooltip(
|
||||
message:
|
||||
widget.tooltip ?? LocaleKeys.shareTab_onlyFullAccessCanInvite.tr(),
|
||||
child: IgnorePointer(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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<ShareAccessLevel> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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!,
|
||||
|
||||
@ -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<ViewInfoBloc>.value(
|
||||
value: bloc,
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<ViewInfoBloc>.value(
|
||||
value: bloc,
|
||||
),
|
||||
BlocProvider<PageAccessLevelBloc>.value(
|
||||
value: pageAccessLevelBloc,
|
||||
),
|
||||
],
|
||||
child: Row(
|
||||
children: [
|
||||
ShareButton(key: ValueKey(view.id), view: view),
|
||||
|
||||
@ -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<ViewInfoBloc>.value(
|
||||
value: bloc,
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<ViewInfoBloc>.value(
|
||||
value: bloc,
|
||||
),
|
||||
BlocProvider<PageAccessLevelBloc>.value(
|
||||
value: pageAccessLevelBloc,
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
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<ViewInfoBloc>.value(
|
||||
value: bloc,
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<ViewInfoBloc>.value(
|
||||
value: bloc,
|
||||
),
|
||||
BlocProvider<PageAccessLevelBloc>.value(
|
||||
value: pageAccessLevelBloc,
|
||||
),
|
||||
],
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@ -87,10 +87,6 @@ class _DocumentPageState extends State<DocumentPage>
|
||||
providers: [
|
||||
BlocProvider.value(value: getIt<ActionNavigationBloc>()),
|
||||
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()),
|
||||
|
||||
@ -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()),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<ConfirmPopup> {
|
||||
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<ConfirmPopup> {
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -602,6 +602,8 @@ Future<void> 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<void> showConfirmDialog({
|
||||
child: ConfirmPopup(
|
||||
title: title,
|
||||
description: description,
|
||||
titleStyle: titleStyle,
|
||||
descriptionStyle: descriptionStyle,
|
||||
confirmButtonBuilder: confirmButtonBuilder,
|
||||
onConfirm: () => onConfirm?.call(),
|
||||
onCancel: () => onCancel?.call(),
|
||||
|
||||
@ -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<MoreViewActions> {
|
||||
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<MoreViewActions> {
|
||||
const SpaceEvent.initial(openFirstPage: false),
|
||||
),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: context.read<PageAccessLevelBloc>(),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<ViewBloc, ViewState>(
|
||||
builder: (context, viewState) {
|
||||
@ -124,6 +123,7 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
||||
final pageAccessLevelBloc = context.watch<PageAccessLevelBloc>();
|
||||
final pageAccessLevelState = pageAccessLevelBloc.state;
|
||||
final view = pageAccessLevelState.view;
|
||||
|
||||
final appearanceSettings = context.watch<AppearanceSettingsCubit>().state;
|
||||
final dateFormat = appearanceSettings.dateFormat;
|
||||
final timeFormat = appearanceSettings.timeFormat;
|
||||
|
||||
@ -26,16 +26,10 @@ class LockPageAction extends StatefulWidget {
|
||||
class _LockPageActionState extends State<LockPageAction> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => PageAccessLevelBloc(view: widget.view)
|
||||
..add(
|
||||
PageAccessLevelEvent.initial(),
|
||||
),
|
||||
child: BlocBuilder<PageAccessLevelBloc, PageAccessLevelState>(
|
||||
builder: (context, state) {
|
||||
return _buildTextButton(context);
|
||||
},
|
||||
),
|
||||
return BlocBuilder<PageAccessLevelBloc, PageAccessLevelState>(
|
||||
builder: (context, state) {
|
||||
return _buildTextButton(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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<PageAccessLevelBloc, PageAccessLevelState>(
|
||||
buildWhen: (previous, current) =>
|
||||
@ -66,10 +67,11 @@ class ViewTitleBar extends StatelessWidget {
|
||||
SpacePermission.publicToAll => SharedSectionType.public,
|
||||
SpacePermission.private => SharedSectionType.private,
|
||||
};
|
||||
if (!context.read<PageAccessLevelBloc>().state.isShared) {
|
||||
context.read<PageAccessLevelBloc>().add(
|
||||
PageAccessLevelEvent.updateSectionType(sectionType),
|
||||
);
|
||||
final bloc = context.read<PageAccessLevelBloc>();
|
||||
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<UserWorkspaceBloc>().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<UserWorkspaceBloc>().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<ViewTitle> {
|
||||
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<ViewTitleBloc, ViewTitleState>(
|
||||
listenWhen: (previous, current) {
|
||||
if (previous.view == null || current.view == null) {
|
||||
@ -440,19 +468,23 @@ class _ViewTitleState extends State<ViewTitle> {
|
||||
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<ViewTitle> {
|
||||
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,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -18,6 +18,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
WidgetTestWrapper(
|
||||
child: PeopleWithAccessSection(
|
||||
isInPublicPage: true,
|
||||
currentUserEmail: user.email,
|
||||
users: [user],
|
||||
callbacks: PeopleWithAccessSectionCallbacks(
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -808,6 +808,9 @@ pub struct SharePageWithUserPayloadPB {
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub access_level: AFAccessLevelPB,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub auto_confirm: bool,
|
||||
}
|
||||
|
||||
impl TryInto<ShareViewWithGuestRequest> for SharePageWithUserPayloadPB {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user