TF-4336 Fix INBOX should be above Favorite folder (#4340)
This commit is contained in:
@@ -253,6 +253,10 @@ class MailboxController extends BaseMailboxController
|
||||
dashboardController: mailboxDashBoardController,
|
||||
toastManager: toastManager,
|
||||
);
|
||||
} else if (failure is CreateDefaultMailboxFailure) {
|
||||
autoCreateVirtualFolder(
|
||||
mailboxDashBoardController.isAINeedsActionEnabled,
|
||||
);
|
||||
} else {
|
||||
super.handleFailureViewState(failure);
|
||||
}
|
||||
@@ -273,9 +277,6 @@ class MailboxController extends BaseMailboxController
|
||||
},
|
||||
(success) {
|
||||
if (success is GetAllMailboxSuccess) {
|
||||
autoCreateVirtualFolder(
|
||||
mailboxDashBoardController.isAINeedsActionEnabled,
|
||||
);
|
||||
mailboxDashBoardController.updateRefreshAllMailboxState(Right(RefreshAllMailboxSuccess()));
|
||||
_handleCreateDefaultFolderIfMissing(mailboxDashBoardController.mapDefaultMailboxIdByRole);
|
||||
_handleDataFromNavigationRouter();
|
||||
@@ -283,10 +284,6 @@ class MailboxController extends BaseMailboxController
|
||||
if (PlatformInfo.isIOS) {
|
||||
_updateMailboxIdsBlockNotificationToKeychain(success.mailboxList);
|
||||
}
|
||||
} else if (success is CreateDefaultMailboxAllSuccess) {
|
||||
autoCreateVirtualFolder(
|
||||
mailboxDashBoardController.isAINeedsActionEnabled,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -614,7 +611,7 @@ class MailboxController extends BaseMailboxController
|
||||
.mailboxList
|
||||
.listSubscribedMailboxesAndDefaultMailboxes;
|
||||
|
||||
await refreshTree(listMailboxDisplayed);
|
||||
await refreshTree(listMailboxDisplayed.withoutVirtualMailbox);
|
||||
|
||||
if (currentContext != null) {
|
||||
syncAllMailboxWithDisplayName(currentContext!);
|
||||
@@ -705,22 +702,32 @@ class MailboxController extends BaseMailboxController
|
||||
.whereNot((role) => mapDefaultMailboxRole.containsKey(role) || findNodeByNameOnFirstLevel(role.value) != null)
|
||||
.toList();
|
||||
|
||||
if (listRoleMissing.isEmpty || accountId == null || session == null) {
|
||||
autoCreateVirtualFolder(
|
||||
mailboxDashBoardController.isAINeedsActionEnabled,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final mapRoles = {
|
||||
for (var role in listRoleMissing)
|
||||
Id(uuid.v1()) : role
|
||||
};
|
||||
log('MailboxController::_handleCreateDefaultFolderIfMissing():mapRoles: $mapRoles');
|
||||
if (mapRoles.isNotEmpty && accountId != null && session != null) {
|
||||
consumeState(_createDefaultMailboxInteractor.execute(
|
||||
session!,
|
||||
accountId!,
|
||||
mapRoles,
|
||||
));
|
||||
}
|
||||
consumeState(_createDefaultMailboxInteractor.execute(
|
||||
session!,
|
||||
accountId!,
|
||||
mapRoles,
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _handleCreateDefaultFolderIfMissingSuccess(CreateDefaultMailboxAllSuccess success) async {
|
||||
if (success.listMailbox.isEmpty) return;
|
||||
if (success.listMailbox.isEmpty) {
|
||||
autoCreateVirtualFolder(
|
||||
mailboxDashBoardController.isAINeedsActionEnabled,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Set<Role?> existingRoles = {};
|
||||
Set<MailboxName> existingNamesWithoutParent = {};
|
||||
@@ -733,10 +740,13 @@ class MailboxController extends BaseMailboxController
|
||||
allMailboxes.add(mailbox.toPresentationMailbox());
|
||||
}
|
||||
|
||||
await buildTree(allMailboxes);
|
||||
await buildTree(allMailboxes.withoutVirtualMailbox);
|
||||
if (currentContext != null) {
|
||||
syncAllMailboxWithDisplayName(currentContext!);
|
||||
}
|
||||
autoCreateVirtualFolder(
|
||||
mailboxDashBoardController.isAINeedsActionEnabled,
|
||||
);
|
||||
_setMapMailbox();
|
||||
_setOutboxMailbox();
|
||||
}
|
||||
@@ -1320,7 +1330,7 @@ class MailboxController extends BaseMailboxController
|
||||
currentMailboxState = success.currentMailboxState;
|
||||
log('MailboxController::_handleGetAllMailboxSuccess:currentMailboxState: $currentMailboxState');
|
||||
final listMailboxDisplayed = success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes;
|
||||
await buildTree(listMailboxDisplayed);
|
||||
await buildTree(listMailboxDisplayed.withoutVirtualMailbox);
|
||||
if (currentContext != null) {
|
||||
syncAllMailboxWithDisplayName(currentContext!);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jmap_dart_client/jmap/core/id.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/mailbox/namespace.dart';
|
||||
import 'package:model/extensions/list_presentation_mailbox_extension.dart';
|
||||
import 'package:model/extensions/presentation_mailbox_extension.dart';
|
||||
import 'package:model/mailbox/expand_mode.dart';
|
||||
import 'package:model/mailbox/mailbox_state.dart';
|
||||
import 'package:model/mailbox/presentation_mailbox.dart';
|
||||
import 'package:model/mailbox/select_mode.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart';
|
||||
@@ -261,4 +267,341 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Virtual Folders Exclusion & Sorting Impact', () {
|
||||
late List<PresentationMailbox> filteredMailboxes;
|
||||
final virtualStarred = PresentationMailbox.favoriteFolder;
|
||||
final virtualActionRequired = PresentationMailbox.actionRequiredFolder;
|
||||
|
||||
setUp(() {
|
||||
final allMailboxesIncludingVirtual = [
|
||||
PresentationMailbox(
|
||||
MailboxId(Id("default_root")),
|
||||
parentId: null,
|
||||
name: MailboxName('Inbox'),
|
||||
role: Role('inbox'),
|
||||
sortOrder: SortOrder(sortValue: 10),
|
||||
),
|
||||
PresentationMailbox(
|
||||
MailboxId(Id("default_child")),
|
||||
parentId: MailboxId(Id("default_root")),
|
||||
name: MailboxName('Sub Inbox'),
|
||||
),
|
||||
PresentationMailbox(
|
||||
MailboxId(Id("personal_root")),
|
||||
parentId: null,
|
||||
name: MailboxName('My Folder'),
|
||||
namespace: Namespace('Personal'),
|
||||
),
|
||||
PresentationMailbox(
|
||||
MailboxId(Id("team_root")),
|
||||
parentId: null,
|
||||
name: MailboxName('Team Folder'),
|
||||
namespace: Namespace('Team'),
|
||||
),
|
||||
virtualStarred,
|
||||
virtualActionRequired,
|
||||
];
|
||||
|
||||
filteredMailboxes = allMailboxesIncludingVirtual.withoutVirtualMailbox;
|
||||
});
|
||||
|
||||
void expectNoVirtualFoldersRecursively(List<MailboxNode>? nodes) {
|
||||
if (nodes == null || nodes.isEmpty) return;
|
||||
for (final node in nodes) {
|
||||
expect(
|
||||
node.item.isVirtualFolder,
|
||||
isFalse,
|
||||
reason: 'Found Virtual Folder: ${node.item.name?.name}',
|
||||
);
|
||||
expectNoVirtualFoldersRecursively(node.childrenItems);
|
||||
}
|
||||
}
|
||||
|
||||
test(
|
||||
'generateMailboxTreeInUI: Trees and nested sub-trees must exclude virtual folders',
|
||||
() async {
|
||||
final generatedTrees = await TreeBuilder().generateMailboxTreeInUI(
|
||||
allMailboxes: filteredMailboxes,
|
||||
currentDefaultTree: MailboxTree(MailboxNode.root()),
|
||||
currentPersonalTree: MailboxTree(MailboxNode.root()),
|
||||
currentTeamMailboxTree: MailboxTree(MailboxNode.root()),
|
||||
);
|
||||
|
||||
expectNoVirtualFoldersRecursively(
|
||||
generatedTrees.defaultTree.root.childrenItems,
|
||||
);
|
||||
expectNoVirtualFoldersRecursively(
|
||||
generatedTrees.personalTree.root.childrenItems,
|
||||
);
|
||||
expectNoVirtualFoldersRecursively(
|
||||
generatedTrees.teamMailboxTree.root.childrenItems,
|
||||
);
|
||||
expect(
|
||||
generatedTrees.allMailboxes,
|
||||
everyElement(
|
||||
isNot(predicate<PresentationMailbox>((m) => m.isVirtualFolder))),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'generateMailboxTreeInUIAfterRefreshChanges: Trees and nested sub-trees must exclude virtual folders',
|
||||
() async {
|
||||
final generatedTrees =
|
||||
await TreeBuilder().generateMailboxTreeInUIAfterRefreshChanges(
|
||||
allMailboxes: filteredMailboxes,
|
||||
currentDefaultTree: MailboxTree(MailboxNode.root()),
|
||||
currentPersonalTree: MailboxTree(MailboxNode.root()),
|
||||
currentTeamMailboxTree: MailboxTree(MailboxNode.root()),
|
||||
);
|
||||
|
||||
expectNoVirtualFoldersRecursively(
|
||||
generatedTrees.defaultTree.root.childrenItems,
|
||||
);
|
||||
expectNoVirtualFoldersRecursively(
|
||||
generatedTrees.personalTree.root.childrenItems,
|
||||
);
|
||||
expectNoVirtualFoldersRecursively(
|
||||
generatedTrees.teamMailboxTree.root.childrenItems,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'Inbox is first when virtual folders are excluded via withoutVirtualMailbox before generateMailboxTreeInUI',
|
||||
() async {
|
||||
final allMailboxesIncludingVirtual = [
|
||||
PresentationMailbox(
|
||||
MailboxId(Id("4")),
|
||||
parentId: null,
|
||||
name: MailboxName('Sent'),
|
||||
sortOrder: SortOrder(sortValue: 50),
|
||||
role: Role('sent'),
|
||||
),
|
||||
PresentationMailbox(
|
||||
MailboxId(Id("1")),
|
||||
parentId: null,
|
||||
name: MailboxName('Inbox'),
|
||||
sortOrder: SortOrder(sortValue: 10),
|
||||
role: Role('inbox'),
|
||||
),
|
||||
virtualStarred,
|
||||
virtualActionRequired,
|
||||
];
|
||||
|
||||
final filtered = allMailboxesIncludingVirtual.withoutVirtualMailbox;
|
||||
|
||||
final generatedTree = await TreeBuilder().generateMailboxTreeInUI(
|
||||
allMailboxes: filtered,
|
||||
currentDefaultTree: MailboxTree(MailboxNode.root()),
|
||||
currentPersonalTree: MailboxTree(MailboxNode.root()),
|
||||
currentTeamMailboxTree: MailboxTree(MailboxNode.root()),
|
||||
);
|
||||
|
||||
final children = generatedTree.defaultTree.root.childrenItems ?? [];
|
||||
expect(
|
||||
children,
|
||||
isNotEmpty,
|
||||
reason:
|
||||
'Default tree must have at least one mailbox after filtering virtual folders',
|
||||
);
|
||||
expect(
|
||||
children.first.item.id.id.value,
|
||||
equals('1'),
|
||||
reason:
|
||||
'Inbox (sortOrder=10) must be first when virtual folders are excluded',
|
||||
);
|
||||
expect(
|
||||
children,
|
||||
everyElement(
|
||||
isNot(predicate<MailboxNode>((n) => n.item.isVirtualFolder))),
|
||||
reason: 'No virtual folders should appear in the tree',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'Regression: Virtual folders with null sortOrder sort to top when passed to generateMailboxTreeInUI',
|
||||
() async {
|
||||
final testCaseWithVirtualFolders = [
|
||||
PresentationMailbox(MailboxId(Id("4")),
|
||||
parentId: null,
|
||||
name: MailboxName('Sent'),
|
||||
sortOrder: SortOrder(sortValue: 50),
|
||||
role: Role('sent')),
|
||||
PresentationMailbox(MailboxId(Id("1")),
|
||||
parentId: null,
|
||||
name: MailboxName('Inbox'),
|
||||
sortOrder: SortOrder(sortValue: 10),
|
||||
role: Role('inbox')),
|
||||
virtualStarred,
|
||||
];
|
||||
|
||||
final generatedTree = await TreeBuilder().generateMailboxTreeInUI(
|
||||
allMailboxes: testCaseWithVirtualFolders,
|
||||
currentDefaultTree: MailboxTree(MailboxNode.root()),
|
||||
currentPersonalTree: MailboxTree(MailboxNode.root()),
|
||||
currentTeamMailboxTree: MailboxTree(MailboxNode.root()),
|
||||
);
|
||||
|
||||
final children = generatedTree.defaultTree.root.childrenItems ?? [];
|
||||
expect(
|
||||
children,
|
||||
isNotEmpty,
|
||||
reason: 'Virtual folder must have been routed to the default tree',
|
||||
);
|
||||
expect(
|
||||
children.first.item.id,
|
||||
equals(virtualStarred.id),
|
||||
reason:
|
||||
'Virtual folder with null sortOrder sorts to top — controller must pass allMailboxes.withoutVirtualMailbox to buildTree',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Routing & Sorting', () {
|
||||
test('Mailboxes are routed to correct trees and sorted alphabetically',
|
||||
() async {
|
||||
final mailboxes = [
|
||||
PresentationMailbox(MailboxId(Id("1")),
|
||||
parentId: null, name: MailboxName('Inbox'), role: Role('inbox')),
|
||||
PresentationMailbox(MailboxId(Id("3")),
|
||||
parentId: MailboxId(Id("1")), name: MailboxName('Zebra')),
|
||||
PresentationMailbox(MailboxId(Id("2")),
|
||||
parentId: MailboxId(Id("1")), name: MailboxName('Apple')),
|
||||
PresentationMailbox(
|
||||
MailboxId(Id("4")),
|
||||
parentId: null,
|
||||
name: MailboxName('Personal'),
|
||||
namespace: Namespace('Personal'),
|
||||
),
|
||||
PresentationMailbox(
|
||||
MailboxId(Id("5")),
|
||||
parentId: null,
|
||||
name: MailboxName('Team'),
|
||||
namespace: Namespace('Team'),
|
||||
),
|
||||
];
|
||||
|
||||
final result =
|
||||
await TreeBuilder().generateMailboxTreeInUIAfterRefreshChanges(
|
||||
allMailboxes: mailboxes,
|
||||
currentDefaultTree: MailboxTree(MailboxNode.root()),
|
||||
currentPersonalTree: MailboxTree(MailboxNode.root()),
|
||||
currentTeamMailboxTree: MailboxTree(MailboxNode.root()),
|
||||
);
|
||||
|
||||
expect(result.defaultTree.root.childrenItems?.length, equals(1));
|
||||
expect(
|
||||
result.personalTree.root.childrenItems,
|
||||
isNotEmpty,
|
||||
reason: 'Personal tree must have at least one mailbox',
|
||||
);
|
||||
expect(
|
||||
result.personalTree.root.childrenItems?.first.item.name?.name,
|
||||
equals('Personal'),
|
||||
);
|
||||
expect(
|
||||
result.teamMailboxTree.root.childrenItems,
|
||||
isNotEmpty,
|
||||
reason: 'Team tree must have at least one mailbox',
|
||||
);
|
||||
expect(
|
||||
result.teamMailboxTree.root.childrenItems?.first.item.name?.name,
|
||||
equals('Team'),
|
||||
);
|
||||
|
||||
final inboxChildren =
|
||||
result.defaultTree.root.childrenItems?.first.childrenItems ?? [];
|
||||
expect(inboxChildren.length, equals(2));
|
||||
expect(
|
||||
inboxChildren[0].item.name?.name,
|
||||
equals('Apple'),
|
||||
reason: 'Apple must sort before Zebra',
|
||||
);
|
||||
expect(inboxChildren[1].item.name?.name, equals('Zebra'));
|
||||
});
|
||||
});
|
||||
|
||||
group('UI State Preservation', () {
|
||||
test('Preserves expandMode and selectMode from previous trees', () async {
|
||||
final targetId = MailboxId(Id("1"));
|
||||
final mailbox = PresentationMailbox(
|
||||
targetId,
|
||||
parentId: null,
|
||||
name: MailboxName('Inbox'),
|
||||
role: Role('inbox'),
|
||||
);
|
||||
|
||||
final oldNode = MailboxNode(
|
||||
mailbox,
|
||||
expandMode: ExpandMode.EXPAND,
|
||||
selectMode: SelectMode.ACTIVE,
|
||||
);
|
||||
final currentDefaultTree = MailboxTree(MailboxNode.root())
|
||||
..root.addChildNode(oldNode);
|
||||
|
||||
final result =
|
||||
await TreeBuilder().generateMailboxTreeInUIAfterRefreshChanges(
|
||||
allMailboxes: [mailbox],
|
||||
currentDefaultTree: currentDefaultTree,
|
||||
currentPersonalTree: MailboxTree(MailboxNode.root()),
|
||||
currentTeamMailboxTree: MailboxTree(MailboxNode.root()),
|
||||
);
|
||||
|
||||
final newNode = result.defaultTree.root.childrenItems?.first;
|
||||
expect(
|
||||
newNode?.expandMode,
|
||||
equals(ExpandMode.EXPAND),
|
||||
reason: 'expandMode must be carried over',
|
||||
);
|
||||
expect(
|
||||
newNode?.selectMode,
|
||||
equals(SelectMode.ACTIVE),
|
||||
reason: 'selectMode must be carried over',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Cascading Deactivation (generateMailboxTreeInUI only)', () {
|
||||
test('Selecting a parent deactivates it and cascades to its children',
|
||||
() async {
|
||||
final parentId = MailboxId(Id("parent_1"));
|
||||
final childId = MailboxId(Id("child_1"));
|
||||
final grandChildId = MailboxId(Id("grandchild_1"));
|
||||
|
||||
final mailboxes = [
|
||||
PresentationMailbox(
|
||||
parentId,
|
||||
parentId: null,
|
||||
name: MailboxName('Parent'),
|
||||
role: Role('inbox'),
|
||||
),
|
||||
PresentationMailbox(
|
||||
childId,
|
||||
parentId: parentId,
|
||||
name: MailboxName('Child'),
|
||||
),
|
||||
PresentationMailbox(
|
||||
grandChildId,
|
||||
parentId: childId,
|
||||
name: MailboxName('Grandchild'),
|
||||
),
|
||||
];
|
||||
|
||||
final result = await TreeBuilder().generateMailboxTreeInUI(
|
||||
allMailboxes: mailboxes,
|
||||
currentDefaultTree: MailboxTree(MailboxNode.root()),
|
||||
currentPersonalTree: MailboxTree(MailboxNode.root()),
|
||||
currentTeamMailboxTree: MailboxTree(MailboxNode.root()),
|
||||
mailboxIdSelected: parentId,
|
||||
);
|
||||
|
||||
final parentNode = result.defaultTree.root.childrenItems?.first;
|
||||
final childNode = parentNode?.childrenItems?.first;
|
||||
final grandChildNode = childNode?.childrenItems?.first;
|
||||
|
||||
expect(parentNode?.nodeState, equals(MailboxState.deactivated));
|
||||
expect(childNode?.nodeState, equals(MailboxState.deactivated));
|
||||
expect(grandChildNode?.nodeState, equals(MailboxState.deactivated));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user