feat: implement bulk email moving functionality in email store
This commit is contained in:
@@ -30,7 +30,7 @@ interface UseMailboxDropReturn {
|
|||||||
export function useMailboxDrop({ mailbox, onDropComplete, onSuccess, onError }: UseMailboxDropOptions): UseMailboxDropReturn {
|
export function useMailboxDrop({ mailbox, onDropComplete, onSuccess, onError }: UseMailboxDropOptions): UseMailboxDropReturn {
|
||||||
const [isOver, setIsOver] = useState(false);
|
const [isOver, setIsOver] = useState(false);
|
||||||
const { client } = useAuthStore();
|
const { client } = useAuthStore();
|
||||||
const { moveToMailbox, selectedEmailIds, clearSelection, fetchEmails, selectedMailbox } = useEmailStore();
|
const { moveEmailsToMailbox, selectedEmailIds, clearSelection, fetchEmails, selectedMailbox } = useEmailStore();
|
||||||
const { isDragging, sourceMailboxId, draggedEmails, endDrag } = useDragDropContext();
|
const { isDragging, sourceMailboxId, draggedEmails, endDrag } = useDragDropContext();
|
||||||
|
|
||||||
// Determine if this is a valid drop target
|
// Determine if this is a valid drop target
|
||||||
@@ -106,13 +106,8 @@ export function useMailboxDrop({ mailbox, onDropComplete, onSuccess, onError }:
|
|||||||
|
|
||||||
const emailIds: string[] = JSON.parse(emailIdsJson);
|
const emailIds: string[] = JSON.parse(emailIdsJson);
|
||||||
|
|
||||||
// Get the destination mailbox ID (use originalId for shared folders)
|
// Move in a single bulk JMAP request (store handles counter updates).
|
||||||
const destinationId = mailbox.originalId || mailbox.id;
|
await moveEmailsToMailbox(client, emailIds, mailbox.id);
|
||||||
|
|
||||||
// Move emails one by one (store handles counter updates)
|
|
||||||
for (const emailId of emailIds) {
|
|
||||||
await moveToMailbox(client, emailId, destinationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear selection if any selected emails were moved
|
// Clear selection if any selected emails were moved
|
||||||
if (emailIds.some(id => selectedEmailIds.has(id))) {
|
if (emailIds.some(id => selectedEmailIds.has(id))) {
|
||||||
@@ -148,7 +143,7 @@ export function useMailboxDrop({ mailbox, onDropComplete, onSuccess, onError }:
|
|||||||
} finally {
|
} finally {
|
||||||
endDrag();
|
endDrag();
|
||||||
}
|
}
|
||||||
}, [client, mailbox, isValidTarget, moveToMailbox, selectedEmailIds, clearSelection, fetchEmails, selectedMailbox, endDrag, onDropComplete, onSuccess, onError]);
|
}, [client, mailbox, isValidTarget, moveEmailsToMailbox, selectedEmailIds, clearSelection, fetchEmails, selectedMailbox, endDrag, onDropComplete, onSuccess, onError]);
|
||||||
|
|
||||||
const valid = isValidTarget();
|
const valid = isValidTarget();
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ interface EmailStore {
|
|||||||
deleteEmail: (client: IJMAPClient, emailId: string, forceDelete?: boolean) => Promise<void>;
|
deleteEmail: (client: IJMAPClient, emailId: string, forceDelete?: boolean) => Promise<void>;
|
||||||
markAsRead: (client: IJMAPClient, emailId: string, read: boolean) => Promise<void>;
|
markAsRead: (client: IJMAPClient, emailId: string, read: boolean) => Promise<void>;
|
||||||
moveToMailbox: (client: IJMAPClient, emailId: string, mailboxId: string) => Promise<void>;
|
moveToMailbox: (client: IJMAPClient, emailId: string, mailboxId: string) => Promise<void>;
|
||||||
|
moveEmailsToMailbox: (client: IJMAPClient, emailIds: string[], mailboxId: string) => Promise<void>;
|
||||||
moveThreadToMailbox: (client: IJMAPClient, emailId: string, mailboxId: string) => Promise<void>;
|
moveThreadToMailbox: (client: IJMAPClient, emailId: string, mailboxId: string) => Promise<void>;
|
||||||
searchEmails: (client: IJMAPClient, query: string) => Promise<void>;
|
searchEmails: (client: IJMAPClient, query: string) => Promise<void>;
|
||||||
advancedSearch: (client: IJMAPClient) => Promise<void>;
|
advancedSearch: (client: IJMAPClient) => Promise<void>;
|
||||||
@@ -803,6 +804,80 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
moveEmailsToMailbox: async (client, emailIds, destinationMailboxId) => {
|
||||||
|
if (emailIds.length === 0) return;
|
||||||
|
if (emailIds.length === 1) {
|
||||||
|
await get().moveToMailbox(client, emailIds[0], destinationMailboxId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { emails, mailboxes, selectedMailbox, isUnifiedView } = get();
|
||||||
|
const destMailbox = mailboxes.find(mb => mb.id === destinationMailboxId);
|
||||||
|
const jmapDestId = destMailbox?.originalId || destinationMailboxId;
|
||||||
|
const idSet = new Set(emailIds);
|
||||||
|
const affected = emails.filter(e => idSet.has(e.id));
|
||||||
|
|
||||||
|
if (isUnifiedView) {
|
||||||
|
// In unified view, emails may span accounts — group and dispatch per-account.
|
||||||
|
const byAccount = new Map<string, string[]>();
|
||||||
|
for (const e of affected) {
|
||||||
|
const acct = e.accountId || '__default__';
|
||||||
|
if (!byAccount.has(acct)) byAccount.set(acct, []);
|
||||||
|
byAccount.get(acct)!.push(e.id);
|
||||||
|
}
|
||||||
|
await Promise.all(Array.from(byAccount.entries()).map(async ([acct, ids]) => {
|
||||||
|
const acctClient = acct === '__default__' ? client : useAuthStore.getState().getClientForAccount(acct);
|
||||||
|
if (!acctClient) return;
|
||||||
|
await acctClient.batchMoveEmails(ids, jmapDestId);
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const currentMailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
||||||
|
const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined;
|
||||||
|
await client.batchMoveEmails(emailIds, jmapDestId, accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust counters and drop moved emails from the current view.
|
||||||
|
let unreadDelta = 0;
|
||||||
|
const sourceMailboxIds = new Set<string>();
|
||||||
|
for (const e of affected) {
|
||||||
|
if (!e.keywords?.$seen) unreadDelta += 1;
|
||||||
|
if (e.mailboxIds) for (const mid of Object.keys(e.mailboxIds)) sourceMailboxIds.add(mid);
|
||||||
|
}
|
||||||
|
const movedCount = affected.length;
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
emails: state.emails.filter(e => !idSet.has(e.id)),
|
||||||
|
selectedEmail: state.selectedEmail && idSet.has(state.selectedEmail.id) ? null : state.selectedEmail,
|
||||||
|
selectedEmailIds: (() => {
|
||||||
|
const next = new Set(state.selectedEmailIds);
|
||||||
|
for (const id of idSet) next.delete(id);
|
||||||
|
return next;
|
||||||
|
})(),
|
||||||
|
mailboxes: state.mailboxes.map(mb => {
|
||||||
|
if (sourceMailboxIds.has(mb.id)) {
|
||||||
|
return {
|
||||||
|
...mb,
|
||||||
|
totalEmails: Math.max(0, mb.totalEmails - movedCount),
|
||||||
|
unreadEmails: Math.max(0, mb.unreadEmails - unreadDelta),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (mb.id === destinationMailboxId) {
|
||||||
|
return {
|
||||||
|
...mb,
|
||||||
|
totalEmails: mb.totalEmails + movedCount,
|
||||||
|
unreadEmails: mb.unreadEmails + unreadDelta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mb;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: error instanceof Error ? error.message : 'Failed to move emails' });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
moveThreadToMailbox: async (client, emailId, destinationMailboxId) => {
|
moveThreadToMailbox: async (client, emailId, destinationMailboxId) => {
|
||||||
try {
|
try {
|
||||||
const state = get();
|
const state = get();
|
||||||
|
|||||||
Reference in New Issue
Block a user