From 190ccc7bc304a3b1a6e0082e5aaccdadb7d3842d Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 4 Feb 2026 13:53:01 +0700 Subject: [PATCH] TF-4301 Add "Create a new label" button in Label As menus and wire up tagging in email view --- .../views/button/tmail_button_widget.dart | 6 +- .../container/tmail_container_widget.dart | 2 +- .../views/dialog/confirm_dialog_button.dart | 6 +- .../modal_list_action_button_widget.dart | 18 +- .../loading/cupertino_loading_widget.dart | 10 +- ...info_when_open_mail_from_tag_scenario.dart | 2 +- .../presentation/composer_view_web.dart | 1 - .../controller/single_email_controller.dart | 12 +- .../email/presentation/email_view.dart | 25 ++- .../handle_label_for_email_extension.dart | 27 ++- .../email_action_reactor.dart | 10 +- .../handle_label_action_type_extension.dart | 99 ++++------- .../labels/presentation/label_controller.dart | 120 ++++++++----- .../mixin/add_label_to_email_mixin.dart | 15 +- .../presentation/mixin/label_modal_mixin.dart | 56 +++++++ .../mixin/label_sub_menu_mixin.dart | 4 +- .../widgets/add_label_to_email_modal.dart | 64 ++++--- .../widgets/create_new_label_modal.dart | 158 ++++++++++++++---- .../widgets/label_list_context_menu.dart | 66 ++++++-- .../presentation/base_mailbox_view.dart | 1 - .../handle_label_action_type_extension.dart | 1 - .../identities/widgets/signature_builder.dart | 3 +- .../handle_email_more_action_extension.dart | 12 ++ .../email_more_action_context_menu_mixin.dart | 11 +- .../thread/presentation/thread_view.dart | 10 ++ .../labels/add_label_to_thread_extension.dart | 19 ++- .../on_thread_detail_action_click.dart | 37 +++- lib/main/bindings/local/local_bindings.dart | 6 + lib/main/routes/dialog_router.dart | 46 +++-- 29 files changed, 600 insertions(+), 247 deletions(-) create mode 100644 lib/features/labels/presentation/mixin/label_modal_mixin.dart diff --git a/core/lib/presentation/views/button/tmail_button_widget.dart b/core/lib/presentation/views/button/tmail_button_widget.dart index 0b7a75f4d..931c5b08e 100644 --- a/core/lib/presentation/views/button/tmail_button_widget.dart +++ b/core/lib/presentation/views/button/tmail_button_widget.dart @@ -43,7 +43,7 @@ class TMailButtonWidget extends StatelessWidget { final bool isLoading; final Color? hoverColor; final TextOverflow? textOverflow; - final Alignment? alignment; + final AlignmentGeometry? alignment; final bool isTextExpanded; const TMailButtonWidget({ @@ -109,7 +109,7 @@ class TMailButtonWidget extends StatelessWidget { List? boxShadow, EdgeInsetsGeometry? margin, Color? hoverColor, - Alignment? alignment, + AlignmentGeometry? alignment, BoxBorder? border, }) { return TMailButtonWidget( @@ -165,7 +165,7 @@ class TMailButtonWidget extends StatelessWidget { int? maxLines, Color? hoverColor, TextOverflow? textOverflow, - Alignment? alignment, + AlignmentGeometry? alignment, bool isTextExpanded = false, }) { return TMailButtonWidget( diff --git a/core/lib/presentation/views/container/tmail_container_widget.dart b/core/lib/presentation/views/container/tmail_container_widget.dart index 70974fba2..d5c9fc6b8 100644 --- a/core/lib/presentation/views/container/tmail_container_widget.dart +++ b/core/lib/presentation/views/container/tmail_container_widget.dart @@ -23,7 +23,7 @@ class TMailContainerWidget extends StatelessWidget { final List? boxShadow; final BoxBorder? border; final Color? hoverColor; - final Alignment? alignment; + final AlignmentGeometry? alignment; const TMailContainerWidget({ super.key, diff --git a/core/lib/presentation/views/dialog/confirm_dialog_button.dart b/core/lib/presentation/views/dialog/confirm_dialog_button.dart index 0597ebe22..5ddb74d77 100644 --- a/core/lib/presentation/views/dialog/confirm_dialog_button.dart +++ b/core/lib/presentation/views/dialog/confirm_dialog_button.dart @@ -15,6 +15,7 @@ class ConfirmDialogButton extends StatelessWidget { final double? radius; final EdgeInsetsGeometry? padding; final VoidCallback? onTapAction; + final Widget? child; const ConfirmDialogButton({ super.key, @@ -28,6 +29,7 @@ class ConfirmDialogButton extends StatelessWidget { this.textStyle, this.radius, this.padding, + this.child, this.onTapAction, }); @@ -50,7 +52,7 @@ class ConfirmDialogButton extends StatelessWidget { padding: padding ?? const EdgeInsets.symmetric(horizontal: 10), ), onPressed: onTapAction, - child: icon == null + child: child ?? (icon == null ? Text( label, style: textStyle ?? ThemeUtils.textStyleM3LabelLarge(color: textColor), @@ -79,7 +81,7 @@ class ConfirmDialogButton extends StatelessWidget { ), ), ], - ), + )), ); } } diff --git a/core/lib/presentation/views/dialog/modal_list_action_button_widget.dart b/core/lib/presentation/views/dialog/modal_list_action_button_widget.dart index 88a3b1353..f33d3c8e0 100644 --- a/core/lib/presentation/views/dialog/modal_list_action_button_widget.dart +++ b/core/lib/presentation/views/dialog/modal_list_action_button_widget.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/views/dialog/confirm_dialog_button.dart'; +import 'package:core/presentation/views/loading/cupertino_loading_widget.dart'; import 'package:flutter/material.dart'; class ModalListActionButtonWidget extends StatelessWidget { @@ -11,6 +12,7 @@ class ModalListActionButtonWidget extends StatelessWidget { final VoidCallback onPositiveAction; final EdgeInsetsGeometry? padding; final bool isPositiveActionEnabled; + final bool isProgressing; const ModalListActionButtonWidget({ super.key, @@ -19,6 +21,7 @@ class ModalListActionButtonWidget extends StatelessWidget { required this.onPositiveAction, required this.onNegativeAction, this.isPositiveActionEnabled = true, + this.isProgressing = false, this.padding, this.positiveKey, this.negativeKey, @@ -52,12 +55,25 @@ class ModalListActionButtonWidget extends StatelessWidget { ), ); + Widget progressingButton = const SizedBox( + height: 48, + width: 153, + child: ConfirmDialogButton( + label: '', + padding: EdgeInsets.zero, + backgroundColor: AppColor.primaryMain, + child: CupertinoLoadingWidget(color: Colors.white), + ), + ); + Widget bodyWidget = Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Flexible(child: negativeButton), const SizedBox(width: 8), - Flexible(child: positiveButton), + Flexible( + child: isProgressing ? progressingButton : positiveButton, + ), ], ); diff --git a/core/lib/presentation/views/loading/cupertino_loading_widget.dart b/core/lib/presentation/views/loading/cupertino_loading_widget.dart index 214783ece..0a8951f9d 100644 --- a/core/lib/presentation/views/loading/cupertino_loading_widget.dart +++ b/core/lib/presentation/views/loading/cupertino_loading_widget.dart @@ -8,6 +8,7 @@ class CupertinoLoadingWidget extends StatelessWidget { final EdgeInsetsGeometry? padding; final bool isCenter; final String? semanticLabel; + final Color? color; const CupertinoLoadingWidget({ super.key, @@ -15,6 +16,7 @@ class CupertinoLoadingWidget extends StatelessWidget { this.padding, this.isCenter = true, this.semanticLabel, + this.color, }); @override @@ -24,8 +26,8 @@ class CupertinoLoadingWidget extends StatelessWidget { child: SizedBox( width: size ?? CupertinoLoadingWidgetStyles.size, height: size ?? CupertinoLoadingWidgetStyles.size, - child: const CupertinoActivityIndicator( - color: CupertinoLoadingWidgetStyles.progressColor + child: CupertinoActivityIndicator( + color: color ?? CupertinoLoadingWidgetStyles.progressColor ) ) ) @@ -34,8 +36,8 @@ class CupertinoLoadingWidget extends StatelessWidget { child: SizedBox( width: size ?? CupertinoLoadingWidgetStyles.size, height: size ?? CupertinoLoadingWidgetStyles.size, - child: const CupertinoActivityIndicator( - color: CupertinoLoadingWidgetStyles.progressColor + child: CupertinoActivityIndicator( + color: color ?? CupertinoLoadingWidgetStyles.progressColor ) ), ); diff --git a/integration_test/scenarios/labels/display_folder_info_when_open_mail_from_tag_scenario.dart b/integration_test/scenarios/labels/display_folder_info_when_open_mail_from_tag_scenario.dart index 27894389b..49b38b83f 100644 --- a/integration_test/scenarios/labels/display_folder_info_when_open_mail_from_tag_scenario.dart +++ b/integration_test/scenarios/labels/display_folder_info_when_open_mail_from_tag_scenario.dart @@ -6,7 +6,7 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import '../../base/base_test_scenario.dart'; import '../../mixin/provisioning_label_scenario_mixin.dart'; -import '../../robots/label_robot.dart'; +import '../../robots/labels/label_robot.dart'; import '../../robots/thread_robot.dart'; class DisplayFolderInfoWhenOpenMailFromTagScenario extends BaseTestScenario diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index ecfb57db9..b45dd3c2b 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -61,7 +61,6 @@ class ComposerView extends GetWidget { MessageDialogActionManager().isDialogOpened || EmailActionReactor.isDialogOpened || ColorDialogPicker().isOpened.isTrue || - dialogRouter.isRuleFilterDialogOpened.isTrue || dialogRouter.isDialogOpened; if (isOverlayEnabled) { diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index b33c7b298..55e59152f 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -952,9 +952,15 @@ class SingleEmailController extends BaseController with AppLoaderMixin { pressEmailAction(actionType, presentationEmail); break; case EmailActionType.labelAs: - if (!isLabelAvailable) return; - mailboxDashBoardController - .openAddLabelToEmailDialogModal(presentationEmail); + mailboxDashBoardController.openAddLabelToEmailDialogModal( + email: presentationEmail, + onCreateANewLabelAction: () { + mailboxDashBoardController.labelController.onCreateALabelAction( + accountId: accountId, + shouldPop: true, + ); + }, + ); break; default: break; diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index df41e4981..59901183e 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -110,9 +110,14 @@ class EmailView extends GetWidget { openPopupMenu: controller.mailboxDashBoardController.openPopupMenuActionGroup, onSelectLabelAction: (label, isSelected) => controller.onToggleLabelAction( + emailId: presentationEmail.id, + label: label, + isSelected: isSelected, + ), + onCreateANewLabelAction: () => + controller.createNewLabelToEmail( + context, presentationEmail.id, - label, - isSelected, ), ); }, @@ -276,9 +281,9 @@ class EmailView extends GetWidget { isMobileResponsive: isMobileResponsive, labels: emailLabels, onDeleteLabelAction: (label) => controller.onToggleLabelAction( - presentationEmail.id, - label, - false, + emailId: presentationEmail.id, + label: label, + isSelected: false, ), ); }), @@ -321,9 +326,14 @@ class EmailView extends GetWidget { openPopupMenu: controller.mailboxDashBoardController.openPopupMenuActionGroup, onSelectLabelAction: (label, isSelected) => controller.onToggleLabelAction( + emailId: presentationEmail.id, + label: label, + isSelected: isSelected, + ), + onCreateANewLabelAction: () => + controller.createNewLabelToEmail( + context, presentationEmail.id, - label, - isSelected, ), ), onToggleThreadDetailCollapseExpand: onToggleThreadDetailCollapseExpand, @@ -601,7 +611,6 @@ class EmailView extends GetWidget { MessageDialogActionManager().isDialogOpened || EmailActionReactor.isDialogOpened || ColorDialogPicker().isOpened.isTrue || - dialogRouter.isRuleFilterDialogOpened.isTrue || dialogRouter.isDialogOpened; if (isOverlayEnabled) { diff --git a/lib/features/email/presentation/extensions/handle_label_for_email_extension.dart b/lib/features/email/presentation/extensions/handle_label_for_email_extension.dart index b3ebf9abd..f5bef26b0 100644 --- a/lib/features/email/presentation/extensions/handle_label_for_email_extension.dart +++ b/lib/features/email/presentation/extensions/handle_label_for_email_extension.dart @@ -1,10 +1,13 @@ import 'package:core/utils/app_logger.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:labels/model/label.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/extensions/email_loaded_extension.dart'; +import 'package:tmail_ui_user/features/labels/presentation/extensions/handle_label_action_type_extension.dart'; +import 'package:tmail_ui_user/features/labels/presentation/models/label_action_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/labels/handle_logic_label_extension.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/map_keywords_extension.dart'; import 'package:tmail_ui_user/features/thread/domain/extensions/presentation_email_map_extension.dart'; @@ -15,7 +18,11 @@ extension HandleLabelForEmailExtension on SingleEmailController { return mailboxDashBoardController.isLabelAvailable; } - void onToggleLabelAction(EmailId? emailId, Label label, bool isSelected) { + void onToggleLabelAction({ + required EmailId? emailId, + required Label label, + required bool isSelected, + }) { if (emailId == null) { logWarning('HandleLabelForEmailExtension::onToggleLabelAction: Email id is null'); return; @@ -125,4 +132,22 @@ extension HandleLabelForEmailExtension on SingleEmailController { remove: remove, ); } + + void createNewLabelToEmail(BuildContext context, EmailId? emailId) { + if (emailId == null) { + logWarning('HandleLabelForEmailExtension::createNewLabelToEmail: Email id is null'); + return; + } + + mailboxDashBoardController.labelController.handleLabelActionType( + actionType: LabelActionType.create, + accountId: accountId, + onLabelActionCallback: (label) => + onToggleLabelAction( + emailId: emailId, + label: label, + isSelected: true, + ), + ); + } } diff --git a/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart b/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart index 8eb8f7ac5..2067e33a2 100644 --- a/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart +++ b/lib/features/email/presentation/utils/email_action_reactor/email_action_reactor.dart @@ -54,6 +54,7 @@ import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_ import 'package:tmail_ui_user/features/email/presentation/widgets/email_address_dialog_builder.dart'; import 'package:tmail_ui_user/features/labels/presentation/mixin/label_sub_menu_mixin.dart'; import 'package:tmail_ui_user/features/labels/presentation/widgets/label_item_context_menu.dart'; +import 'package:tmail_ui_user/features/labels/presentation/widgets/label_list_context_menu.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_email_rule_filter_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart'; @@ -486,6 +487,7 @@ class EmailActionReactor with LabelSubMenuMixin { required bool isLabelAvailable, required OpenBottomSheetContextMenuAction openBottomSheetContextMenu, required OpenPopupMenuActionGroup openPopupMenu, + required OnCreateANewLabelAction onCreateANewLabelAction, List