TF-4178 Implement create new label

This commit is contained in:
dab246
2025-12-09 14:34:06 +07:00
committed by Dat H. Pham
parent 38c47ca373
commit 6c959fcf81
12 changed files with 163 additions and 39 deletions
@@ -8,6 +8,7 @@ class ModalListActionButtonWidget extends StatelessWidget {
final VoidCallback onNegativeAction;
final VoidCallback onPositiveAction;
final EdgeInsetsGeometry? padding;
final bool isPositiveActionEnabled;
const ModalListActionButtonWidget({
super.key,
@@ -15,6 +16,7 @@ class ModalListActionButtonWidget extends StatelessWidget {
required this.negativeLabel,
required this.onPositiveAction,
required this.onNegativeAction,
this.isPositiveActionEnabled = true,
this.padding,
});
@@ -34,9 +36,13 @@ class ModalListActionButtonWidget extends StatelessWidget {
height: 48,
child: ConfirmDialogButton(
label: positiveLabel,
backgroundColor: AppColor.primaryMain,
textColor: Colors.white,
onTapAction: onPositiveAction,
backgroundColor: isPositiveActionEnabled
? AppColor.primaryMain
: AppColor.profileMenuDivider.withValues(alpha: 0.12),
textColor: isPositiveActionEnabled
? Colors.white
: AppColor.m3SurfaceBackground.withValues(alpha: 0.38),
onTapAction: isPositiveActionEnabled ? onPositiveAction : null,
),
);
@@ -79,9 +79,13 @@ mixin HandleSetErrorMixin {
}
parseErrorForSetResponse(SetResponse? response, Id requestId) {
final mapError =
response?.notCreated ?? response?.notUpdated ?? response?.notDestroyed;
if (mapError != null && mapError.containsKey(requestId)) {
final mapError = <Id, SetError>{
...?response?.notCreated,
...?response?.notUpdated,
...?response?.notDestroyed,
};
if (mapError.containsKey(requestId)) {
final setError = mapError[requestId];
log('HandleSetErrorMixin::parseErrorForSetResponse():setError: $setError');
if (setError?.type == ErrorMethodResponse.invalidArguments) {
@@ -1,6 +1,8 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
@@ -9,11 +11,15 @@ import 'package:labels/model/label.dart';
import 'package:labels/utils/labels_constants.dart';
import 'package:model/mailbox/expand_mode.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/labels/domain/state/create_new_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/get_all_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/presentation/label_interactor_bindings.dart';
import 'package:tmail_ui_user/features/labels/presentation/widgets/create_new_label_modal.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';
class LabelController extends BaseController {
@@ -22,6 +28,7 @@ class LabelController extends BaseController {
final isCreateNewLabelModalVisible = RxBool(false);
GetAllLabelInteractor? _getAllLabelInteractor;
CreateNewLabelInteractor? _createNewLabelInteractor;
bool isLabelCapabilitySupported(Session session, AccountId accountId) {
return LabelsConstants.labelsCapability.isSupported(session, accountId);
@@ -30,6 +37,7 @@ class LabelController extends BaseController {
void injectLabelsBindings() {
LabelInteractorBindings().dependencies();
_getAllLabelInteractor = getBinding<GetAllLabelInteractor>();
_createNewLabelInteractor = getBinding<CreateNewLabelInteractor>();
}
void getAllLabels(AccountId accountId) {
@@ -44,7 +52,7 @@ class LabelController extends BaseController {
: ExpandMode.COLLAPSE;
}
Future<void> openCreateNewLabelModal() async {
Future<void> openCreateNewLabelModal(AccountId? accountId) async {
if (PlatformInfo.isWeb) {
isCreateNewLabelModalVisible.value = true;
}
@@ -52,7 +60,10 @@ class LabelController extends BaseController {
await Get.generalDialog(
barrierDismissible: true,
barrierLabel: 'create-new-label-modal',
pageBuilder: (_, __, ___) => CreateNewLabelModal(labels: labels),
pageBuilder: (_, __, ___) => CreateNewLabelModal(
labels: labels,
onCreateNewLabelCallback: (label) => _createNewLabel(accountId, label),
),
).whenComplete(() {
if (PlatformInfo.isWeb) {
isCreateNewLabelModalVisible.value = false;
@@ -60,10 +71,41 @@ class LabelController extends BaseController {
});
}
void _createNewLabel(AccountId? accountId, Label label) {
log('LabelController::_createNewLabel:Label: $label');
if (accountId == null) {
consumeState(
Stream.value(Left(CreateNewLabelFailure(NotFoundAccountIdException()))),
);
} else if (_createNewLabelInteractor == null) {
consumeState(
Stream.value(Left(CreateNewLabelFailure(InteractorNotInitialized()))),
);
} else {
consumeState(_createNewLabelInteractor!.execute(accountId, label));
}
}
void _handleCreateNewLabelSuccess(CreateNewLabelSuccess success) {
toastManager.showMessageSuccess(success);
_addLabelToList(success.newLabel);
}
void _handleCreateNewLabelFailure(CreateNewLabelFailure failure) {
toastManager.showMessageFailure(failure);
}
void _addLabelToList(Label newLabel) {
labels.add(newLabel);
labels.sortByAlphabetically();
}
@override
void handleSuccessViewState(Success success) {
if (success is GetAllLabelSuccess) {
labels.value = success.labels..sortByAlphabetically();
} else if (success is CreateNewLabelSuccess) {
_handleCreateNewLabelSuccess(success);
} else {
super.handleSuccessViewState(success);
}
@@ -73,6 +115,8 @@ class LabelController extends BaseController {
void handleFailureViewState(Failure failure) {
if (failure is GetAllLabelFailure) {
labels.value = [];
} else if (failure is CreateNewLabelFailure) {
_handleCreateNewLabelFailure(failure);
} else {
super.handleFailureViewState(failure);
}
@@ -81,6 +125,7 @@ class LabelController extends BaseController {
@override
void onClose() {
_getAllLabelInteractor = null;
_createNewLabelInteractor = null;
super.onClose();
}
}
@@ -6,6 +6,7 @@ import 'package:tmail_ui_user/features/labels/data/datasource_impl/label_datasou
import 'package:tmail_ui_user/features/labels/data/network/label_api.dart';
import 'package:tmail_ui_user/features/labels/data/repository/label_repository_impl.dart';
import 'package:tmail_ui_user/features/labels/domain/repository/label_repository.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/create_new_label_interactor.dart';
import 'package:tmail_ui_user/features/labels/domain/usecases/get_all_label_interactor.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart';
import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart';
@@ -31,6 +32,7 @@ class LabelInteractorBindings extends InteractorsBindings {
@override
void bindingsInteractor() {
Get.lazyPut(() => GetAllLabelInteractor(Get.find<LabelRepository>()));
Get.lazyPut(() => CreateNewLabelInteractor(Get.find<LabelRepository>()));
Get.lazyPut(() => VerifyNameInteractor());
}
@@ -11,13 +11,11 @@ import 'package:core/presentation/views/dialog/modal_list_action_button_widget.d
import 'package:core/utils/platform_info.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:labels/extensions/list_label_extension.dart';
import 'package:labels/model/label.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/mailbox_creator/domain/model/verification/duplicate_name_validator.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/empty_name_validator.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/name_with_space_only_validator.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/special_character_validator.dart';
import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/validator.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';
@@ -25,10 +23,17 @@ import 'package:tmail_ui_user/features/mailbox_creator/presentation/extensions/v
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
typedef OnCreateNewLabelCallback = Function(Label label);
class CreateNewLabelModal extends StatefulWidget {
final List<Label> labels;
final OnCreateNewLabelCallback onCreateNewLabelCallback;
const CreateNewLabelModal({super.key, required this.labels});
const CreateNewLabelModal({
super.key,
required this.labels,
required this.onCreateNewLabelCallback,
});
@override
State<CreateNewLabelModal> createState() => _CreateNewLabelModalState();
@@ -41,6 +46,7 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
final ValueNotifier<String?> _labelNameErrorTextNotifier =
ValueNotifier(null);
final ValueNotifier<Color?> _labelSelectedColorNotifier = ValueNotifier(null);
final ValueNotifier<bool> _createLabelStateNotifier = ValueNotifier(false);
final TextEditingController _nameInputController = TextEditingController();
final FocusNode _nameInputFocusNode = FocusNode();
@@ -203,12 +209,20 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
),
),
),
ModalListActionButtonWidget(
positiveLabel: appLocalizations.createLabel,
negativeLabel: appLocalizations.cancel,
padding: const EdgeInsets.symmetric(vertical: 25),
onPositiveAction: _onCreateNewLabel,
onNegativeAction: _onCloseModal,
ValueListenableBuilder(
valueListenable: _createLabelStateNotifier,
builder: (_, value, __) {
return ModalListActionButtonWidget(
positiveLabel: appLocalizations.createLabel,
negativeLabel: appLocalizations.cancel,
padding: const EdgeInsets.symmetric(
vertical: 25,
),
isPositiveActionEnabled: value,
onPositiveAction: _onCreateNewLabel,
onNegativeAction: _onCloseModal,
);
},
),
],
),
@@ -248,10 +262,9 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
AppLocalizations appLocalizations,
String value,
) {
_labelNameErrorTextNotifier.value = _verifyLabelName(
appLocalizations,
value,
);
final errorText = _verifyLabelName(appLocalizations, value);
_labelNameErrorTextNotifier.value = errorText;
_createLabelStateNotifier.value = errorText == null;
}
String? _verifyLabelName(AppLocalizations appLocalizations, String value) {
@@ -272,7 +285,6 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
EmptyNameValidator(),
NameWithSpaceOnlyValidator(),
DuplicateNameValidator(_labelDisplayNameList),
SpecialCharacterValidator(),
];
void _clearInputFocus() {
@@ -281,6 +293,15 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
void _onCreateNewLabel() {
_clearInputFocus();
final newLabel = Label(
displayName: _nameInputController.text,
color: _selectedColor != null
? HexColor(_selectedColor!.toHexTriplet())
: null,
);
widget.onCreateNewLabelCallback(newLabel);
popBack();
}
@@ -318,6 +339,7 @@ class _CreateNewLabelModalState extends State<CreateNewLabelModal> {
_nameInputController.dispose();
_labelNameErrorTextNotifier.dispose();
_labelSelectedColorNotifier.dispose();
_createLabelStateNotifier.dispose();
_labelDisplayNameList = [];
super.dispose();
}
@@ -350,6 +350,7 @@ abstract class BaseMailboxView extends GetWidget<MailboxController>
final labelController =
controller.mailboxDashBoardController.labelController;
final accountId = controller.accountId;
final labelListExpandMode = labelController.labelListExpandMode.value;
final countLabels = labelController.labels.length;
@@ -364,7 +365,8 @@ abstract class BaseMailboxView extends GetWidget<MailboxController>
expandMode: labelListExpandMode,
countLabels: countLabels,
onToggleLabelListState: labelController.toggleLabelListState,
onAddNewLabel: labelController.openCreateNewLabelModal,
onAddNewLabel: () =>
labelController.openCreateNewLabelModal(accountId),
);
} else {
return const SizedBox.shrink();
@@ -54,8 +54,6 @@ extension ValicatorFailureExtension on VerifyNameFailure {
return appLocalizations.this_field_cannot_be_blank;
} else if (exception is DuplicatedNameException) {
return appLocalizations.tagNameAlreadyExists;
} else if (exception is SpecialCharacterException) {
return appLocalizations.tagNameCannotContainSpecialCharacters;
} else if (exception is NameWithSpaceOnlyException) {
return appLocalizations.thisFieldCannotContainOnlySpaces;
} else {
+16 -6
View File
@@ -5232,12 +5232,6 @@
"placeholders_order": [],
"placeholders": {}
},
"tagNameCannotContainSpecialCharacters": "Tag name cannot contain special characters",
"@tagNameCannotContainSpecialCharacters": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"tagNameAlreadyExists": "A tag with this name already exists. Please choose a different name.",
"@tagNameAlreadyExists": {
"type": "text",
@@ -5255,5 +5249,21 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"createLabelSuccessfullyMessage": "You successfully created {labelName} label",
"@createLabelSuccessfullyMessage": {
"type": "text",
"placeholders_order": [
"labelName"
],
"placeholders": {
"labelName": {}
}
},
"createNewLabelFailure": "Create new label failure",
"@createNewLabelFailure": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
+1
View File
@@ -0,0 +1 @@
class InteractorNotInitialized implements Exception {}
+15 -7
View File
@@ -5556,13 +5556,6 @@ class AppLocalizations {
);
}
String get tagNameCannotContainSpecialCharacters {
return Intl.message(
'Tag name cannot contain special characters',
name: 'tagNameCannotContainSpecialCharacters',
);
}
String get tagNameAlreadyExists {
return Intl.message(
'A tag with this name already exists. Please choose a different name.',
@@ -5583,4 +5576,19 @@ class AppLocalizations {
name: 'chooseAColourForThisLabel',
);
}
String createLabelSuccessfullyMessage(String labelName) {
return Intl.message(
'You successfully created $labelName label',
name: 'createLabelSuccessfullyMessage',
args: [labelName],
);
}
String get createNewLabelFailure {
return Intl.message(
'Create new label failure',
name: 'createNewLabelFailure',
);
}
}
+9
View File
@@ -15,6 +15,7 @@ import 'package:flutter_svg/svg.dart';
import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart';
import 'package:jmap_dart_client/jmap/core/error/method/exception/error_method_response_exception.dart';
import 'package:jmap_dart_client/jmap/core/error/set_error.dart';
import 'package:labels/extensions/label_extension.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/email/mark_star_action.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
@@ -27,6 +28,7 @@ import 'package:tmail_ui_user/features/download/domain/state/parse_email_by_blob
import 'package:tmail_ui_user/features/download/domain/state/preview_email_from_eml_file_state.dart';
import 'package:tmail_ui_user/features/home/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart';
import 'package:tmail_ui_user/features/labels/domain/state/create_new_label_state.dart';
import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart';
import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart';
import 'package:tmail_ui_user/features/login/domain/exceptions/oauth_authorization_error.dart';
@@ -196,6 +198,9 @@ class ToastManager {
} else if (failure is MoveFolderContentFailure) {
message = message ??
appLocalizations.moveFolderContentToastMessage;
} else if (failure is CreateNewLabelFailure) {
message = message ??
appLocalizations.createNewLabelFailure;
}
log('ToastManager::showMessageFailure: Message: $message');
if (message?.trim().isNotEmpty == true) {
@@ -259,6 +264,10 @@ class ToastManager {
message = success.markStarAction == MarkStarAction.markStar
? appLocalizations.mailHasBeenStarred
: appLocalizations.mailHasBeenUnstarred;
} else if (success is CreateNewLabelSuccess) {
message = appLocalizations.createLabelSuccessfullyMessage(
success.newLabel.safeDisplayName,
);
}
log('ToastManager::showMessageSuccess: Message: $message');
if (message?.trim().isNotEmpty == true) {
@@ -17,11 +17,13 @@ import 'package:jmap_dart_client/jmap/identities/identity.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:labels/model/label.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:model/email/attachment.dart';
import 'package:model/email/email_action_type.dart';
import 'package:model/extensions/session_extension.dart';
import 'package:model/mailbox/expand_mode.dart';
import 'package:model/mailbox/presentation_mailbox.dart';
import 'package:rich_text_composer/rich_text_composer.dart';
import 'package:tmail_ui_user/features/base/before_reconnect_manager.dart';
@@ -46,6 +48,7 @@ import 'package:tmail_ui_user/features/email/domain/usecases/print_email_interac
import 'package:tmail_ui_user/features/email/domain/usecases/save_template_email_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
import 'package:tmail_ui_user/features/labels/presentation/label_controller.dart';
import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart';
@@ -96,6 +99,17 @@ class MockRichTextWebController extends Mock implements RichTextWebController {
bool get codeViewEnabled => false;
}
class MockLabelController extends Mock implements LabelController {
@override
RxList<Label> get labels => RxList([]);
@override
Rx<ExpandMode> get labelListExpandMode => Rx(ExpandMode.EXPAND);
@override
RxBool get isCreateNewLabelModalVisible => RxBool(false);
}
class MockMailboxDashBoardController extends Mock implements MailboxDashBoardController {
@override
InternalFinalCallback<void> get onStart => mockControllerCallback();
@@ -151,6 +165,9 @@ class MockMailboxDashBoardController extends Mock implements MailboxDashBoardCon
bool isAIScribeEndpointAvailable({Session? session, AccountId? accountId}) {
return false;
}
@override
LabelController get labelController => MockLabelController();
}
@GenerateNiceMocks([