feat: Add folder sync selection and All Mail exclusion logic

This commit is contained in:
rustmailer
2025-11-22 17:02:20 +08:00
parent 1e9a5fde51
commit 1042fad6d0
@@ -27,7 +27,7 @@ import {
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Loader2 } from 'lucide-react' import { Loader2, CheckSquare, Square } from 'lucide-react'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import { AccountModel } from '../data/schema' import { AccountModel } from '../data/schema'
import { toast } from '@/hooks/use-toast' import { toast } from '@/hooks/use-toast'
@@ -50,6 +50,7 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
const [selectedFolders, setSelectedFolders] = useState<string[]>(currentRow.sync_folders || []); const [selectedFolders, setSelectedFolders] = useState<string[]>(currentRow.sync_folders || []);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: mailboxes, isLoading } = useQuery({ const { data: mailboxes, isLoading } = useQuery({
queryKey: ['account-mailboxes', currentRow.id], queryKey: ['account-mailboxes', currentRow.id],
queryFn: () => list_mailboxes(currentRow.id, true), queryFn: () => list_mailboxes(currentRow.id, true),
@@ -64,21 +65,56 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
.map(mailbox => mailbox.id.toString()); .map(mailbox => mailbox.id.toString());
}, [mailboxes, selectedFolders]); }, [mailboxes, selectedFolders]);
// Convert data to tree structure
const treeData = useMemo(() => { const treeData = useMemo(() => {
if (!mailboxes) return []; if (!mailboxes) return [];
return buildTree(mailboxes, undefined, true, true); return buildTree(mailboxes, undefined, true, true);
}, [mailboxes]); }, [mailboxes]);
const handleSelectItems = useCallback((selectedItems: TreeDataItem[]) => { const handleSelectItems = useCallback((selectedItems: TreeDataItem[]) => {
const allMailboxes = mailboxes || [];
const selected = selectedItems const selected = selectedItems
.map(item => mailboxes?.find(m => m.id === parseInt(item.id, 10))?.name) .map(item => mailboxes?.find(m => m.id === parseInt(item.id, 10))?.name)
.filter(Boolean) as string[]; .filter(Boolean) as string[];
const allMailSelected = selected.some(selectedName => {
const mailbox = allMailboxes.find(m => m.name === selectedName);
if (!mailbox) return false;
return mailbox.attributes.some(a => a.attr === 'All');
});
if (allMailSelected) {
toast({
title: 'Heads Up: "All Mail" Folder Selected',
description: 'Selecting folders with the "All Mail" attribute will likely lead to duplicating messages already synced from folders like Inbox and Sent. This may consume significantly more storage space.',
action: <ToastAction altText="I Understand">OK</ToastAction>,
});
}
setSelectedFolders(selected); setSelectedFolders(selected);
}, [mailboxes]); }, [mailboxes]);
const handleSelectAll = useCallback(() => {
if (!mailboxes) return;
const validFolderNames = mailboxes
.filter(mailbox => {
const isAllMail = mailbox.attributes.some(a => a.attr === 'All');
if (isAllMail) return false;
return true;
})
.map(m => m.name);
setSelectedFolders(validFolderNames);
if (validFolderNames.length < mailboxes.length) {
toast({
description: "Selected standard folders. 'All Mail' was skipped to avoid duplicates.",
});
}
}, [mailboxes]);
const handleDeselectAll = useCallback(() => {
setSelectedFolders([]);
}, []);
const updateMutation = useMutation({ const updateMutation = useMutation({
mutationFn: (data: Record<string, any>) => update_account(currentRow?.id ?? '', data), mutationFn: (data: Record<string, any>) => update_account(currentRow?.id ?? '', data),
onSuccess: handleSuccess, onSuccess: handleSuccess,
@@ -96,6 +132,7 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
setIsSubmitting(false); setIsSubmitting(false);
onOpenChange(false); onOpenChange(false);
} }
function handleError(error: AxiosError) { function handleError(error: AxiosError) {
const errorMessage = (error.response?.data as { message?: string })?.message || const errorMessage = (error.response?.data as { message?: string })?.message ||
error.message || error.message ||
@@ -111,7 +148,6 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
console.error(error); console.error(error);
} }
const handleSubmit = async () => { const handleSubmit = async () => {
if (selectedFolders.length === 0) { if (selectedFolders.length === 0) {
toast({ toast({
@@ -138,7 +174,29 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-end"> <div className="flex items-center justify-between pt-2">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleSelectAll}
disabled={isLoading || !mailboxes || mailboxes.length === 0}
className="h-8"
>
<CheckSquare className="w-4 h-4 mr-2" />
Select All
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDeselectAll}
disabled={isLoading || selectedFolders.length === 0}
className="h-8"
>
<Square className="w-4 h-4 mr-2" />
Deselect All
</Button>
</div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{selectedFolders.length} folder(s) selected {selectedFolders.length} folder(s) selected
</div> </div>
@@ -160,6 +218,7 @@ export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
)} )}
{!isLoading && ( {!isLoading && (
<TreeView <TreeView
key={selectedFolders.length > 0 ? `tree-${selectedFolders.length}-${selectedFolders[0]}` : 'tree-empty'}
data={treeData} data={treeData}
multiple multiple
expandAll expandAll