[Part 2] TF-4308 Add Create new label button in ChooseLabelModal (#4457)

This commit is contained in:
Dat Vu
2026-04-17 16:51:49 +07:00
committed by GitHub
parent a80de79537
commit a693ceb456
9 changed files with 297 additions and 254 deletions
@@ -1,31 +1,19 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:labels/model/label.dart'; import 'package:labels/model/label.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/create_new_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/edit_label_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart';
class OpenEditLabelModalParams with EquatableMixin { class OpenEditLabelModalParams with EquatableMixin {
final Label selectedLabel; final Label selectedLabel;
final AccountId accountId; final AccountId? accountId;
final VerifyNameInteractor verifyNameInteractor;
final CreateNewLabelInteractor createNewLabelInteractor;
final EditLabelInteractor editLabelInteractor;
const OpenEditLabelModalParams({ const OpenEditLabelModalParams({
required this.selectedLabel, required this.selectedLabel,
required this.accountId, required this.accountId,
required this.verifyNameInteractor,
required this.createNewLabelInteractor,
required this.editLabelInteractor,
}); });
@override @override
List<Object?> get props => [ List<Object?> get props => [
selectedLabel, selectedLabel,
accountId, accountId,
verifyNameInteractor,
createNewLabelInteractor,
editLabelInteractor,
]; ];
} }
@@ -41,6 +41,8 @@ class AddListLabelToListEmailsDelegate extends BaseController {
); );
}, },
imagePaths: imagePaths, imagePaths: imagePaths,
onCreateALabelAction: () => _labelController.onCreateALabelAction(
accountId: _labelController.accountId, hasResult: true),
), ),
dialogLabel: 'choose-label-modal', dialogLabel: 'choose-label-modal',
); );
@@ -9,12 +9,9 @@ import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.d
import 'package:tmail_ui_user/features/labels/domain/model/open_edit_label_modal_params.dart'; import 'package:tmail_ui_user/features/labels/domain/model/open_edit_label_modal_params.dart';
import 'package:tmail_ui_user/features/labels/domain/state/delete_a_label_state.dart'; import 'package:tmail_ui_user/features/labels/domain/state/delete_a_label_state.dart';
import 'package:tmail_ui_user/features/labels/domain/state/edit_label_state.dart'; import 'package:tmail_ui_user/features/labels/domain/state/edit_label_state.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/create_new_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/edit_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/presentation/label_controller.dart'; import 'package:tmail_ui_user/features/labels/presentation/label_controller.dart';
import 'package:tmail_ui_user/features/labels/presentation/models/label_action_type.dart'; import 'package:tmail_ui_user/features/labels/presentation/models/label_action_type.dart';
import 'package:tmail_ui_user/features/labels/presentation/widgets/create_new_label_modal.dart'; import 'package:tmail_ui_user/features/labels/presentation/widgets/create_new_label_modal.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart';
import 'package:tmail_ui_user/main/exceptions/logic_exception.dart'; import 'package:tmail_ui_user/main/exceptions/logic_exception.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -48,31 +45,10 @@ extension HandleLabelActionTypeExtension on LabelController {
required AccountId? accountId, required AccountId? accountId,
required Label label, required Label label,
}) async { }) async {
final createNewLabelInteractor = getBinding<CreateNewLabelInteractor>();
final editLabelInteractor = getBinding<EditLabelInteractor>();
final verifyNameInteractor = getBinding<VerifyNameInteractor>();
final isInteractorsInitialized = createNewLabelInteractor != null &&
editLabelInteractor != null &&
verifyNameInteractor != null;
if (!isInteractorsInitialized) {
_handleEditLabelFailure(EditLabelFailure(const InteractorNotInitialized()));
return;
}
if (accountId == null) {
_handleEditLabelFailure(EditLabelFailure(NotFoundAccountIdException()));
return;
}
final resultState = await openEditLabelModal( final resultState = await openEditLabelModal(
params: OpenEditLabelModalParams( params: OpenEditLabelModalParams(
selectedLabel: label, selectedLabel: label,
accountId: accountId, accountId: accountId,
verifyNameInteractor: verifyNameInteractor,
createNewLabelInteractor: createNewLabelInteractor,
editLabelInteractor: editLabelInteractor,
), ),
); );
@@ -86,10 +62,12 @@ extension HandleLabelActionTypeExtension on LabelController {
void _handleEditLabelSuccess(EditLabelSuccess success) { void _handleEditLabelSuccess(EditLabelSuccess success) {
toastManager.showMessageSuccess(success); toastManager.showMessageSuccess(success);
syncListLabels(success.newLabel); syncListLabels(success.newLabel);
popBack();
} }
void _handleEditLabelFailure(EditLabelFailure failure) { void _handleEditLabelFailure(EditLabelFailure failure) {
toastManager.showMessageFailure(failure); toastManager.showMessageFailure(failure);
popBack();
} }
void syncListLabels(Label newLabel) { void syncListLabels(Label newLabel) {
@@ -8,15 +8,12 @@ import 'package:jmap_dart_client/jmap/core/state.dart';
import 'package:labels/labels.dart'; import 'package:labels/labels.dart';
import 'package:model/mailbox/expand_mode.dart'; import 'package:model/mailbox/expand_mode.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart';
import 'package:tmail_ui_user/features/labels/domain/model/open_edit_label_modal_params.dart'; import 'package:tmail_ui_user/features/labels/domain/model/open_edit_label_modal_params.dart';
import 'package:tmail_ui_user/features/labels/domain/state/create_new_label_state.dart'; import 'package:tmail_ui_user/features/labels/domain/state/create_new_label_state.dart';
import 'package:tmail_ui_user/features/labels/domain/state/delete_a_label_state.dart'; import 'package:tmail_ui_user/features/labels/domain/state/delete_a_label_state.dart';
import 'package:tmail_ui_user/features/labels/domain/state/get_all_label_state.dart'; import 'package:tmail_ui_user/features/labels/domain/state/get_all_label_state.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/create_new_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/delete_a_label_interactor.dart'; import 'package:tmail_ui_user/features/labels/domain/usecases/delete_a_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/edit_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/get_all_label_interactor.dart'; import 'package:tmail_ui_user/features/labels/domain/usecases/get_all_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/get_label_changes_interactor.dart'; import 'package:tmail_ui_user/features/labels/domain/usecases/get_label_changes_interactor.dart';
import 'package:tmail_ui_user/features/labels/presentation/extensions/handle_label_action_type_extension.dart'; import 'package:tmail_ui_user/features/labels/presentation/extensions/handle_label_action_type_extension.dart';
@@ -25,13 +22,11 @@ import 'package:tmail_ui_user/features/labels/presentation/extensions/handle_lab
import 'package:tmail_ui_user/features/labels/presentation/label_interactor_bindings.dart'; import 'package:tmail_ui_user/features/labels/presentation/label_interactor_bindings.dart';
import 'package:tmail_ui_user/features/labels/presentation/mixin/label_context_menu_mixin.dart'; import 'package:tmail_ui_user/features/labels/presentation/mixin/label_context_menu_mixin.dart';
import 'package:tmail_ui_user/features/labels/presentation/widgets/create_new_label_modal.dart'; import 'package:tmail_ui_user/features/labels/presentation/widgets/create_new_label_modal.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart';
import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/dialog_router.dart';
import 'package:tmail_ui_user/features/manage_account/domain/state/get_label_setting_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_label_setting_state.dart';
import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_label_setting_state_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_label_setting_state_interactor.dart';
import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/websocket/web_socket_queue_handler.dart';
import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart';
import 'package:tmail_ui_user/main/exceptions/logic_exception.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
class LabelController extends BaseController class LabelController extends BaseController
@@ -142,10 +137,7 @@ class LabelController extends BaseController
} }
Future<dynamic> openCreateNewLabelModal({ Future<dynamic> openCreateNewLabelModal({
required AccountId accountId, required AccountId? accountId,
required VerifyNameInteractor verifyNameInteractor,
required CreateNewLabelInteractor createNewLabelInteractor,
required EditLabelInteractor editLabelInteractor,
}) async { }) async {
return DialogRouter().openDialogModal( return DialogRouter().openDialogModal(
child: CreateNewLabelModal( child: CreateNewLabelModal(
@@ -153,9 +145,6 @@ class LabelController extends BaseController
labels: labels, labels: labels,
accountId: accountId, accountId: accountId,
imagePaths: imagePaths, imagePaths: imagePaths,
verifyNameInteractor: verifyNameInteractor,
createNewLabelInteractor: createNewLabelInteractor,
editLabelInteractor: editLabelInteractor,
), ),
dialogLabel: 'create-new-label-modal', dialogLabel: 'create-new-label-modal',
); );
@@ -172,50 +161,28 @@ class LabelController extends BaseController
imagePaths: imagePaths, imagePaths: imagePaths,
selectedLabel: params.selectedLabel, selectedLabel: params.selectedLabel,
actionType: LabelActionType.edit, actionType: LabelActionType.edit,
verifyNameInteractor: params.verifyNameInteractor,
createNewLabelInteractor: params.createNewLabelInteractor,
editLabelInteractor: params.editLabelInteractor,
), ),
dialogLabel: 'edit-label-modal', dialogLabel: 'edit-label-modal',
); );
} }
Future<void> onCreateALabelAction({ Future<Label?> onCreateALabelAction({
required AccountId? accountId, required AccountId? accountId,
OnLabelActionCallback? onLabelActionCallback, OnLabelActionCallback? onLabelActionCallback,
bool shouldPop = false, bool shouldPop = false,
bool hasResult = false,
}) async { }) async {
final createNewLabelInteractor = getBinding<CreateNewLabelInteractor>();
final editLabelInteractor = getBinding<EditLabelInteractor>();
final verifyNameInteractor = getBinding<VerifyNameInteractor>();
final isInteractorsInitialized = createNewLabelInteractor != null &&
editLabelInteractor != null &&
verifyNameInteractor != null;
if (!isInteractorsInitialized) {
_handleCreateNewLabelFailure(
failure: CreateNewLabelFailure(const InteractorNotInitialized()),
);
return;
}
if (accountId == null) {
_handleCreateNewLabelFailure(
failure: CreateNewLabelFailure(NotFoundAccountIdException()),
);
return;
}
final resultState = await openCreateNewLabelModal( final resultState = await openCreateNewLabelModal(
accountId: accountId, accountId: accountId,
verifyNameInteractor: verifyNameInteractor,
createNewLabelInteractor: createNewLabelInteractor,
editLabelInteractor: editLabelInteractor,
); );
if (resultState is CreateNewLabelSuccess) { if (resultState is CreateNewLabelSuccess) {
_handleCreateNewLabelSuccess( _addLabelToList(resultState.newLabel);
if (hasResult) {
toastManager.showMessageSuccess(resultState);
return resultState.newLabel;
}
_handleCreateNewLabelSuccessWithoutListUpdate(
success: resultState, success: resultState,
onLabelActionCallback: onLabelActionCallback, onLabelActionCallback: onLabelActionCallback,
shouldPop: shouldPop, shouldPop: shouldPop,
@@ -223,9 +190,11 @@ class LabelController extends BaseController
} else if (resultState is CreateNewLabelFailure) { } else if (resultState is CreateNewLabelFailure) {
_handleCreateNewLabelFailure(failure: resultState); _handleCreateNewLabelFailure(failure: resultState);
} }
return null;
} }
void _handleCreateNewLabelSuccess({ void _handleCreateNewLabelSuccessWithoutListUpdate({
required CreateNewLabelSuccess success, required CreateNewLabelSuccess success,
OnLabelActionCallback? onLabelActionCallback, OnLabelActionCallback? onLabelActionCallback,
bool shouldPop = false, bool shouldPop = false,
@@ -233,7 +202,6 @@ class LabelController extends BaseController
if (onLabelActionCallback == null && !shouldPop) { if (onLabelActionCallback == null && !shouldPop) {
toastManager.showMessageSuccess(success); toastManager.showMessageSuccess(success);
} }
_addLabelToList(success.newLabel);
if (onLabelActionCallback != null) { if (onLabelActionCallback != null) {
onLabelActionCallback(success.newLabel); onLabelActionCallback(success.newLabel);
} }
@@ -11,7 +11,6 @@ import 'package:tmail_ui_user/features/labels/domain/usecases/edit_label_interac
import 'package:tmail_ui_user/features/labels/domain/usecases/delete_a_label_interactor.dart'; import 'package:tmail_ui_user/features/labels/domain/usecases/delete_a_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/get_all_label_interactor.dart'; import 'package:tmail_ui_user/features/labels/domain/usecases/get_all_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/get_label_changes_interactor.dart'; import 'package:tmail_ui_user/features/labels/domain/usecases/get_label_changes_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart';
import 'package:tmail_ui_user/main/exceptions/thrower/remote_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/thrower/remote_exception_thrower.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@@ -35,11 +34,11 @@ class LabelInteractorBindings extends InteractorsBindings {
@override @override
void bindingsInteractor() { void bindingsInteractor() {
Get.lazyPut(() => GetAllLabelInteractor(Get.find<LabelRepository>())); Get.lazyPut(() => GetAllLabelInteractor(Get.find<LabelRepository>()));
Get.lazyPut(() => CreateNewLabelInteractor(Get.find<LabelRepository>())); // Use `fenix: true` to avoid GetX dispose or SmartManagement cleanup when using it in dialogs.
Get.lazyPut(() => EditLabelInteractor(Get.find<LabelRepository>())); Get.lazyPut(() => CreateNewLabelInteractor(Get.find<LabelRepository>()), fenix: true);
Get.lazyPut(() => EditLabelInteractor(Get.find<LabelRepository>()), fenix: true);
Get.lazyPut(() => DeleteALabelInteractor(Get.find<LabelRepository>())); Get.lazyPut(() => DeleteALabelInteractor(Get.find<LabelRepository>()));
Get.lazyPut(() => GetLabelChangesInteractor(Get.find<LabelRepository>())); Get.lazyPut(() => GetLabelChangesInteractor(Get.find<LabelRepository>()));
Get.lazyPut(() => VerifyNameInteractor());
} }
@override @override
@@ -12,17 +12,20 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
typedef OnLabelAsToEmailsAction = Function(List<Label> labels); typedef OnLabelAsToEmailsAction = Function(List<Label> labels);
typedef OnCreateALabelAction = Future<Label?> Function();
class ChooseLabelModal extends StatefulWidget { class ChooseLabelModal extends StatefulWidget {
final List<Label> labels; final List<Label> labels;
final ImagePaths imagePaths; final ImagePaths imagePaths;
final OnLabelAsToEmailsAction onLabelAsToEmailsAction; final OnLabelAsToEmailsAction onLabelAsToEmailsAction;
final OnCreateALabelAction onCreateALabelAction;
const ChooseLabelModal({ const ChooseLabelModal({
super.key, super.key,
required this.labels, required this.labels,
required this.imagePaths, required this.imagePaths,
required this.onLabelAsToEmailsAction, required this.onLabelAsToEmailsAction,
required this.onCreateALabelAction,
}); });
@override @override
@@ -30,84 +33,80 @@ class ChooseLabelModal extends StatefulWidget {
} }
class _ChooseLabelModalState extends State<ChooseLabelModal> { class _ChooseLabelModalState extends State<ChooseLabelModal> {
late final ValueNotifier<List<Label>> _labelListNotifier;
@override
void initState() {
super.initState();
_labelListNotifier = ValueNotifier([...widget.labels]);
}
@override
void dispose() {
_labelListNotifier.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appLocalizations = AppLocalizations.of(context);
return LayoutBuilder(builder: (_, constraints) { return LayoutBuilder(builder: (_, constraints) {
final currentScreenWidth = constraints.maxWidth; final currentScreenWidth = constraints.maxWidth;
final currentScreenHeight = constraints.maxHeight; final currentScreenHeight = constraints.maxHeight;
final height = math.max(0.0, math.min(currentScreenHeight - 100, 645.0)); final height = math.max(0.0, math.min(currentScreenHeight - 100, 645.0));
final width = math.max(0.0, math.min(currentScreenWidth - 32, 536.0)); final width = math.max(0.0, math.min(currentScreenWidth - 32, 536.0));
Widget bodyWidget = Container( return Center(
decoration: BoxDecoration( child: Container(
color: Colors.white, width: width,
borderRadius: const BorderRadius.all(Radius.circular(16)), height: height,
boxShadow: [ decoration: _buildModalDecoration(),
BoxShadow( clipBehavior: Clip.antiAlias,
color: Colors.black.withValues(alpha: 0.08), child: Stack(children: [_buildBodyContent(), _buildCloseButton()]),
blurRadius: 24,
offset: const Offset(0, 2),
),
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 2,
),
],
),
width: width,
height: height,
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(appLocalizations),
const Divider(height: 1, color: Colors.black12),
_buildSubtitle(appLocalizations),
if (widget.labels.isNotEmpty)
Expanded(
child: ListLabelWithActionModal(
labels: widget.labels,
imagePaths: widget.imagePaths,
onLabelAsToEmailsAction: widget.onLabelAsToEmailsAction,
onCloseModal: _onCloseModal,
),
)
else
Expanded(
child: Center(
child: NoLabelYetWidget(imagePaths: widget.imagePaths),
),
),
],
),
DefaultCloseButtonWidget(
iconClose: widget.imagePaths.icCloseDialog,
isAlignTopEnd: false,
onTapActionCallback: _onCloseModal,
),
],
), ),
); );
return Center(child: bodyWidget);
}); });
} }
Widget _buildTitle(AppLocalizations appLocalizations) { BoxDecoration _buildModalDecoration() {
return BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(16)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 24,
offset: const Offset(0, 2),
),
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 2,
),
],
);
}
Widget _buildBodyContent() {
final appLocalizations = AppLocalizations.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeader(appLocalizations),
const Divider(height: 1, color: Colors.black12),
_buildSubtitle(appLocalizations),
Expanded(child: _buildListOrEmptyState()),
],
);
}
Widget _buildHeader(AppLocalizations appLocalizations) {
return Container( return Container(
height: 52, height: 52,
alignment: Alignment.center, alignment: Alignment.center,
padding: const EdgeInsetsDirectional.symmetric(horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text( child: Text(
appLocalizations.chooseLabel, appLocalizations.chooseLabel,
style: ThemeUtils.textStyleHeadingH6().copyWith( style: ThemeUtils.textStyleHeadingH6().copyWith(color: Colors.black),
color: Colors.black,
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -130,6 +129,45 @@ class _ChooseLabelModalState extends State<ChooseLabelModal> {
); );
} }
Widget _buildListOrEmptyState() {
return ValueListenableBuilder<List<Label>>(
valueListenable: _labelListNotifier,
builder: (context, labels, _) {
if (labels.isEmpty) {
return Center(
child: NoLabelYetWidget(
imagePaths: widget.imagePaths,
onCreateLabel: _onCreateNewLabel,
),
);
}
return ListLabelWithActionModal(
labels: labels,
imagePaths: widget.imagePaths,
onLabelAsToEmailsAction: widget.onLabelAsToEmailsAction,
onCloseModal: _onCloseModal,
onCreateALabelAction: _onCreateNewLabel,
);
},
);
}
Widget _buildCloseButton() {
return DefaultCloseButtonWidget(
iconClose: widget.imagePaths.icCloseDialog,
isAlignTopEnd: false,
onTapActionCallback: _onCloseModal,
);
}
Future<void> _onCreateNewLabel() async {
final newLabel = await widget.onCreateALabelAction();
if (newLabel != null) {
_labelListNotifier.value = [..._labelListNotifier.value, newLabel];
}
}
void _onCloseModal() { void _onCloseModal() {
popBack(); popBack();
} }
@@ -19,6 +19,7 @@ import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:labels/labels.dart'; import 'package:labels/labels.dart';
import 'package:tmail_ui_user/features/base/widget/label_input_field_builder.dart'; import 'package:tmail_ui_user/features/base/widget/label_input_field_builder.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/labels/domain/exceptions/label_exceptions.dart'; import 'package:tmail_ui_user/features/labels/domain/exceptions/label_exceptions.dart';
import 'package:tmail_ui_user/features/labels/domain/model/edit_label_request.dart'; import 'package:tmail_ui_user/features/labels/domain/model/edit_label_request.dart';
import 'package:tmail_ui_user/features/labels/domain/state/create_new_label_state.dart'; import 'package:tmail_ui_user/features/labels/domain/state/create_new_label_state.dart';
@@ -33,6 +34,7 @@ import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification
import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_view_state.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_view_state.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart';
import 'package:tmail_ui_user/main/exceptions/logic_exception.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -46,12 +48,9 @@ enum LabelPositiveButtonState {
class CreateNewLabelModal extends StatefulWidget { class CreateNewLabelModal extends StatefulWidget {
final List<Label> labels; final List<Label> labels;
final AccountId accountId; final AccountId? accountId;
final ImagePaths imagePaths; final ImagePaths imagePaths;
final LabelActionType actionType; final LabelActionType actionType;
final CreateNewLabelInteractor createNewLabelInteractor;
final EditLabelInteractor editLabelInteractor;
final VerifyNameInteractor verifyNameInteractor;
final Label? selectedLabel; final Label? selectedLabel;
const CreateNewLabelModal({ const CreateNewLabelModal({
@@ -59,9 +58,6 @@ class CreateNewLabelModal extends StatefulWidget {
required this.labels, required this.labels,
required this.accountId, required this.accountId,
required this.imagePaths, required this.imagePaths,
required this.createNewLabelInteractor,
required this.editLabelInteractor,
required this.verifyNameInteractor,
this.actionType = LabelActionType.create, this.actionType = LabelActionType.create,
this.selectedLabel, this.selectedLabel,
}); });
@@ -85,10 +81,16 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
List<String> _labelDisplayNameList = <String>[]; List<String> _labelDisplayNameList = <String>[];
Color? _selectedColor; Color? _selectedColor;
StreamSubscription? _streamSubscription; StreamSubscription? _streamSubscription;
CreateNewLabelInteractor? _createNewLabelInteractor;
EditLabelInteractor? _editLabelInteractor;
VerifyNameInteractor? _verifyNameInteractor;
AccountId? _accountId;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_accountId = widget.accountId;
_initInteractors();
final selectedLabel = widget.selectedLabel; final selectedLabel = widget.selectedLabel;
final labels = widget.labels; final labels = widget.labels;
if (selectedLabel != null) { if (selectedLabel != null) {
@@ -109,6 +111,18 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
}); });
} }
void _initInteractors() {
_createNewLabelInteractor = getBinding<CreateNewLabelInteractor>();
_editLabelInteractor = getBinding<EditLabelInteractor>();
_verifyNameInteractor = getBinding<VerifyNameInteractor>();
}
void _disposeInteractors() {
_createNewLabelInteractor = null;
_editLabelInteractor = null;
_verifyNameInteractor = null;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appLocalizations = AppLocalizations.of(context); final appLocalizations = AppLocalizations.of(context);
@@ -376,7 +390,10 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
} }
String? _verifyLabelName(AppLocalizations appLocalizations, String value) { String? _verifyLabelName(AppLocalizations appLocalizations, String value) {
final result = widget.verifyNameInteractor.execute( if (_verifyNameInteractor == null) {
return null;
}
final result = _verifyNameInteractor!.execute(
value, value,
_validators, _validators,
); );
@@ -459,8 +476,18 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
} }
void _performCreateLabel(Label newLabel) { void _performCreateLabel(Label newLabel) {
_streamSubscription = widget.createNewLabelInteractor if (_createNewLabelInteractor == null) {
.execute(widget.accountId, newLabel) popBack(result: CreateNewLabelFailure(const InteractorNotInitialized()));
return;
}
if (_accountId == null) {
popBack(result: CreateNewLabelFailure(NotFoundAccountIdException()));
return;
}
_streamSubscription = _createNewLabelInteractor!
.execute(_accountId!, newLabel)
.listen(_handleDataStream, onError: _handleErrorStream); .listen(_handleDataStream, onError: _handleErrorStream);
} }
@@ -511,8 +538,16 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
newLabel: newLabel, newLabel: newLabel,
); );
_streamSubscription = widget.editLabelInteractor if (_editLabelInteractor == null) {
.execute(widget.accountId, labelRequest) popBack(result: EditLabelFailure(const InteractorNotInitialized()));
return;
}
if (_accountId == null) {
popBack(result: EditLabelFailure(NotFoundAccountIdException()));
return;
}
_streamSubscription = _editLabelInteractor!
.execute(_accountId!, labelRequest)
.listen(_handleDataStream, onError: _handleErrorStream); .listen(_handleDataStream, onError: _handleErrorStream);
} }
@@ -528,6 +563,8 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
_labelDisplayNameList = []; _labelDisplayNameList = [];
_streamSubscription?.cancel(); _streamSubscription?.cancel();
_streamSubscription = null; _streamSubscription = null;
_accountId = null;
_disposeInteractors();
super.dispose(); super.dispose();
} }
} }
@@ -1,6 +1,11 @@
import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/utils/app_logger.dart';
import 'package:core/presentation/views/dialog/modal_list_action_button_widget.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/views/dialog/confirm_dialog_button.dart';
import 'package:core/presentation/views/dialog/modal_list_action_button_widget.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:labels/model/label.dart'; import 'package:labels/model/label.dart';
import 'package:tmail_ui_user/features/labels/presentation/widgets/choose_label_modal.dart'; import 'package:tmail_ui_user/features/labels/presentation/widgets/choose_label_modal.dart';
import 'package:tmail_ui_user/features/mailbox/presentation/widgets/labels/label_list_item.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/widgets/labels/label_list_item.dart';
@@ -11,6 +16,7 @@ class ListLabelWithActionModal extends StatefulWidget {
final ImagePaths imagePaths; final ImagePaths imagePaths;
final OnLabelAsToEmailsAction onLabelAsToEmailsAction; final OnLabelAsToEmailsAction onLabelAsToEmailsAction;
final VoidCallback onCloseModal; final VoidCallback onCloseModal;
final AsyncCallback onCreateALabelAction;
const ListLabelWithActionModal({ const ListLabelWithActionModal({
super.key, super.key,
@@ -18,6 +24,7 @@ class ListLabelWithActionModal extends StatefulWidget {
required this.imagePaths, required this.imagePaths,
required this.onLabelAsToEmailsAction, required this.onLabelAsToEmailsAction,
required this.onCloseModal, required this.onCloseModal,
required this.onCreateALabelAction,
}); });
@override @override
@@ -26,87 +33,112 @@ class ListLabelWithActionModal extends StatefulWidget {
} }
class _ListLabelWithActionModalState extends State<ListLabelWithActionModal> { class _ListLabelWithActionModalState extends State<ListLabelWithActionModal> {
final ValueNotifier<bool> _addLabelStateNotifier = ValueNotifier(false); final ValueNotifier<Set<Id>> _selectedIdsNotifier = ValueNotifier({});
final ValueNotifier<List<Label>> _selectedLabelStateNotifier =
ValueNotifier([]);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final labelList = widget.labels;
final appLocalizations = AppLocalizations.of(context);
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Expanded( Expanded(child: _buildLabelListView()),
child: ListView.builder( _buildBottomActionButtons(),
padding: EdgeInsets.zero,
itemCount: labelList.length,
itemBuilder: (context, index) {
final label = labelList[index];
return ValueListenableBuilder(
valueListenable: _selectedLabelStateNotifier,
builder: (_, selectedLabels, __) {
final isSelected = selectedLabels.contains(label);
return LabelListItem(
label: label,
imagePaths: widget.imagePaths,
padding: const EdgeInsetsDirectional.only(
start: 4,
end: 16,
),
enableSelectedIcon: true,
isSelected: isSelected,
onOpenLabelCallback: _onToggleLabel,
);
},
);
},
),
),
ValueListenableBuilder(
valueListenable: _addLabelStateNotifier,
builder: (_, value, __) {
return ModalListActionButtonWidget(
positiveLabel: appLocalizations.addLabel,
negativeLabel: appLocalizations.cancel,
padding: const EdgeInsets.symmetric(
vertical: 25,
horizontal: 25,
),
isPositiveActionEnabled: value,
onPositiveAction: _onAddLabel,
onNegativeAction: widget.onCloseModal,
);
},
),
], ],
); );
} }
void _onToggleLabel(Label selectedLabel) { Widget _buildLabelListView() {
final currentLabels = _selectedLabelStateNotifier.value; final labels = widget.labels;
return ListView.builder(
if (currentLabels.contains(selectedLabel)) { itemCount: labels.length + 1,
_selectedLabelStateNotifier.value = padding: EdgeInsets.zero,
currentLabels.where((label) => label.id != selectedLabel.id).toList(); itemBuilder: (_, index) {
} else { if (index == labels.length) {
_selectedLabelStateNotifier.value = [...currentLabels, selectedLabel]; return _buildCreateLabelButton();
} }
return _buildLabelItem(labels[index]);
_addLabelStateNotifier.value = _selectedLabelStateNotifier.value.isNotEmpty; },
);
} }
void _onAddLabel() { Widget _buildLabelItem(Label label) {
widget.onLabelAsToEmailsAction(_selectedLabelStateNotifier.value); return ValueListenableBuilder<Set<Id>>(
valueListenable: _selectedIdsNotifier,
builder: (_, selectedIds, __) {
final isSelected = selectedIds.contains(label.id);
return LabelListItem(
label: label,
imagePaths: widget.imagePaths,
padding: const EdgeInsetsDirectional.only(start: 4, end: 16),
enableSelectedIcon: true,
isSelected: isSelected,
onOpenLabelCallback: _onToggleLabel,
);
},
);
}
Widget _buildCreateLabelButton() {
return Container(
height: 36,
alignment: AlignmentDirectional.centerStart,
margin:
const EdgeInsetsDirectional.only(start: 4, end: 8, top: 4, bottom: 8),
child: ConfirmDialogButton(
label: AppLocalizations.of(context).createALabel,
backgroundColor: Colors.transparent,
textColor: AppColor.primaryMain,
padding: const EdgeInsetsDirectional.only(start: 10, end: 16),
icon: widget.imagePaths.icAddIdentity,
onTapAction: widget.onCreateALabelAction,
),
);
}
Widget _buildBottomActionButtons() {
final localizations = AppLocalizations.of(context);
return ValueListenableBuilder<Set<Id>>(
valueListenable: _selectedIdsNotifier,
builder: (_, selectedIds, __) {
return ModalListActionButtonWidget(
positiveLabel: localizations.addLabel,
negativeLabel: localizations.cancel,
padding: const EdgeInsets.all(25),
isPositiveActionEnabled: selectedIds.isNotEmpty,
onPositiveAction: _onConfirmAction,
onNegativeAction: widget.onCloseModal,
);
},
);
}
void _onToggleLabel(Label label) {
final currentIds = Set<Id>.from(_selectedIdsNotifier.value);
final labelId = label.id;
if (labelId == null) {
logWarning('ListLabelWithActionModal::_onToggleLabel: labelId is null for label: $label');
return;
}
if (currentIds.contains(labelId)) {
currentIds.remove(labelId);
} else {
currentIds.add(labelId);
}
_selectedIdsNotifier.value = currentIds;
}
void _onConfirmAction() {
final selectedLabels = widget.labels
.where((label) => _selectedIdsNotifier.value.contains(label.id))
.toList();
widget.onLabelAsToEmailsAction(selectedLabels);
widget.onCloseModal(); widget.onCloseModal();
} }
@override @override
void dispose() { void dispose() {
_addLabelStateNotifier.dispose(); _selectedIdsNotifier.dispose();
_selectedLabelStateNotifier.dispose();
super.dispose(); super.dispose();
} }
} }
@@ -1,14 +1,15 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/presentation/utils/theme_utils.dart';
import 'package:core/presentation/views/dialog/confirm_dialog_button.dart'; import 'package:core/presentation/views/dialog/confirm_dialog_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
class NoLabelYetWidget extends StatelessWidget { class NoLabelYetWidget extends StatelessWidget {
final ImagePaths imagePaths; final ImagePaths imagePaths;
final VoidCallback? onCreateLabel; final AsyncCallback? onCreateLabel;
const NoLabelYetWidget({ const NoLabelYetWidget({
super.key, super.key,
@@ -20,22 +21,22 @@ class NoLabelYetWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appLocalizations = AppLocalizations.of(context); final appLocalizations = AppLocalizations.of(context);
return Container( return Center(
margin: const EdgeInsets.all(16),
constraints: const BoxConstraints(maxWidth: 352),
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
child: Column( padding: const EdgeInsets.all(16),
mainAxisSize: MainAxisSize.min, child: ConstrainedBox(
children: [ constraints: const BoxConstraints(maxWidth: 352),
_IconNoTag(icon: imagePaths.icNoTag), child: Column(
_LabelNoTag( mainAxisSize: MainAxisSize.min,
text: appLocalizations.noLabelsYet, children: [
textStyle: ThemeUtils.textStyleInter600(), _IconNoTag(icon: imagePaths.icNoTag),
padding: const EdgeInsetsDirectional.only(top: 16), _LabelNoTag(
), text: appLocalizations.noLabelsYet,
Flexible( textStyle: ThemeUtils.textStyleInter600(),
child: _LabelNoTag( padding: const EdgeInsetsDirectional.only(top: 16),
),
_LabelNoTag(
text: appLocalizations.noLabelsYetMessageDescriptions, text: appLocalizations.noLabelsYetMessageDescriptions,
textStyle: ThemeUtils.textStyleInter400.copyWith( textStyle: ThemeUtils.textStyleInter400.copyWith(
fontSize: 16, fontSize: 16,
@@ -45,22 +46,22 @@ class NoLabelYetWidget extends StatelessWidget {
), ),
padding: const EdgeInsetsDirectional.only(top: 36), padding: const EdgeInsetsDirectional.only(top: 36),
), ),
), if (onCreateLabel != null)
if (onCreateLabel != null) Container(
Container( constraints: const BoxConstraints(minWidth: 186),
constraints: const BoxConstraints(minWidth: 186), height: 48,
height: 48, margin: const EdgeInsetsDirectional.only(top: 16),
margin: const EdgeInsetsDirectional.only(top: 16), child: ConfirmDialogButton(
child: ConfirmDialogButton( label: appLocalizations.createALabel,
label: appLocalizations.createALabel, backgroundColor: Colors.white,
backgroundColor: Colors.white, textColor: AppColor.primaryMain,
textColor: AppColor.primaryMain, borderColor: AppColor.primaryMain,
borderColor: AppColor.primaryMain, icon: imagePaths.icAddIdentity,
icon: imagePaths.icAddIdentity, onTapAction: onCreateLabel!,
onTapAction: onCreateLabel!, ),
), ),
),
], ],
),
), ),
), ),
); );