TF-4383 Handle read only labels

This commit is contained in:
dab246
2026-03-25 18:36:10 +07:00
committed by Dat H. Pham
parent c17782bfd4
commit eb017b54b1
10 changed files with 97 additions and 7 deletions
@@ -29,12 +29,15 @@ extension LabelExtension on Label {
} }
} }
bool get isReadOnly => readOnly == true;
Label copyWith({ Label copyWith({
Id? id, Id? id,
KeyWordIdentifier? keyword, KeyWordIdentifier? keyword,
String? displayName, String? displayName,
HexColor? color, HexColor? color,
String? description, String? description,
bool? readOnly,
}) { }) {
return Label( return Label(
id: id ?? this.id, id: id ?? this.id,
@@ -42,6 +45,7 @@ extension LabelExtension on Label {
keyword: keyword ?? this.keyword, keyword: keyword ?? this.keyword,
color: color ?? this.color, color: color ?? this.color,
description: description ?? this.description, description: description ?? this.description,
readOnly: readOnly ?? this.readOnly,
); );
} }
+1
View File
@@ -1,5 +1,6 @@
export 'package:labels/model/label.dart'; export 'package:labels/model/label.dart';
export 'package:labels/model/hex_color.dart'; export 'package:labels/model/hex_color.dart';
export 'package:labels/model/labels_capability.dart';
export 'package:labels/utils/labels_constants.dart'; export 'package:labels/utils/labels_constants.dart';
+3
View File
@@ -24,6 +24,7 @@ class Label with EquatableMixin {
final String? displayName; final String? displayName;
final HexColor? color; final HexColor? color;
final String? description; final String? description;
final bool? readOnly;
Label({ Label({
this.id, this.id,
@@ -31,6 +32,7 @@ class Label with EquatableMixin {
this.displayName, this.displayName,
this.color, this.color,
this.description, this.description,
this.readOnly,
}); });
factory Label.fromJson(Map<String, dynamic> json) => _$LabelFromJson(json); factory Label.fromJson(Map<String, dynamic> json) => _$LabelFromJson(json);
@@ -44,5 +46,6 @@ class Label with EquatableMixin {
displayName, displayName,
color, color,
description, description,
readOnly,
]; ];
} }
+23
View File
@@ -0,0 +1,23 @@
import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart';
import 'package:json_annotation/json_annotation.dart';
part 'labels_capability.g.dart';
@JsonSerializable(explicitToJson: true, includeIfNull: false)
class LabelsCapability extends CapabilityProperties {
final int? version;
LabelsCapability({this.version});
factory LabelsCapability.fromJson(Map<String, dynamic> json) =>
_$LabelsCapabilityFromJson(json);
Map<String, dynamic> toJson() => _$LabelsCapabilityToJson(this);
static LabelsCapability deserialize(Map<String, dynamic> json) {
return LabelsCapability.fromJson(json);
}
@override
List<Object?> get props => [version];
}
@@ -10,6 +10,7 @@ import 'package:jmap_dart_client/jmap/core/capability/submission_capability.dart
import 'package:jmap_dart_client/jmap/core/capability/vacation_capability.dart'; import 'package:jmap_dart_client/jmap/core/capability/vacation_capability.dart';
import 'package:jmap_dart_client/jmap/core/capability/web_socket_ticket_capability.dart'; import 'package:jmap_dart_client/jmap/core/capability/web_socket_ticket_capability.dart';
import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart'; import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart';
import 'package:labels/model/labels_capability.dart';
import 'package:model/saas/saas_account_capability.dart'; import 'package:model/saas/saas_account_capability.dart';
import 'package:model/support/contact_support_capability.dart'; import 'package:model/support/contact_support_capability.dart';
import 'package:scribe/scribe/ai/presentation/model/ai_capability.dart'; import 'package:scribe/scribe/ai/presentation/model/ai_capability.dart';
@@ -41,6 +42,8 @@ class CapabilityPropertiesConverter {
return properties.toJson(); return properties.toJson();
} else if (properties is AICapability) { } else if (properties is AICapability) {
return properties.toJson(); return properties.toJson();
} else if (properties is LabelsCapability) {
return properties.toJson();
} else if (properties is DefaultCapability) { } else if (properties is DefaultCapability) {
return properties.properties; return properties.properties;
} else if (properties is EmptyCapability) { } else if (properties is EmptyCapability) {
@@ -12,6 +12,8 @@ import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart
import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart';
import 'package:labels/model/labels_capability.dart';
import 'package:labels/utils/labels_constants.dart';
import 'package:model/download_all/download_all_capability.dart'; import 'package:model/download_all/download_all_capability.dart';
import 'package:model/mailbox/mailbox_constants.dart'; import 'package:model/mailbox/mailbox_constants.dart';
import 'package:model/model.dart'; import 'package:model/model.dart';
@@ -36,6 +38,7 @@ extension SessionExtensions on Session {
capabilityServerSettings: SettingsCapability.deserialize, capabilityServerSettings: SettingsCapability.deserialize,
linagoraSaaSCapability: SaaSAccountCapability.deserialize, linagoraSaaSCapability: SaaSAccountCapability.deserialize,
AiScribeConstants.aiCapability: AICapability.fromJson, AiScribeConstants.aiCapability: AICapability.fromJson,
LabelsConstants.labelsCapability: LabelsCapability.fromJson,
}; };
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@@ -199,4 +202,22 @@ extension SessionExtensions on Session {
return null; return null;
} }
} }
LabelsCapability? getLabelsCapability(AccountId accountId) {
try {
if (!LabelsConstants.labelsCapability.isSupported(this, accountId)) {
return null;
}
final labelsCapability = getCapabilityProperties<LabelsCapability>(
accountId,
LabelsConstants.labelsCapability,
);
log('SessionExtensions::getLabelsCapability:labelsCapability = $labelsCapability');
return labelsCapability;
} catch (e) {
logWarning('SessionExtensions::getLabelsCapability():[Exception] $e');
return null;
}
}
} }
@@ -7,19 +7,18 @@ 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:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/state.dart';
import 'package:labels/extensions/list_label_extension.dart'; import 'package:labels/labels.dart';
import 'package:labels/model/label.dart';
import 'package:labels/utils/labels_constants.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/data/exceptions/session_exceptions.dart';
import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.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/edit_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/edit_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/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/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';
@@ -53,6 +52,7 @@ class LabelController extends BaseController with LabelContextMenuMixin {
State? _currentLabelState; State? _currentLabelState;
AccountId? _accountId; AccountId? _accountId;
Session? _session; Session? _session;
LabelsCapability? _labelsCapability;
@override @override
void onInit() { void onInit() {
@@ -64,9 +64,17 @@ class LabelController extends BaseController with LabelContextMenuMixin {
return LabelsConstants.labelsCapability.isSupported(session, accountId); return LabelsConstants.labelsCapability.isSupported(session, accountId);
} }
LabelsCapability? get labelsCapability => _labelsCapability;
bool get shouldAskReadOnly {
final labelVersion = labelsCapability?.version;
return labelVersion != null && labelVersion >= 1;
}
void checkLabelSettingState(Session session, AccountId accountId) { void checkLabelSettingState(Session session, AccountId accountId) {
_session = session; _session = session;
_accountId = accountId; _accountId = accountId;
_labelsCapability = _session?.getLabelsCapability(accountId);
_getLabelSettingStateInteractor = _getLabelSettingStateInteractor =
getBinding<GetLabelSettingStateInteractor>(); getBinding<GetLabelSettingStateInteractor>();
if (_getLabelSettingStateInteractor != null) { if (_getLabelSettingStateInteractor != null) {
@@ -245,6 +253,7 @@ class LabelController extends BaseController with LabelContextMenuMixin {
_currentLabelState = null; _currentLabelState = null;
_accountId = null; _accountId = null;
_session = null; _session = null;
_labelsCapability = null;
super.onClose(); super.onClose();
} }
} }
@@ -413,6 +413,7 @@ abstract class BaseMailboxView extends GetWidget<MailboxController>
if (isLabelAvailable) { if (isLabelAvailable) {
final labelListExpandMode = labelController.labelListExpandMode.value; final labelListExpandMode = labelController.labelListExpandMode.value;
final labels = labelController.labels; final labels = labelController.labels;
final shouldAskReadOnly = labelController.shouldAskReadOnly;
return AnimatedContainer( return AnimatedContainer(
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
@@ -421,6 +422,7 @@ abstract class BaseMailboxView extends GetWidget<MailboxController>
labels: labels, labels: labels,
imagePaths: controller.imagePaths, imagePaths: controller.imagePaths,
isDesktop: isDesktop, isDesktop: isDesktop,
shouldAskReadOnly: shouldAskReadOnly,
labelIdSelected: labelIdSelected, labelIdSelected: labelIdSelected,
isMobileResponsive: isMobileResponsive, isMobileResponsive: isMobileResponsive,
onOpenLabelCallback: (label) => controller.openMailbox( onOpenLabelCallback: (label) => controller.openMailbox(
@@ -16,6 +16,7 @@ class LabelListItem extends StatefulWidget {
final ImagePaths imagePaths; final ImagePaths imagePaths;
final bool isDesktop; final bool isDesktop;
final bool isSelected; final bool isSelected;
final bool shouldAskReadOnly;
final bool isMobileResponsive; final bool isMobileResponsive;
final OnOpenLabelCallback onOpenLabelCallback; final OnOpenLabelCallback onOpenLabelCallback;
final OnOpenLabelContextMenuAction? onOpenContextMenu; final OnOpenLabelContextMenuAction? onOpenContextMenu;
@@ -28,6 +29,7 @@ class LabelListItem extends StatefulWidget {
required this.onOpenLabelCallback, required this.onOpenLabelCallback,
this.isDesktop = false, this.isDesktop = false,
this.isSelected = false, this.isSelected = false,
this.shouldAskReadOnly = false,
this.isMobileResponsive = false, this.isMobileResponsive = false,
this.onOpenContextMenu, this.onOpenContextMenu,
this.onLongPressLabelItemAction, this.onLongPressLabelItemAction,
@@ -75,7 +77,26 @@ class _LabelListItemState extends State<LabelListItem> {
: MailboxItemWidgetStyles.mobileLabelIconSpace; : MailboxItemWidgetStyles.mobileLabelIconSpace;
} }
bool get _isMenuButtonVisible => _isContextMenuVisible || _isItemHovered; bool get _isMenuButtonVisible {
final isActive = _isContextMenuVisible || _isItemHovered;
if (!widget.shouldAskReadOnly) {
return isActive;
}
return isActive && !widget.label.isReadOnly;
}
bool get _isOnLongPressActive {
final isTouchPlatform =
PlatformInfo.isWebTouchDevice || PlatformInfo.isMobile;
if (!widget.shouldAskReadOnly) {
return isTouchPlatform;
}
return isTouchPlatform && !widget.label.isReadOnly;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -85,7 +106,7 @@ class _LabelListItemState extends State<LabelListItem> {
borderRadius: _borderRadius, borderRadius: _borderRadius,
onHover: _handleHoverChanged, onHover: _handleHoverChanged,
onTap: () => widget.onOpenLabelCallback(widget.label), onTap: () => widget.onOpenLabelCallback(widget.label),
onLongPress: PlatformInfo.isWebTouchDevice || PlatformInfo.isMobile onLongPress: _isOnLongPressActive
? () => widget.onLongPressLabelItemAction?.call(widget.label) ? () => widget.onLongPressLabelItemAction?.call(widget.label)
: null, : null,
child: Container( child: Container(
@@ -9,6 +9,7 @@ class LabelListView extends StatelessWidget {
final List<Label> labels; final List<Label> labels;
final ImagePaths imagePaths; final ImagePaths imagePaths;
final bool isDesktop; final bool isDesktop;
final bool shouldAskReadOnly;
final Id? labelIdSelected; final Id? labelIdSelected;
final OnOpenLabelCallback onOpenLabelCallback; final OnOpenLabelCallback onOpenLabelCallback;
final bool isMobileResponsive; final bool isMobileResponsive;
@@ -21,6 +22,7 @@ class LabelListView extends StatelessWidget {
required this.imagePaths, required this.imagePaths,
required this.onOpenLabelCallback, required this.onOpenLabelCallback,
this.isDesktop = false, this.isDesktop = false,
this.shouldAskReadOnly = false,
this.labelIdSelected, this.labelIdSelected,
this.isMobileResponsive = false, this.isMobileResponsive = false,
this.onOpenContextMenu, this.onOpenContextMenu,
@@ -42,6 +44,7 @@ class LabelListView extends StatelessWidget {
imagePaths: imagePaths, imagePaths: imagePaths,
isSelected: label.id == labelIdSelected, isSelected: label.id == labelIdSelected,
isDesktop: isDesktop, isDesktop: isDesktop,
shouldAskReadOnly: shouldAskReadOnly,
onOpenLabelCallback: onOpenLabelCallback, onOpenLabelCallback: onOpenLabelCallback,
isMobileResponsive: isMobileResponsive, isMobileResponsive: isMobileResponsive,
onOpenContextMenu: onOpenContextMenu, onOpenContextMenu: onOpenContextMenu,