Skip to content

Commit 289cf25

Browse files
authored
feat: integrate guest role (AppFlowy-IO#7967)
* fix: turn off share apis when fg is off * chore: bump client-api version * fix: use workspace id in turn into member api * feat: hide the invite textfield when user role is guest * fix: improve error handling for invitation failures in share tab * fix: handle sharing disabled state in share tab * chore: bump version * feat: replace sidebar menu with AFMenu * chore: add error code * feat: shared with me * fix: app scale issue * chore: udpate error message * feat: support more access levels * chore: delegate the view item events * feat: inject the callbacks into shared page list * fix: flutter analyze * feat: support rename / change icon in shared folder * feat: added shared page actions button * fix: tests * chore: update version
1 parent c664b63 commit 289cf25

File tree

25 files changed

+682
-117
lines changed

25 files changed

+682
-117
lines changed

frontend/appflowy_flutter/integration_test/desktop/uncategorized/zoom_in_out_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ void main() {
5656
await tester.pumpAndSettle();
5757

5858
currentScaleFactor += 0.1;
59+
currentScaleFactor = double.parse(
60+
currentScaleFactor.toStringAsFixed(2),
61+
);
5962

6063
final scaleFactor = await windowSizeManager.getScaleFactor();
6164
expect(currentScaleFactor, appflowyScaleFactor);

frontend/appflowy_flutter/lib/features/share_tab/data/repositories/local_share_with_user_repository.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ class LocalShareWithUserRepository extends ShareWithUserRepository {
145145

146146
@override
147147
Future<FlowyResult<void, FlowyError>> changeRole({
148-
required String pageId,
148+
required String workspaceId,
149149
required String email,
150150
required ShareRole role,
151151
}) async {

frontend/appflowy_flutter/lib/features/share_tab/data/repositories/rust_share_with_user_repository.dart

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class RustShareWithUserRepository extends ShareWithUserRepository {
2323

2424
return result.fold(
2525
(success) {
26-
Log.info('get shared users: $success');
26+
Log.debug('get shared users: $success');
2727

2828
return FlowySuccess(success.sharedUsers);
2929
},
@@ -93,30 +93,31 @@ class RustShareWithUserRepository extends ShareWithUserRepository {
9393
Future<FlowyResult<SharedUsers, FlowyError>> getAvailableSharedUsers({
9494
required String pageId,
9595
}) async {
96-
// TODO: Implement this
9796
return FlowySuccess([]);
9897
}
9998

10099
@override
101100
Future<FlowyResult<void, FlowyError>> changeRole({
102-
required String pageId,
101+
required String workspaceId,
103102
required String email,
104103
required ShareRole role,
105104
}) async {
106105
final request = UpdateWorkspaceMemberPB(
107-
workspaceId: pageId,
106+
workspaceId: workspaceId,
108107
email: email,
109108
role: role.userRole,
110109
);
111110
final result = await UserEventUpdateWorkspaceMember(request).send();
112111
return result.fold(
113112
(success) {
114-
Log.info('change role($role) for user($email) in page($pageId)');
113+
Log.info(
114+
'change role($role) for user($email) in workspaceId($workspaceId)',
115+
);
115116
return FlowySuccess(success);
116117
},
117118
(failure) {
118119
Log.error(
119-
'failed to change role($role) for user($email) in page($pageId)',
120+
'failed to change role($role) for user($email) in workspaceId($workspaceId)',
120121
failure,
121122
);
122123
return FlowyFailure(failure);

frontend/appflowy_flutter/lib/features/share_tab/data/repositories/share_with_user_repository.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ abstract class ShareWithUserRepository {
3434

3535
/// Change the role of a user in a shared page.
3636
Future<FlowyResult<void, FlowyError>> changeRole({
37-
required String pageId,
37+
required String workspaceId,
3838
required String email,
3939
required ShareRole role,
4040
});

frontend/appflowy_flutter/lib/features/share_tab/logic/share_with_user_bloc.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:appflowy/features/share_tab/data/models/models.dart';
22
import 'package:appflowy/features/share_tab/data/repositories/share_with_user_repository.dart';
33
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
44
import 'package:appflowy/plugins/shared/share/constants.dart';
5+
import 'package:appflowy/shared/feature_flags.dart';
56
import 'package:appflowy/startup/startup.dart';
67
import 'package:appflowy/user/application/user_service.dart';
78
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -37,6 +38,17 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
3738
Initial event,
3839
Emitter<ShareWithUserState> emit,
3940
) async {
41+
if (!FeatureFlag.sharedSection.isOn) {
42+
emit(
43+
state.copyWith(
44+
errorMessage: 'Sharing is currently disabled.',
45+
users: [],
46+
isLoading: false,
47+
),
48+
);
49+
return;
50+
}
51+
4052
final result = await UserBackendService.getCurrentUserProfile();
4153
final currentUser = result.fold(
4254
(user) => user,
@@ -63,6 +75,10 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
6375
GetSharedUsers event,
6476
Emitter<ShareWithUserState> emit,
6577
) async {
78+
if (!FeatureFlag.sharedSection.isOn) {
79+
return;
80+
}
81+
6682
emit(
6783
state.copyWith(
6884
errorMessage: '',
@@ -288,7 +304,7 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
288304
);
289305

290306
final result = await repository.changeRole(
291-
pageId: pageId,
307+
workspaceId: workspaceId,
292308
email: event.email,
293309
role: ShareRole.member,
294310
);

frontend/appflowy_flutter/lib/features/share_tab/presentation/share_tab.dart

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';
1+
import 'package:appflowy/features/share_tab/data/models/models.dart';
22
import 'package:appflowy/features/share_tab/logic/share_with_user_bloc.dart';
33
import 'package:appflowy/features/share_tab/presentation/widgets/copy_link_widget.dart';
44
import 'package:appflowy/features/share_tab/presentation/widgets/people_with_access_section.dart';
55
import 'package:appflowy/features/share_tab/presentation/widgets/share_with_user_widget.dart';
66
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
7+
import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart';
78
import 'package:appflowy_ui/appflowy_ui.dart';
9+
import 'package:collection/collection.dart';
810
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
911
import 'package:flutter/material.dart';
1012
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -46,20 +48,29 @@ class _ShareTabState extends State<ShareTab> {
4648
return const SizedBox.shrink();
4749
}
4850

51+
final currentUserRole = state.users
52+
.firstWhereOrNull(
53+
(user) => user.email == state.currentUser?.email,
54+
)
55+
?.role;
56+
4957
return Column(
5058
crossAxisAlignment: CrossAxisAlignment.start,
5159
mainAxisSize: MainAxisSize.min,
5260
children: [
5361
// share page with user by email
54-
VSpace(theme.spacing.l),
55-
ShareWithUserWidget(
56-
controller: controller,
57-
onInvite: (emails) => _onSharePageWithUser(
58-
context,
59-
emails: emails,
60-
accessLevel: ShareAccessLevel.readOnly,
62+
// hide this when the user is guest
63+
if (currentUserRole != ShareRole.guest) ...[
64+
VSpace(theme.spacing.l),
65+
ShareWithUserWidget(
66+
controller: controller,
67+
onInvite: (emails) => _onSharePageWithUser(
68+
context,
69+
emails: emails,
70+
accessLevel: ShareAccessLevel.readOnly,
71+
),
6172
),
62-
),
73+
],
6374

6475
// shared users
6576

@@ -104,7 +115,12 @@ class _ShareTabState extends State<ShareTab> {
104115
) {
105116
return PeopleWithAccessSectionCallbacks(
106117
onSelectAccessLevel: (user, accessLevel) {
107-
// do nothing. the event doesn't support in the backend yet
118+
context.read<ShareWithUserBloc>().add(
119+
ShareWithUserEvent.updateAccessLevel(
120+
email: user.email,
121+
accessLevel: accessLevel,
122+
),
123+
);
108124
},
109125
onTurnIntoMember: (user) {
110126
context.read<ShareWithUserBloc>().add(
@@ -133,9 +149,22 @@ class _ShareTabState extends State<ShareTab> {
133149
message: 'Invitation sent',
134150
);
135151
}, (error) {
136-
// TODO: handle the limiation error
152+
String message;
153+
switch (error.code) {
154+
case ErrorCode.InvalidGuest:
155+
message = 'The email is already in the list';
156+
break;
157+
case ErrorCode.FreePlanGuestLimitExceeded:
158+
message = 'Please upgrade to a Pro plan to invite more guests';
159+
break;
160+
case ErrorCode.PaidPlanGuestLimitExceeded:
161+
message = 'You have reached the maximum number of guests';
162+
break;
163+
default:
164+
message = error.msg;
165+
}
137166
showToastNotification(
138-
message: error.msg,
167+
message: message,
139168
type: ToastificationType.error,
140169
);
141170
});

frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/access_level_list_widget.dart

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,51 +50,34 @@ class AccessLevelListCallbacks {
5050
class AccessLevelListWidget extends StatelessWidget {
5151
const AccessLevelListWidget({
5252
super.key,
53-
this.enableAccessLevelSelection = false,
5453
required this.selectedAccessLevel,
5554
required this.callbacks,
55+
required this.supportedAccessLevels,
5656
});
5757

58-
/// Enable access level selection
59-
final bool enableAccessLevelSelection;
60-
6158
/// The currently selected access level
6259
final ShareAccessLevel selectedAccessLevel;
6360

6461
/// Callbacks
6562
final AccessLevelListCallbacks callbacks;
6663

64+
/// Supported access levels
65+
final List<ShareAccessLevel> supportedAccessLevels;
66+
6767
@override
6868
Widget build(BuildContext context) {
6969
final theme = AppFlowyTheme.of(context);
7070
return AFMenu(
71-
width: enableAccessLevelSelection ? 240 : 160,
71+
width: supportedAccessLevels.isNotEmpty ? 240 : 160,
7272
children: [
7373
// Display all available access level options
74-
if (enableAccessLevelSelection) ...[
75-
_buildAccessLevelItem(
76-
context,
77-
accessLevel: ShareAccessLevel.fullAccess,
78-
onTap: () =>
79-
callbacks.onSelectAccessLevel(ShareAccessLevel.fullAccess),
80-
),
81-
_buildAccessLevelItem(
82-
context,
83-
accessLevel: ShareAccessLevel.readAndWrite,
84-
onTap: () =>
85-
callbacks.onSelectAccessLevel(ShareAccessLevel.readAndWrite),
86-
),
87-
_buildAccessLevelItem(
88-
context,
89-
accessLevel: ShareAccessLevel.readAndComment,
90-
onTap: () =>
91-
callbacks.onSelectAccessLevel(ShareAccessLevel.readAndComment),
92-
),
93-
_buildAccessLevelItem(
94-
context,
95-
accessLevel: ShareAccessLevel.readOnly,
96-
onTap: () =>
97-
callbacks.onSelectAccessLevel(ShareAccessLevel.readOnly),
74+
if (supportedAccessLevels.isNotEmpty) ...[
75+
...supportedAccessLevels.map(
76+
(accessLevel) => _buildAccessLevelItem(
77+
context,
78+
accessLevel: accessLevel,
79+
onTap: () => callbacks.onSelectAccessLevel(accessLevel),
80+
),
9881
),
9982
AFDivider(spacing: theme.spacing.m),
10083
],

frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/edit_access_level_widget.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class EditAccessLevelWidget extends StatefulWidget {
1010
super.key,
1111
required this.callbacks,
1212
required this.selectedAccessLevel,
13+
required this.supportedAccessLevels,
1314
this.disabled = false,
1415
});
1516

@@ -22,6 +23,9 @@ class EditAccessLevelWidget extends StatefulWidget {
2223
/// Whether the widget is disabled
2324
final bool disabled;
2425

26+
/// Supported access levels
27+
final List<ShareAccessLevel> supportedAccessLevels;
28+
2529
@override
2630
State<EditAccessLevelWidget> createState() => _EditAccessLevelWidgetState();
2731
}
@@ -47,6 +51,7 @@ class _EditAccessLevelWidgetState extends State<EditAccessLevelWidget> {
4751
popover: (_) {
4852
return AccessLevelListWidget(
4953
selectedAccessLevel: widget.selectedAccessLevel,
54+
supportedAccessLevels: widget.supportedAccessLevels,
5055
callbacks: widget.callbacks.copyWith(
5156
onSelectAccessLevel: (accessLevel) {
5257
widget.callbacks.onSelectAccessLevel(accessLevel);

frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_group_widget.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ class SharedGroupWidget extends StatelessWidget {
7474
Widget _buildTrailing(BuildContext context) {
7575
return EditAccessLevelWidget(
7676
disabled: true,
77+
supportedAccessLevels: ShareAccessLevel.values,
7778
selectedAccessLevel: ShareAccessLevel.fullAccess,
7879
callbacks: AccessLevelListCallbacks.none(),
7980
);

frontend/appflowy_flutter/lib/features/share_tab/presentation/widgets/shared_user_widget.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,22 @@ class SharedUserWidget extends StatelessWidget {
9696
) {
9797
final isCurrentUser = user.email == currentUser.email;
9898
final theme = AppFlowyTheme.of(context);
99+
// Guest: read and write, read only
100+
// Member: full access <- only have full access until the backend supports more access levels
101+
// Owner: all access levels
102+
final supportedAccessLevels = switch (user.role) {
103+
ShareRole.guest => [
104+
ShareAccessLevel.readAndWrite,
105+
ShareAccessLevel.readOnly,
106+
],
107+
ShareRole.member => [ShareAccessLevel.fullAccess],
108+
ShareRole.owner => [ShareAccessLevel.fullAccess],
109+
};
99110
// The current guest user can't edit the access level of the other user
100-
return isCurrentUser || currentUser.role == ShareRole.guest
111+
return isCurrentUser ||
112+
currentUser.role == ShareRole.guest ||
113+
user.role == ShareRole.member ||
114+
user.role == ShareRole.owner
101115
? AFGhostTextButton.disabled(
102116
text: user.accessLevel.i18n,
103117
textStyle: theme.textStyle.body.standard(
@@ -106,6 +120,7 @@ class SharedUserWidget extends StatelessWidget {
106120
)
107121
: EditAccessLevelWidget(
108122
selectedAccessLevel: user.accessLevel,
123+
supportedAccessLevels: supportedAccessLevels,
109124
callbacks: callbacks ?? AccessLevelListCallbacks.none(),
110125
);
111126
}

0 commit comments

Comments
 (0)