mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-06-27 02:50:15 +00:00
feat: turn guest into member (#7958)
* feat: turn guest into member * feat: handle share results * fix: clear email textfield after inviting * test: add turn into member test * fix: double toasts show when sharing with guest
This commit is contained in:
parent
f5e2e4e915
commit
b0a02e6870
@ -1,6 +1,8 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/share_role.dart';
|
||||
|
||||
typedef SharedUsers = List<SharedUser>;
|
||||
|
||||
/// Represents a user with a role on a shared page.
|
||||
class SharedUser {
|
||||
SharedUser({
|
||||
|
@ -9,7 +9,7 @@ import 'share_with_user_repository.dart';
|
||||
class LocalShareWithUserRepository extends ShareWithUserRepository {
|
||||
LocalShareWithUserRepository();
|
||||
|
||||
final List<SharedUser> _sharedUsers = [
|
||||
final SharedUsers _sharedUsers = [
|
||||
SharedUser(
|
||||
email: 'lucas.xu@appflowy.io',
|
||||
name: 'Lucas Xu',
|
||||
@ -61,7 +61,7 @@ class LocalShareWithUserRepository extends ShareWithUserRepository {
|
||||
),
|
||||
];
|
||||
|
||||
final List<SharedUser> _availableSharedUsers = [
|
||||
final SharedUsers _availableSharedUsers = [
|
||||
SharedUser(
|
||||
email: 'guest_email@appflowy.io',
|
||||
name: 'Guest',
|
||||
@ -79,7 +79,7 @@ class LocalShareWithUserRepository extends ShareWithUserRepository {
|
||||
];
|
||||
|
||||
@override
|
||||
Future<FlowyResult<List<SharedUser>, FlowyError>> getSharedUsersInPage({
|
||||
Future<FlowyResult<SharedUsers, FlowyError>> getSharedUsersInPage({
|
||||
required String pageId,
|
||||
}) async {
|
||||
return FlowySuccess(_sharedUsers);
|
||||
@ -134,7 +134,7 @@ class LocalShareWithUserRepository extends ShareWithUserRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<List<SharedUser>, FlowyError>> getAvailableSharedUsers({
|
||||
Future<FlowyResult<SharedUsers, FlowyError>> getAvailableSharedUsers({
|
||||
required String pageId,
|
||||
}) async {
|
||||
return FlowySuccess([
|
||||
|
@ -13,7 +13,7 @@ class RustShareWithUserRepository extends ShareWithUserRepository {
|
||||
RustShareWithUserRepository();
|
||||
|
||||
@override
|
||||
Future<FlowyResult<List<SharedUser>, FlowyError>> getSharedUsersInPage({
|
||||
Future<FlowyResult<SharedUsers, FlowyError>> getSharedUsersInPage({
|
||||
required String pageId,
|
||||
}) async {
|
||||
final request = GetSharedUsersPayloadPB(
|
||||
@ -90,7 +90,7 @@ class RustShareWithUserRepository extends ShareWithUserRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<List<SharedUser>, FlowyError>> getAvailableSharedUsers({
|
||||
Future<FlowyResult<SharedUsers, FlowyError>> getAvailableSharedUsers({
|
||||
required String pageId,
|
||||
}) async {
|
||||
// TODO: Implement this
|
||||
|
@ -8,12 +8,12 @@ import 'package:appflowy_result/appflowy_result.dart';
|
||||
/// for the future.
|
||||
abstract class ShareWithUserRepository {
|
||||
/// Gets the list of users and their roles for a shared page.
|
||||
Future<FlowyResult<List<SharedUser>, FlowyError>> getSharedUsersInPage({
|
||||
Future<FlowyResult<SharedUsers, FlowyError>> getSharedUsersInPage({
|
||||
required String pageId,
|
||||
});
|
||||
|
||||
/// Gets the list of users that are available to be shared with.
|
||||
Future<FlowyResult<List<SharedUser>, FlowyError>> getAvailableSharedUsers({
|
||||
Future<FlowyResult<SharedUsers, FlowyError>> getAvailableSharedUsers({
|
||||
required String pageId,
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:appflowy/features/share_tab/data/models/share_access_level.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/shared_user.dart';
|
||||
import 'package:appflowy/features/share_tab/data/models/models.dart';
|
||||
import 'package:appflowy/features/share_tab/data/repositories/share_with_user_repository.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
@ -27,6 +26,7 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
||||
on<UpdateGeneralAccess>(_onUpdateGeneralAccess);
|
||||
on<CopyLink>(_onCopyLink);
|
||||
on<SearchAvailableUsers>(_onSearchAvailableUsers);
|
||||
on<TurnIntoMember>(_onTurnIntoMember);
|
||||
}
|
||||
|
||||
final ShareWithUserRepository repository;
|
||||
@ -48,14 +48,7 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
||||
viewId: pageId,
|
||||
);
|
||||
|
||||
final shareResult = await repository.getSharedUsersInPage(
|
||||
pageId: pageId,
|
||||
);
|
||||
|
||||
final users = shareResult.fold(
|
||||
(users) => users,
|
||||
(error) => <SharedUser>[],
|
||||
);
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -74,6 +67,10 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
||||
state.copyWith(
|
||||
errorMessage: '',
|
||||
initialResult: null,
|
||||
shareResult: null,
|
||||
removeResult: null,
|
||||
updateAccessLevelResult: null,
|
||||
turnIntoMemberResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
@ -105,6 +102,9 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
||||
state.copyWith(
|
||||
errorMessage: '',
|
||||
shareResult: null,
|
||||
turnIntoMemberResult: null,
|
||||
removeResult: null,
|
||||
updateAccessLevelResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
@ -114,19 +114,18 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
||||
emails: event.emails,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(_) {
|
||||
await result.fold(
|
||||
(_) async {
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
shareResult: FlowySuccess(null),
|
||||
users: users,
|
||||
),
|
||||
);
|
||||
|
||||
add(
|
||||
const ShareWithUserEvent.getSharedUsers(),
|
||||
);
|
||||
},
|
||||
(error) {
|
||||
(error) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: error.msg,
|
||||
@ -143,7 +142,11 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: '',
|
||||
removeResult: null,
|
||||
shareResult: null,
|
||||
updateAccessLevelResult: null,
|
||||
turnIntoMemberResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
@ -152,19 +155,17 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
||||
emails: event.emails,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(_) {
|
||||
await result.fold(
|
||||
(_) async {
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
emit(
|
||||
state.copyWith(
|
||||
removeResult: FlowySuccess(null),
|
||||
users: users,
|
||||
),
|
||||
);
|
||||
|
||||
add(
|
||||
const ShareWithUserEvent.getSharedUsers(),
|
||||
);
|
||||
},
|
||||
(error) {
|
||||
(error) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
@ -191,24 +192,24 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
||||
emails: [event.email],
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(_) {
|
||||
await result.fold(
|
||||
(_) async {
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
emit(
|
||||
state.copyWith(
|
||||
updateAccessLevelResult: FlowySuccess(null),
|
||||
users: users,
|
||||
),
|
||||
);
|
||||
|
||||
add(
|
||||
const ShareWithUserEvent.getSharedUsers(),
|
||||
},
|
||||
(error) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: error.msg,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
(error) => emit(
|
||||
state.copyWith(
|
||||
errorMessage: error.msg,
|
||||
isLoading: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -271,6 +272,57 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onTurnIntoMember(
|
||||
TurnIntoMember event,
|
||||
Emitter<ShareWithUserState> emit,
|
||||
) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
turnIntoMemberResult: null,
|
||||
errorMessage: '',
|
||||
removeResult: null,
|
||||
shareResult: null,
|
||||
updateAccessLevelResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
final result = await repository.changeRole(
|
||||
pageId: pageId,
|
||||
email: event.email,
|
||||
role: ShareRole.member,
|
||||
);
|
||||
|
||||
await result.fold(
|
||||
(_) async {
|
||||
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||
emit(
|
||||
state.copyWith(
|
||||
turnIntoMemberResult: FlowySuccess(null),
|
||||
users: users,
|
||||
),
|
||||
);
|
||||
},
|
||||
(error) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
errorMessage: error.msg,
|
||||
turnIntoMemberResult: FlowyFailure(error),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<SharedUsers> _getLatestSharedUsersOrCurrentUsers() async {
|
||||
final shareResult = await repository.getSharedUsersInPage(
|
||||
pageId: pageId,
|
||||
);
|
||||
return shareResult.fold(
|
||||
(users) => users,
|
||||
(error) => state.users,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -312,14 +364,19 @@ class ShareWithUserEvent with _$ShareWithUserEvent {
|
||||
const factory ShareWithUserEvent.searchAvailableUsers({
|
||||
required String query,
|
||||
}) = SearchAvailableUsers;
|
||||
|
||||
/// Turns the user into a member.
|
||||
const factory ShareWithUserEvent.turnIntoMember({
|
||||
required String email,
|
||||
}) = TurnIntoMember;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ShareWithUserState with _$ShareWithUserState {
|
||||
const factory ShareWithUserState({
|
||||
@Default(null) UserProfilePB? currentUser,
|
||||
@Default([]) List<SharedUser> users,
|
||||
@Default([]) List<SharedUser> availableUsers,
|
||||
@Default([]) SharedUsers users,
|
||||
@Default([]) SharedUsers availableUsers,
|
||||
@Default(false) bool isLoading,
|
||||
@Default('') String errorMessage,
|
||||
@Default('') String shareLink,
|
||||
@ -329,6 +386,7 @@ class ShareWithUserState with _$ShareWithUserState {
|
||||
@Default(null) FlowyResult<void, FlowyError>? shareResult,
|
||||
@Default(null) FlowyResult<void, FlowyError>? removeResult,
|
||||
@Default(null) FlowyResult<void, FlowyError>? updateAccessLevelResult,
|
||||
@Default(null) FlowyResult<void, FlowyError>? turnIntoMemberResult,
|
||||
}) = _ShareWithUserState;
|
||||
|
||||
factory ShareWithUserState.initial() => const ShareWithUserState();
|
||||
|
@ -9,7 +9,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ShareTab extends StatelessWidget {
|
||||
class ShareTab extends StatefulWidget {
|
||||
const ShareTab({
|
||||
super.key,
|
||||
required this.workspaceId,
|
||||
@ -19,13 +19,27 @@ class ShareTab extends StatelessWidget {
|
||||
final String workspaceId;
|
||||
final String pageId;
|
||||
|
||||
@override
|
||||
State<ShareTab> createState() => _ShareTabState();
|
||||
}
|
||||
|
||||
class _ShareTabState extends State<ShareTab> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return BlocConsumer<ShareWithUserBloc, ShareWithUserState>(
|
||||
listener: (context, state) {
|
||||
_onShareWithUserError(context, state);
|
||||
_onListenShareWithUserState(context, state);
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
@ -39,6 +53,7 @@ class ShareTab extends StatelessWidget {
|
||||
// share page with user by email
|
||||
VSpace(theme.spacing.l),
|
||||
ShareWithUserWidget(
|
||||
controller: controller,
|
||||
onInvite: (emails) => _onSharePageWithUser(
|
||||
context,
|
||||
emails: emails,
|
||||
@ -92,7 +107,9 @@ class ShareTab extends StatelessWidget {
|
||||
// do nothing. the event doesn't support in the backend yet
|
||||
},
|
||||
onTurnIntoMember: (user) {
|
||||
// do nothing. the event doesn't support in the backend yet
|
||||
context.read<ShareWithUserBloc>().add(
|
||||
ShareWithUserEvent.turnIntoMember(email: user.email),
|
||||
);
|
||||
},
|
||||
onRemoveAccess: (user) {
|
||||
context.read<ShareWithUserBloc>().add(
|
||||
@ -102,13 +119,20 @@ class ShareTab extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _onShareWithUserError(
|
||||
void _onListenShareWithUserState(
|
||||
BuildContext context,
|
||||
ShareWithUserState state,
|
||||
) {
|
||||
final shareResult = state.shareResult;
|
||||
if (shareResult != null) {
|
||||
shareResult.onFailure((error) {
|
||||
shareResult.fold((success) {
|
||||
// clear the controller to avoid showing the previous emails
|
||||
controller.clear();
|
||||
|
||||
showToastNotification(
|
||||
message: 'Invitation sent',
|
||||
);
|
||||
}, (error) {
|
||||
// TODO: handle the limiation error
|
||||
showToastNotification(
|
||||
message: error.msg,
|
||||
@ -116,5 +140,33 @@ class ShareTab extends StatelessWidget {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final removeResult = state.removeResult;
|
||||
if (removeResult != null) {
|
||||
removeResult.fold((success) {
|
||||
showToastNotification(
|
||||
message: 'Removed guest successfully',
|
||||
);
|
||||
}, (error) {
|
||||
showToastNotification(
|
||||
message: error.msg,
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final updateAccessLevelResult = state.updateAccessLevelResult;
|
||||
if (updateAccessLevelResult != null) {
|
||||
updateAccessLevelResult.fold((success) {
|
||||
showToastNotification(
|
||||
message: 'Updated access level successfully',
|
||||
);
|
||||
}, (error) {
|
||||
showToastNotification(
|
||||
message: error.msg,
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class PeopleWithAccessSection extends StatelessWidget {
|
||||
});
|
||||
|
||||
final String currentUserEmail;
|
||||
final List<SharedUser> users;
|
||||
final SharedUsers users;
|
||||
final PeopleWithAccessSectionCallbacks? callbacks;
|
||||
|
||||
@override
|
||||
|
@ -9,28 +9,33 @@ class ShareWithUserWidget extends StatefulWidget {
|
||||
const ShareWithUserWidget({
|
||||
super.key,
|
||||
required this.onInvite,
|
||||
this.controller,
|
||||
});
|
||||
|
||||
final void Function(List<String> emails) onInvite;
|
||||
final TextEditingController? controller;
|
||||
|
||||
@override
|
||||
State<ShareWithUserWidget> createState() => _ShareWithUserWidgetState();
|
||||
}
|
||||
|
||||
class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
late final TextEditingController effectiveController;
|
||||
bool isButtonEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
controller.addListener(_onTextChanged);
|
||||
effectiveController = widget.controller ?? TextEditingController();
|
||||
effectiveController.addListener(_onTextChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
if (widget.controller == null) {
|
||||
effectiveController.dispose();
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
@ -43,7 +48,7 @@ class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: AFTextField(
|
||||
controller: controller,
|
||||
controller: effectiveController,
|
||||
size: AFTextFieldSize.m,
|
||||
hintText: LocaleKeys.shareTab_inviteByEmail.tr(),
|
||||
),
|
||||
@ -53,7 +58,7 @@ class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
||||
text: LocaleKeys.shareTab_invite.tr(),
|
||||
disabled: !isButtonEnabled,
|
||||
onTap: () {
|
||||
widget.onInvite(controller.text.trim().split(','));
|
||||
widget.onInvite(effectiveController.text.trim().split(','));
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -62,7 +67,7 @@ class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
||||
|
||||
void _onTextChanged() {
|
||||
setState(() {
|
||||
final texts = controller.text.trim().split(',');
|
||||
final texts = effectiveController.text.trim().split(',');
|
||||
isButtonEnabled = texts.isNotEmpty && texts.every(isEmail);
|
||||
});
|
||||
}
|
||||
|
@ -35,6 +35,11 @@ class SharedSection extends StatelessWidget {
|
||||
return SharedSectionError(errorMessage: state.errorMessage);
|
||||
}
|
||||
|
||||
// hide the shared section if there are no shared pages
|
||||
if (state.sharedPages.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -22,7 +22,7 @@ extension SharedViewPBExtension on SharedViewPB {
|
||||
}
|
||||
|
||||
extension RepeatedSharedUserPBExtension on RepeatedSharedUserPB {
|
||||
List<SharedUser> get sharedUsers {
|
||||
SharedUsers get sharedUsers {
|
||||
return items.map((e) => e.sharedUser).toList();
|
||||
}
|
||||
}
|
||||
|
@ -38,17 +38,19 @@ void main() {
|
||||
wait: const Duration(milliseconds: 100),
|
||||
expect: () => [
|
||||
// First state: shareResult is null
|
||||
isA<ShareWithUserState>()
|
||||
.having((s) => s.shareResult, 'shareResult', isNull),
|
||||
// Second state: shareResult is Success
|
||||
isA<ShareWithUserState>()
|
||||
.having((s) => s.shareResult, 'shareResult', isNotNull),
|
||||
// Third state: users updated, shareResult still Success
|
||||
isA<ShareWithUserState>().having(
|
||||
(s) => s.users.any((u) => u.email == email),
|
||||
'users contains new user',
|
||||
isTrue,
|
||||
(s) => s.shareResult,
|
||||
'shareResult',
|
||||
isNull,
|
||||
),
|
||||
// Second state: shareResult is Success and users updated
|
||||
isA<ShareWithUserState>()
|
||||
.having((s) => s.shareResult, 'shareResult', isNotNull)
|
||||
.having(
|
||||
(s) => s.users.any((u) => u.email == email),
|
||||
'users contains new user',
|
||||
isTrue,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@ -65,15 +67,14 @@ void main() {
|
||||
// First state: removeResult is null
|
||||
isA<ShareWithUserState>()
|
||||
.having((s) => s.removeResult, 'removeResult', isNull),
|
||||
// Second state: removeResult is Success
|
||||
// Second state: removeResult is Success and users updated
|
||||
isA<ShareWithUserState>()
|
||||
.having((s) => s.removeResult, 'removeResult', isNotNull),
|
||||
// Third state: users updated, removeResult still Success
|
||||
isA<ShareWithUserState>().having(
|
||||
(s) => s.users.any((u) => u.email == email),
|
||||
'users contains removed user',
|
||||
isFalse,
|
||||
),
|
||||
.having((s) => s.removeResult, 'removeResult', isNotNull)
|
||||
.having(
|
||||
(s) => s.users.any((u) => u.email == email),
|
||||
'users contains removed user',
|
||||
isFalse,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@ -94,18 +95,69 @@ void main() {
|
||||
'updateAccessLevelResult',
|
||||
isNull,
|
||||
),
|
||||
// Second state: updateAccessLevelResult is Success
|
||||
isA<ShareWithUserState>().having(
|
||||
(s) => s.updateAccessLevelResult,
|
||||
'updateAccessLevelResult',
|
||||
isNotNull,
|
||||
// Second state: updateAccessLevelResult is Success and users updated
|
||||
isA<ShareWithUserState>()
|
||||
.having(
|
||||
(s) => s.updateAccessLevelResult,
|
||||
'updateAccessLevelResult',
|
||||
isNotNull,
|
||||
)
|
||||
.having(
|
||||
(s) => s.users.firstWhere((u) => u.email == email).accessLevel,
|
||||
'vivian accessLevel',
|
||||
ShareAccessLevel.fullAccess,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final guestEmail = 'guest@appflowy.io';
|
||||
blocTest<ShareWithUserBloc, ShareWithUserState>(
|
||||
'turns user into member',
|
||||
build: () => bloc,
|
||||
act: (bloc) => bloc
|
||||
..add(
|
||||
ShareWithUserEvent.share(
|
||||
emails: [guestEmail],
|
||||
accessLevel: ShareAccessLevel.readOnly,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
ShareWithUserEvent.turnIntoMember(
|
||||
email: guestEmail,
|
||||
),
|
||||
),
|
||||
// Third state: users updated, vivian's access level is fullAccess
|
||||
wait: const Duration(milliseconds: 100),
|
||||
expect: () => [
|
||||
// First state: shareResult is null
|
||||
isA<ShareWithUserState>().having(
|
||||
(s) => s.users.firstWhere((u) => u.email == email).accessLevel,
|
||||
'vivian accessLevel',
|
||||
ShareAccessLevel.fullAccess,
|
||||
(s) => s.shareResult,
|
||||
'shareResult',
|
||||
isNull,
|
||||
),
|
||||
// Second state: shareResult is Success and users updated
|
||||
isA<ShareWithUserState>()
|
||||
.having(
|
||||
(s) => s.shareResult,
|
||||
'shareResult',
|
||||
isNotNull,
|
||||
)
|
||||
.having(
|
||||
(s) => s.users.any((u) => u.email == guestEmail),
|
||||
'users contains guest@appflowy.io',
|
||||
isTrue,
|
||||
),
|
||||
// Third state: turnIntoMemberResult is Success and users updated
|
||||
isA<ShareWithUserState>()
|
||||
.having(
|
||||
(s) => s.turnIntoMemberResult,
|
||||
'turnIntoMemberResult',
|
||||
isNotNull,
|
||||
)
|
||||
.having(
|
||||
(s) => s.users.firstWhere((u) => u.email == guestEmail).role,
|
||||
'guest@appflowy.io role',
|
||||
ShareRole.member,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user