fix(unified-mailbox): update shared/group folder counters on delete/move/read
In the unified All mail / Unread / Starred views, deleting (or moving /
marking read) a message from a shared/group folder left that folder's
sidebar counter at its old value.
Root cause: lib/unified-mailbox.ts decorates shared emails WITHOUT
namespacing their `mailboxIds`, so they carry the owner's bare JMAP ids,
while the shared mailbox is stored with a namespaced id (`${ownerId}:${origId}`)
and `isShared: true`. `emailInMailbox` only matched the namespaced `mailbox.id`
and disabled the `originalId` fallback for shared mailboxes, so no shared
email ever matched its folder and the counter math skipped it.
Match shared mailboxes via `originalId` too, scoped to the owning account
(`sourceAccountId === mailbox.accountId`) so a bare owner id can't collide
with another account's folder. This is the single matching helper used by all
counter paths (delete/move/markRead/spam), so they're all fixed at once.
Adds a regression test covering deletion of a shared-folder email in the
unified view.
This commit is contained in:
@@ -54,6 +54,8 @@ function makeClient() {
|
||||
toggleStar: vi.fn().mockResolvedValue(undefined),
|
||||
moveEmail: vi.fn().mockResolvedValue(undefined),
|
||||
batchMarkAsRead: vi.fn().mockResolvedValue(undefined),
|
||||
batchDeleteEmails: vi.fn().mockResolvedValue(undefined),
|
||||
batchMoveEmails: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as IJMAPClient;
|
||||
}
|
||||
|
||||
@@ -180,6 +182,40 @@ describe('unified-view single-email action routing (#281)', () => {
|
||||
expect(activeClient.toggleStar).toHaveBeenCalledWith('email-shared', true, 'owner-x');
|
||||
});
|
||||
|
||||
it('decrements a shared/group folder counter when deleting from the unified view', async () => {
|
||||
// Real app: the active account's `mailboxes` includes its delegated shared
|
||||
// folders (namespaced id + originalId + owner accountId). Unified-fetched
|
||||
// shared emails carry the owner's BARE mailboxIds and sourceAccountId=owner.
|
||||
// Regression: emailInMailbox missed these, so the shared folder's badge
|
||||
// stayed at its old value after deleting in All mail / All unread.
|
||||
useEmailStore.setState({
|
||||
mailboxes: [
|
||||
makeMailbox({ id: 'a-inbox', role: 'inbox', unreadEmails: 2, totalEmails: 5 }),
|
||||
makeMailbox({
|
||||
id: 'owner-x:x-inbox', originalId: 'x-inbox', name: 'Shared Inbox',
|
||||
role: 'inbox', isShared: true, accountId: 'owner-x',
|
||||
unreadEmails: 4, totalEmails: 10,
|
||||
}),
|
||||
],
|
||||
emails: [
|
||||
makeEmail({
|
||||
id: 'email-shared', accountId: 'owner-x',
|
||||
sourceClientAccountId: 'account-a', sourceAccountId: 'owner-x',
|
||||
keywords: {}, // unread
|
||||
mailboxIds: { 'x-inbox': true }, // BARE owner id (not namespaced)
|
||||
}),
|
||||
],
|
||||
selectedEmailIds: new Set(['email-shared']),
|
||||
});
|
||||
|
||||
await useEmailStore.getState().batchDelete(activeClient, true);
|
||||
|
||||
expect(activeClient.batchDeleteEmails).toHaveBeenCalledWith(['email-shared'], 'owner-x');
|
||||
const shared = useEmailStore.getState().mailboxes.find(m => m.id === 'owner-x:x-inbox')!;
|
||||
expect(shared.unreadEmails).toBe(3); // was 4
|
||||
expect(shared.totalEmails).toBe(9); // was 10
|
||||
});
|
||||
|
||||
it('still uses the active/passed client outside unified view', async () => {
|
||||
useEmailStore.setState({
|
||||
isUnifiedView: false,
|
||||
|
||||
Reference in New Issue
Block a user