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 {
|
||||
const [isOver, setIsOver] = useState(false);
|
||||
const { client } = useAuthStore();
|
||||
const { moveToMailbox, selectedEmailIds, clearSelection, fetchEmails, selectedMailbox } = useEmailStore();
|
||||
const { moveEmailsToMailbox, selectedEmailIds, clearSelection, fetchEmails, selectedMailbox } = useEmailStore();
|
||||
const { isDragging, sourceMailboxId, draggedEmails, endDrag } = useDragDropContext();
|
||||
|
||||
// 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);
|
||||
|
||||
// Get the destination mailbox ID (use originalId for shared folders)
|
||||
const destinationId = mailbox.originalId || mailbox.id;
|
||||
|
||||
// Move emails one by one (store handles counter updates)
|
||||
for (const emailId of emailIds) {
|
||||
await moveToMailbox(client, emailId, destinationId);
|
||||
}
|
||||
// Move in a single bulk JMAP request (store handles counter updates).
|
||||
await moveEmailsToMailbox(client, emailIds, mailbox.id);
|
||||
|
||||
// Clear selection if any selected emails were moved
|
||||
if (emailIds.some(id => selectedEmailIds.has(id))) {
|
||||
@@ -148,7 +143,7 @@ export function useMailboxDrop({ mailbox, onDropComplete, onSuccess, onError }:
|
||||
} finally {
|
||||
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();
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ interface EmailStore {
|
||||
deleteEmail: (client: IJMAPClient, emailId: string, forceDelete?: boolean) => Promise<void>;
|
||||
markAsRead: (client: IJMAPClient, emailId: string, read: boolean) => 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>;
|
||||
searchEmails: (client: IJMAPClient, query: string) => 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) => {
|
||||
try {
|
||||
const state = get();
|
||||
|
||||
Reference in New Issue
Block a user