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_access_level.dart';
|
||||||
import 'package:appflowy/features/share_tab/data/models/share_role.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.
|
/// Represents a user with a role on a shared page.
|
||||||
class SharedUser {
|
class SharedUser {
|
||||||
SharedUser({
|
SharedUser({
|
||||||
|
@ -9,7 +9,7 @@ import 'share_with_user_repository.dart';
|
|||||||
class LocalShareWithUserRepository extends ShareWithUserRepository {
|
class LocalShareWithUserRepository extends ShareWithUserRepository {
|
||||||
LocalShareWithUserRepository();
|
LocalShareWithUserRepository();
|
||||||
|
|
||||||
final List<SharedUser> _sharedUsers = [
|
final SharedUsers _sharedUsers = [
|
||||||
SharedUser(
|
SharedUser(
|
||||||
email: 'lucas.xu@appflowy.io',
|
email: 'lucas.xu@appflowy.io',
|
||||||
name: 'Lucas Xu',
|
name: 'Lucas Xu',
|
||||||
@ -61,7 +61,7 @@ class LocalShareWithUserRepository extends ShareWithUserRepository {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
final List<SharedUser> _availableSharedUsers = [
|
final SharedUsers _availableSharedUsers = [
|
||||||
SharedUser(
|
SharedUser(
|
||||||
email: 'guest_email@appflowy.io',
|
email: 'guest_email@appflowy.io',
|
||||||
name: 'Guest',
|
name: 'Guest',
|
||||||
@ -79,7 +79,7 @@ class LocalShareWithUserRepository extends ShareWithUserRepository {
|
|||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlowyResult<List<SharedUser>, FlowyError>> getSharedUsersInPage({
|
Future<FlowyResult<SharedUsers, FlowyError>> getSharedUsersInPage({
|
||||||
required String pageId,
|
required String pageId,
|
||||||
}) async {
|
}) async {
|
||||||
return FlowySuccess(_sharedUsers);
|
return FlowySuccess(_sharedUsers);
|
||||||
@ -134,7 +134,7 @@ class LocalShareWithUserRepository extends ShareWithUserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlowyResult<List<SharedUser>, FlowyError>> getAvailableSharedUsers({
|
Future<FlowyResult<SharedUsers, FlowyError>> getAvailableSharedUsers({
|
||||||
required String pageId,
|
required String pageId,
|
||||||
}) async {
|
}) async {
|
||||||
return FlowySuccess([
|
return FlowySuccess([
|
||||||
|
@ -13,7 +13,7 @@ class RustShareWithUserRepository extends ShareWithUserRepository {
|
|||||||
RustShareWithUserRepository();
|
RustShareWithUserRepository();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlowyResult<List<SharedUser>, FlowyError>> getSharedUsersInPage({
|
Future<FlowyResult<SharedUsers, FlowyError>> getSharedUsersInPage({
|
||||||
required String pageId,
|
required String pageId,
|
||||||
}) async {
|
}) async {
|
||||||
final request = GetSharedUsersPayloadPB(
|
final request = GetSharedUsersPayloadPB(
|
||||||
@ -90,7 +90,7 @@ class RustShareWithUserRepository extends ShareWithUserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlowyResult<List<SharedUser>, FlowyError>> getAvailableSharedUsers({
|
Future<FlowyResult<SharedUsers, FlowyError>> getAvailableSharedUsers({
|
||||||
required String pageId,
|
required String pageId,
|
||||||
}) async {
|
}) async {
|
||||||
// TODO: Implement this
|
// TODO: Implement this
|
||||||
|
@ -8,12 +8,12 @@ import 'package:appflowy_result/appflowy_result.dart';
|
|||||||
/// for the future.
|
/// for the future.
|
||||||
abstract class ShareWithUserRepository {
|
abstract class ShareWithUserRepository {
|
||||||
/// Gets the list of users and their roles for a shared page.
|
/// 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,
|
required String pageId,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Gets the list of users that are available to be shared with.
|
/// 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,
|
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/models.dart';
|
||||||
import 'package:appflowy/features/share_tab/data/models/shared_user.dart';
|
|
||||||
import 'package:appflowy/features/share_tab/data/repositories/share_with_user_repository.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/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||||
@ -27,6 +26,7 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
|||||||
on<UpdateGeneralAccess>(_onUpdateGeneralAccess);
|
on<UpdateGeneralAccess>(_onUpdateGeneralAccess);
|
||||||
on<CopyLink>(_onCopyLink);
|
on<CopyLink>(_onCopyLink);
|
||||||
on<SearchAvailableUsers>(_onSearchAvailableUsers);
|
on<SearchAvailableUsers>(_onSearchAvailableUsers);
|
||||||
|
on<TurnIntoMember>(_onTurnIntoMember);
|
||||||
}
|
}
|
||||||
|
|
||||||
final ShareWithUserRepository repository;
|
final ShareWithUserRepository repository;
|
||||||
@ -48,14 +48,7 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
|||||||
viewId: pageId,
|
viewId: pageId,
|
||||||
);
|
);
|
||||||
|
|
||||||
final shareResult = await repository.getSharedUsersInPage(
|
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||||
pageId: pageId,
|
|
||||||
);
|
|
||||||
|
|
||||||
final users = shareResult.fold(
|
|
||||||
(users) => users,
|
|
||||||
(error) => <SharedUser>[],
|
|
||||||
);
|
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@ -74,6 +67,10 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
initialResult: null,
|
initialResult: null,
|
||||||
|
shareResult: null,
|
||||||
|
removeResult: null,
|
||||||
|
updateAccessLevelResult: null,
|
||||||
|
turnIntoMemberResult: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -105,6 +102,9 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
|||||||
state.copyWith(
|
state.copyWith(
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
shareResult: null,
|
shareResult: null,
|
||||||
|
turnIntoMemberResult: null,
|
||||||
|
removeResult: null,
|
||||||
|
updateAccessLevelResult: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -114,19 +114,18 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
|||||||
emails: event.emails,
|
emails: event.emails,
|
||||||
);
|
);
|
||||||
|
|
||||||
result.fold(
|
await result.fold(
|
||||||
(_) {
|
(_) async {
|
||||||
|
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
shareResult: FlowySuccess(null),
|
shareResult: FlowySuccess(null),
|
||||||
|
users: users,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
add(
|
|
||||||
const ShareWithUserEvent.getSharedUsers(),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
(error) {
|
(error) async {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
errorMessage: error.msg,
|
errorMessage: error.msg,
|
||||||
@ -143,7 +142,11 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
|||||||
) async {
|
) async {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
|
errorMessage: '',
|
||||||
removeResult: null,
|
removeResult: null,
|
||||||
|
shareResult: null,
|
||||||
|
updateAccessLevelResult: null,
|
||||||
|
turnIntoMemberResult: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -152,19 +155,17 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
|||||||
emails: event.emails,
|
emails: event.emails,
|
||||||
);
|
);
|
||||||
|
|
||||||
result.fold(
|
await result.fold(
|
||||||
(_) {
|
(_) async {
|
||||||
|
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
removeResult: FlowySuccess(null),
|
removeResult: FlowySuccess(null),
|
||||||
|
users: users,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
add(
|
|
||||||
const ShareWithUserEvent.getSharedUsers(),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
(error) {
|
(error) async {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -191,24 +192,24 @@ class ShareWithUserBloc extends Bloc<ShareWithUserEvent, ShareWithUserState> {
|
|||||||
emails: [event.email],
|
emails: [event.email],
|
||||||
);
|
);
|
||||||
|
|
||||||
result.fold(
|
await result.fold(
|
||||||
(_) {
|
(_) async {
|
||||||
|
final users = await _getLatestSharedUsersOrCurrentUsers();
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
updateAccessLevelResult: FlowySuccess(null),
|
updateAccessLevelResult: FlowySuccess(null),
|
||||||
|
users: users,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
add(
|
|
||||||
const ShareWithUserEvent.getSharedUsers(),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
(error) => emit(
|
(error) async {
|
||||||
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
errorMessage: error.msg,
|
errorMessage: error.msg,
|
||||||
isLoading: false,
|
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
|
@freezed
|
||||||
@ -312,14 +364,19 @@ class ShareWithUserEvent with _$ShareWithUserEvent {
|
|||||||
const factory ShareWithUserEvent.searchAvailableUsers({
|
const factory ShareWithUserEvent.searchAvailableUsers({
|
||||||
required String query,
|
required String query,
|
||||||
}) = SearchAvailableUsers;
|
}) = SearchAvailableUsers;
|
||||||
|
|
||||||
|
/// Turns the user into a member.
|
||||||
|
const factory ShareWithUserEvent.turnIntoMember({
|
||||||
|
required String email,
|
||||||
|
}) = TurnIntoMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class ShareWithUserState with _$ShareWithUserState {
|
class ShareWithUserState with _$ShareWithUserState {
|
||||||
const factory ShareWithUserState({
|
const factory ShareWithUserState({
|
||||||
@Default(null) UserProfilePB? currentUser,
|
@Default(null) UserProfilePB? currentUser,
|
||||||
@Default([]) List<SharedUser> users,
|
@Default([]) SharedUsers users,
|
||||||
@Default([]) List<SharedUser> availableUsers,
|
@Default([]) SharedUsers availableUsers,
|
||||||
@Default(false) bool isLoading,
|
@Default(false) bool isLoading,
|
||||||
@Default('') String errorMessage,
|
@Default('') String errorMessage,
|
||||||
@Default('') String shareLink,
|
@Default('') String shareLink,
|
||||||
@ -329,6 +386,7 @@ class ShareWithUserState with _$ShareWithUserState {
|
|||||||
@Default(null) FlowyResult<void, FlowyError>? shareResult,
|
@Default(null) FlowyResult<void, FlowyError>? shareResult,
|
||||||
@Default(null) FlowyResult<void, FlowyError>? removeResult,
|
@Default(null) FlowyResult<void, FlowyError>? removeResult,
|
||||||
@Default(null) FlowyResult<void, FlowyError>? updateAccessLevelResult,
|
@Default(null) FlowyResult<void, FlowyError>? updateAccessLevelResult,
|
||||||
|
@Default(null) FlowyResult<void, FlowyError>? turnIntoMemberResult,
|
||||||
}) = _ShareWithUserState;
|
}) = _ShareWithUserState;
|
||||||
|
|
||||||
factory ShareWithUserState.initial() => const 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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class ShareTab extends StatelessWidget {
|
class ShareTab extends StatefulWidget {
|
||||||
const ShareTab({
|
const ShareTab({
|
||||||
super.key,
|
super.key,
|
||||||
required this.workspaceId,
|
required this.workspaceId,
|
||||||
@ -19,13 +19,27 @@ class ShareTab extends StatelessWidget {
|
|||||||
final String workspaceId;
|
final String workspaceId;
|
||||||
final String pageId;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = AppFlowyTheme.of(context);
|
final theme = AppFlowyTheme.of(context);
|
||||||
|
|
||||||
return BlocConsumer<ShareWithUserBloc, ShareWithUserState>(
|
return BlocConsumer<ShareWithUserBloc, ShareWithUserState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
_onShareWithUserError(context, state);
|
_onListenShareWithUserState(context, state);
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
@ -39,6 +53,7 @@ class ShareTab extends StatelessWidget {
|
|||||||
// share page with user by email
|
// share page with user by email
|
||||||
VSpace(theme.spacing.l),
|
VSpace(theme.spacing.l),
|
||||||
ShareWithUserWidget(
|
ShareWithUserWidget(
|
||||||
|
controller: controller,
|
||||||
onInvite: (emails) => _onSharePageWithUser(
|
onInvite: (emails) => _onSharePageWithUser(
|
||||||
context,
|
context,
|
||||||
emails: emails,
|
emails: emails,
|
||||||
@ -92,7 +107,9 @@ class ShareTab extends StatelessWidget {
|
|||||||
// do nothing. the event doesn't support in the backend yet
|
// do nothing. the event doesn't support in the backend yet
|
||||||
},
|
},
|
||||||
onTurnIntoMember: (user) {
|
onTurnIntoMember: (user) {
|
||||||
// do nothing. the event doesn't support in the backend yet
|
context.read<ShareWithUserBloc>().add(
|
||||||
|
ShareWithUserEvent.turnIntoMember(email: user.email),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onRemoveAccess: (user) {
|
onRemoveAccess: (user) {
|
||||||
context.read<ShareWithUserBloc>().add(
|
context.read<ShareWithUserBloc>().add(
|
||||||
@ -102,13 +119,20 @@ class ShareTab extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onShareWithUserError(
|
void _onListenShareWithUserState(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ShareWithUserState state,
|
ShareWithUserState state,
|
||||||
) {
|
) {
|
||||||
final shareResult = state.shareResult;
|
final shareResult = state.shareResult;
|
||||||
if (shareResult != null) {
|
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
|
// TODO: handle the limiation error
|
||||||
showToastNotification(
|
showToastNotification(
|
||||||
message: error.msg,
|
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 String currentUserEmail;
|
||||||
final List<SharedUser> users;
|
final SharedUsers users;
|
||||||
final PeopleWithAccessSectionCallbacks? callbacks;
|
final PeopleWithAccessSectionCallbacks? callbacks;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -9,28 +9,33 @@ class ShareWithUserWidget extends StatefulWidget {
|
|||||||
const ShareWithUserWidget({
|
const ShareWithUserWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onInvite,
|
required this.onInvite,
|
||||||
|
this.controller,
|
||||||
});
|
});
|
||||||
|
|
||||||
final void Function(List<String> emails) onInvite;
|
final void Function(List<String> emails) onInvite;
|
||||||
|
final TextEditingController? controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ShareWithUserWidget> createState() => _ShareWithUserWidgetState();
|
State<ShareWithUserWidget> createState() => _ShareWithUserWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
||||||
final TextEditingController controller = TextEditingController();
|
late final TextEditingController effectiveController;
|
||||||
bool isButtonEnabled = false;
|
bool isButtonEnabled = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
controller.addListener(_onTextChanged);
|
effectiveController = widget.controller ?? TextEditingController();
|
||||||
|
effectiveController.addListener(_onTextChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
controller.dispose();
|
if (widget.controller == null) {
|
||||||
|
effectiveController.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -43,7 +48,7 @@ class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AFTextField(
|
child: AFTextField(
|
||||||
controller: controller,
|
controller: effectiveController,
|
||||||
size: AFTextFieldSize.m,
|
size: AFTextFieldSize.m,
|
||||||
hintText: LocaleKeys.shareTab_inviteByEmail.tr(),
|
hintText: LocaleKeys.shareTab_inviteByEmail.tr(),
|
||||||
),
|
),
|
||||||
@ -53,7 +58,7 @@ class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
|||||||
text: LocaleKeys.shareTab_invite.tr(),
|
text: LocaleKeys.shareTab_invite.tr(),
|
||||||
disabled: !isButtonEnabled,
|
disabled: !isButtonEnabled,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
widget.onInvite(controller.text.trim().split(','));
|
widget.onInvite(effectiveController.text.trim().split(','));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -62,7 +67,7 @@ class _ShareWithUserWidgetState extends State<ShareWithUserWidget> {
|
|||||||
|
|
||||||
void _onTextChanged() {
|
void _onTextChanged() {
|
||||||
setState(() {
|
setState(() {
|
||||||
final texts = controller.text.trim().split(',');
|
final texts = effectiveController.text.trim().split(',');
|
||||||
isButtonEnabled = texts.isNotEmpty && texts.every(isEmail);
|
isButtonEnabled = texts.isNotEmpty && texts.every(isEmail);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,11 @@ class SharedSection extends StatelessWidget {
|
|||||||
return SharedSectionError(errorMessage: state.errorMessage);
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -22,7 +22,7 @@ extension SharedViewPBExtension on SharedViewPB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension RepeatedSharedUserPBExtension on RepeatedSharedUserPB {
|
extension RepeatedSharedUserPBExtension on RepeatedSharedUserPB {
|
||||||
List<SharedUser> get sharedUsers {
|
SharedUsers get sharedUsers {
|
||||||
return items.map((e) => e.sharedUser).toList();
|
return items.map((e) => e.sharedUser).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,13 +38,15 @@ void main() {
|
|||||||
wait: const Duration(milliseconds: 100),
|
wait: const Duration(milliseconds: 100),
|
||||||
expect: () => [
|
expect: () => [
|
||||||
// First state: shareResult is null
|
// 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(
|
isA<ShareWithUserState>().having(
|
||||||
|
(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),
|
(s) => s.users.any((u) => u.email == email),
|
||||||
'users contains new user',
|
'users contains new user',
|
||||||
isTrue,
|
isTrue,
|
||||||
@ -65,11 +67,10 @@ void main() {
|
|||||||
// First state: removeResult is null
|
// First state: removeResult is null
|
||||||
isA<ShareWithUserState>()
|
isA<ShareWithUserState>()
|
||||||
.having((s) => s.removeResult, 'removeResult', isNull),
|
.having((s) => s.removeResult, 'removeResult', isNull),
|
||||||
// Second state: removeResult is Success
|
// Second state: removeResult is Success and users updated
|
||||||
isA<ShareWithUserState>()
|
isA<ShareWithUserState>()
|
||||||
.having((s) => s.removeResult, 'removeResult', isNotNull),
|
.having((s) => s.removeResult, 'removeResult', isNotNull)
|
||||||
// Third state: users updated, removeResult still Success
|
.having(
|
||||||
isA<ShareWithUserState>().having(
|
|
||||||
(s) => s.users.any((u) => u.email == email),
|
(s) => s.users.any((u) => u.email == email),
|
||||||
'users contains removed user',
|
'users contains removed user',
|
||||||
isFalse,
|
isFalse,
|
||||||
@ -94,19 +95,70 @@ void main() {
|
|||||||
'updateAccessLevelResult',
|
'updateAccessLevelResult',
|
||||||
isNull,
|
isNull,
|
||||||
),
|
),
|
||||||
// Second state: updateAccessLevelResult is Success
|
// Second state: updateAccessLevelResult is Success and users updated
|
||||||
isA<ShareWithUserState>().having(
|
isA<ShareWithUserState>()
|
||||||
|
.having(
|
||||||
(s) => s.updateAccessLevelResult,
|
(s) => s.updateAccessLevelResult,
|
||||||
'updateAccessLevelResult',
|
'updateAccessLevelResult',
|
||||||
isNotNull,
|
isNotNull,
|
||||||
),
|
)
|
||||||
// Third state: users updated, vivian's access level is fullAccess
|
.having(
|
||||||
isA<ShareWithUserState>().having(
|
|
||||||
(s) => s.users.firstWhere((u) => u.email == email).accessLevel,
|
(s) => s.users.firstWhere((u) => u.email == email).accessLevel,
|
||||||
'vivian accessLevel',
|
'vivian accessLevel',
|
||||||
ShareAccessLevel.fullAccess,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
wait: const Duration(milliseconds: 100),
|
||||||
|
expect: () => [
|
||||||
|
// First state: shareResult is null
|
||||||
|
isA<ShareWithUserState>().having(
|
||||||
|
(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