mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
fix: drop ticked messages when the search changes, so Delete can't act on mail no longer listed
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useEmailStore } from '../email-store';
|
||||
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
|
||||
|
||||
function makeEmail(id: string, threadId = `thread-${id}`) {
|
||||
return {
|
||||
@@ -31,6 +32,51 @@ describe('email-store selection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('a different search', () => {
|
||||
const ticked = () => useEmailStore.getState().selectedEmailKeys.size;
|
||||
beforeEach(() => {
|
||||
useEmailStore.setState({ searchQuery: 'beacon', searchFilters: { ...DEFAULT_SEARCH_FILTERS } });
|
||||
useEmailStore.getState().toggleEmailSelection({ id: 'b' });
|
||||
expect(ticked()).toBe(1);
|
||||
});
|
||||
|
||||
it('drops the ticks when the query changes, also to nothing', () => {
|
||||
useEmailStore.getState().setSearchQuery('');
|
||||
expect(ticked()).toBe(0);
|
||||
expect(useEmailStore.getState().lastSelectedEmailKey).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps them when the same query is set again', () => {
|
||||
useEmailStore.getState().setSearchQuery('beacon');
|
||||
expect(ticked()).toBe(1);
|
||||
});
|
||||
|
||||
it('drops them when a search runs for another query, before its results land', () => {
|
||||
const pending = new Promise<never>(() => {});
|
||||
void useEmailStore.getState().searchEmails({ searchEmails: () => pending } as never, 'rollback');
|
||||
expect(ticked()).toBe(0);
|
||||
});
|
||||
|
||||
it('keeps them when the same search runs again', () => {
|
||||
const pending = new Promise<never>(() => {});
|
||||
void useEmailStore.getState().searchEmails({ searchEmails: () => pending } as never, 'beacon');
|
||||
expect(ticked()).toBe(1);
|
||||
});
|
||||
|
||||
it('drops them when a filter changes or the filters are cleared', () => {
|
||||
useEmailStore.getState().setSearchFilters({ isUnread: true });
|
||||
expect(ticked()).toBe(0);
|
||||
useEmailStore.getState().toggleEmailSelection({ id: 'b' });
|
||||
useEmailStore.getState().clearSearchFilters();
|
||||
expect(ticked()).toBe(0);
|
||||
});
|
||||
|
||||
it('keeps them when clearing filters that were not set', () => {
|
||||
useEmailStore.getState().clearSearchFilters();
|
||||
expect(ticked()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleEmailSelection', () => {
|
||||
it('should add email to selection', () => {
|
||||
useEmailStore.getState().toggleEmailSelection({ id: 'b' });
|
||||
|
||||
+11
-2
@@ -69,6 +69,8 @@ type PendingUndoSend = {
|
||||
submissionAccountId?: string;
|
||||
};
|
||||
|
||||
const NO_SELECTION = () => ({ selectedEmailKeys: new Set<string>(), lastSelectedEmailKey: null });
|
||||
|
||||
/** Drop the matching emails from a key-based selection. */
|
||||
function dropFromSelection(
|
||||
state: { emails: Email[]; selectedEmailKeys: Set<string> },
|
||||
@@ -3603,7 +3605,12 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
setLoading: (loading) => set({ isLoading: loading }),
|
||||
setLoadingEmail: (loading) => set({ isLoadingEmail: loading }),
|
||||
setError: (error) => set({ error }),
|
||||
setSearchQuery: (query) => set({ searchQuery: query }),
|
||||
// Another search lists other mail: ticks kept from the last list would stay
|
||||
// counted in the selection bar, with Delete acting on rows nobody can see.
|
||||
setSearchQuery: (query) => set((state) => ({
|
||||
searchQuery: query,
|
||||
...(query === state.searchQuery ? {} : NO_SELECTION()),
|
||||
})),
|
||||
setQuota: (quota) => set({ quota }),
|
||||
|
||||
toggleEmailSelection: (email) => {
|
||||
@@ -5360,7 +5367,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
// trip later. What the mail view renders from is written only once the
|
||||
// request is on its way (see afterRequestDispatch); the controller is
|
||||
// recorded at once so a superseding search can still cancel this one.
|
||||
set({ searchAbortController: controller });
|
||||
set({ searchAbortController: controller, ...(typedQuery === get().searchQuery ? {} : NO_SELECTION()) });
|
||||
afterRequestDispatch(() => {
|
||||
if (get().searchAbortController === controller) set({ isLoading: true, error: null, searchQuery: typedQuery });
|
||||
});
|
||||
@@ -5598,6 +5605,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
setSearchFilters: (filters) => {
|
||||
set((state) => ({
|
||||
searchFilters: { ...state.searchFilters, ...filters },
|
||||
...NO_SELECTION(),
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -5617,6 +5625,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
searchFilters: { ...DEFAULT_SEARCH_FILTERS },
|
||||
searchMailboxId: currentSearchScopeDefault(mailboxesInView(state), state.selectedMailbox),
|
||||
searchScopeFollowsFolder: false,
|
||||
...(isFilterEmpty(state.searchFilters) ? {} : NO_SELECTION()),
|
||||
}));
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user