fix: keep archived messages in the Archive and in search results after a plugin's batch, and update folder counts

This commit is contained in:
Linus Rath
2026-11-03 00:53:35 +01:00
parent 37aaf05d6d
commit 8034b005b8
2 changed files with 37 additions and 4 deletions
@@ -32,7 +32,10 @@ describe('batch functions with an explicit target', () => {
// The selection names another message: a targeted call must leave it alone.
selectedEmailKeys: new Set(['keep']),
isUnifiedView: true,
selectedMailbox: 'inbox',
searchQuery: '',
fetchTagCounts: vi.fn(),
fetchMailboxes: vi.fn(async () => {}),
} as never);
});
@@ -48,6 +51,19 @@ describe('batch functions with an explicit target', () => {
expect([...useEmailStore.getState().selectedEmailKeys]).toEqual(['keep']);
});
it('keeps the rows where they are still listed, and refreshes the folder counts', async () => {
const target = { emailIds: ['m1'], client, emails: [email('m1')] };
// The Archive is the open folder: the archived message belongs there.
useEmailStore.setState({ selectedMailbox: 'arch' } as never);
await useEmailStore.getState().batchArchive(other, target);
expect(useEmailStore.getState().emails.map((e) => e.id)).toEqual(['m1', 'keep']);
// A search still finds it.
useEmailStore.setState({ selectedMailbox: 'inbox', searchQuery: 'invoice' } as never);
await useEmailStore.getState().batchArchive(other, target);
expect(useEmailStore.getState().emails.map((e) => e.id)).toEqual(['m1', 'keep']);
expect(useEmailStore.getState().fetchMailboxes).toHaveBeenCalledTimes(2);
});
it('refuses an Archive someone else reads, asking the server for its share map', async () => {
Object.assign(client, {
supportsMailboxSharing: () => true,
+21 -4
View File
@@ -1415,11 +1415,28 @@ async function archiveTarget(client: IJMAPClient, target: BatchActionTarget): Pr
);
emitEmailMove(move, hookMailboxes);
const moved = new Set(move.emailIds);
useEmailStore.setState(state => ({
emails: state.emails.filter(e => !moved.has(e.id)),
selectedEmailKeys: dropFromSelection(state, e => moved.has(e.id)),
}));
// The rows leave the open list only where they no longer belong: not the
// Archive itself (or a year or month folder in it), not a search, which
// still finds them. Elsewhere the refresh below settles the rest.
const state = useEmailStore.getState();
const archiveTree = new Set([archive.id]);
for (let grew = true; grew;) {
grew = false;
for (const m of [...state.mailboxes, ...mailboxes]) {
if (m.parentId && archiveTree.has(m.parentId) && !archiveTree.has(m.id)) { archiveTree.add(m.id); grew = true; }
}
}
const searching = !!state.searchQuery || !isFilterEmpty(state.searchFilters);
const stillListed = searching || (!!state.selectedMailbox && archiveTree.has(state.selectedMailbox));
if (!stillListed) {
useEmailStore.setState(current => ({
emails: current.emails.filter(e => !moved.has(e.id)),
selectedEmailKeys: dropFromSelection(current, e => moved.has(e.id)),
}));
}
if (!useEmailStore.getState().isUnifiedView) await useEmailStore.getState().refreshCurrentMailbox(client);
// The folders' message and unread counts changed.
await useEmailStore.getState().fetchMailboxes(client);
return true;
}