// // Copyright (c) 2025 rustmailer.com (https://rustmailer.com) // // This file is part of the Bichon Email Archiving Project // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Loader2, CheckSquare, Square } from 'lucide-react' import { useCallback, useMemo, useState } from 'react' import { AccountModel } from '../data/schema' import { toast } from '@/hooks/use-toast' import { list_mailboxes } from '@/api/mailbox/api' import { buildTree } from '@/lib/build-tree' import { TreeDataItem, TreeView } from '@/components/tree-view' import { Skeleton } from '@/components/ui/skeleton' import { update_account } from '@/api/account/api' import { ToastAction } from '@/components/ui/toast' import { AxiosError } from 'axios' import { ScrollArea } from '@/components/ui/scroll-area' import { useTranslation } from 'react-i18next' interface Props { open: boolean onOpenChange: (open: boolean) => void currentRow: AccountModel } export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) { const [selectedFolders, setSelectedFolders] = useState(currentRow.sync_folders || []); const [isSubmitting, setIsSubmitting] = useState(false); const queryClient = useQueryClient(); const { t } = useTranslation() const { data: mailboxes, isLoading } = useQuery({ queryKey: ['account-mailboxes', currentRow.id], queryFn: () => list_mailboxes(currentRow.id, true), enabled: open, }); // Convert mailbox names to IDs for initial selection const initialSelectedItemIds = useMemo(() => { if (!mailboxes) return []; return mailboxes .filter(mailbox => selectedFolders.includes(mailbox.name)) .map(mailbox => mailbox.id.toString()); }, [mailboxes, selectedFolders]); const treeData = useMemo(() => { if (!mailboxes) return []; return buildTree(mailboxes, undefined, true, true); }, [mailboxes]); const handleSelectItems = useCallback((selectedItems: TreeDataItem[]) => { const allMailboxes = mailboxes || []; const selected = selectedItems .map(item => mailboxes?.find(m => m.id === parseInt(item.id, 10))?.name) .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: t('accounts.allMailFolderSelected'), description: t('accounts.allMailFolderSelectedDesc'), action: {t('common.ok')}, }); } setSelectedFolders(selected); }, [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: t('accounts.allMailSkipped'), }); } }, [mailboxes]); const handleDeselectAll = useCallback(() => { setSelectedFolders([]); }, []); const updateMutation = useMutation({ mutationFn: (data: Record) => update_account(currentRow?.id ?? '', data), onSuccess: handleSuccess, onError: handleError }) function handleSuccess() { toast({ title: t('accounts.accountSyncFoldersUpdated'), description: t('accounts.accountUpdatedDesc'), action: {t('common.close')}, }); queryClient.invalidateQueries({ queryKey: ['account-list'] }); setIsSubmitting(false); onOpenChange(false); } function handleError(error: AxiosError) { const errorMessage = (error.response?.data as { message?: string })?.message || error.message || t('accounts.updateFailed'); toast({ variant: "destructive", title: t('accounts.accountSyncFoldersUpdateFailed'), description: errorMessage as string, action: {t('common.tryAgain')}, }); setIsSubmitting(false); console.error(error); } const handleSubmit = async () => { if (selectedFolders.length === 0) { toast({ title: t('common.error'), description: t('accounts.selectAtLeastOneFolder'), variant: 'destructive', }); return; } setIsSubmitting(true); updateMutation.mutate({ sync_folders: selectedFolders, }); }; return ( {t('accounts.selectSyncFolders')} {t('accounts.chooseFoldersToSync', { "email": currentRow.email })} {t('common.selectAll')} {t('common.deselectAll')} {t('accounts.foldersSelected', { count: selectedFolders.length })} {isLoading && ( Loading mailbox folders… {[...Array(8)].map((_, i) => ( ))} )} {!isLoading && ( 0 ? `tree-${selectedFolders.length}-${selectedFolders[0]}` : 'tree-empty'} data={treeData} multiple expandAll clickRowToSelect={false} initialSelectedItemIds={initialSelectedItemIds} onSelectItemsChange={handleSelectItems} /> )} onOpenChange(false)} disabled={isSubmitting} > Cancel {isSubmitting && } Save Changes ); }