// // 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, useQueryClient } from '@tanstack/react-query' import { Loader2, CheckSquare, Square } from 'lucide-react' import { useCallback, useEffect, useMemo, useState } from 'react' import { AccountModel } from '../data/schema' import { toast } from '@/hooks/use-toast' import { list_mailboxes, MailboxData } from '@/api/mailbox/api' import { buildTree, ExtendedTreeItemProps } from '@/lib/build-tree' import { Skeleton } from '@/components/ui/skeleton' import { update_account } from '@/api/account/api' import { ToastAction } from '@/components/ui/toast' import axios, { AxiosError } from 'axios' import { ScrollArea } from '@/components/ui/scroll-area' import { useTranslation } from 'react-i18next' import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; import { useTheme } from '@/context/theme-context' import React from 'react' import Collapse from '@mui/material/Collapse'; import { styled } from '@mui/material/styles'; import { TreeItemCheckbox, TreeItemContent, TreeItemIconContainer, TreeItemLabel, TreeItemRoot } from '@mui/x-tree-view/TreeItem' import { TreeItemDragAndDropOverlay, TreeItemIcon, TreeItemProvider, TreeViewBaseItem, TreeViewSelectionPropagation, useTreeItem, useTreeItemModel, UseTreeItemParameters } from '@mui/x-tree-view' import { animated, useSpring } from '@react-spring/web'; import { TransitionProps } from '@mui/material/transitions' function getParentIds(tree: TreeViewBaseItem[]): string[] { const result: string[] = []; function traverse(nodes: TreeViewBaseItem[]) { for (const node of nodes) { if (node.children && node.children.length > 0) { result.push(node.id); traverse(node.children); } } } traverse(tree); return result; } interface CustomLabelProps { exists?: number; attributes?: { attr: string; extension: string | null }[], children: React.ReactNode; icon?: React.ElementType; expandable?: boolean; } function CustomLabel({ expandable, exists, attributes, children, ...other }: CustomLabelProps) { return ( {children}
{attributes?.map((attr) => { const text = attr.attr === 'Extension' ? attr.extension : attr.attr; return ( {text} ); })}
{exists !== undefined && ( {exists} )}
); } const CustomCollapse = styled(Collapse)({ padding: 0, }); const AnimatedCollapse = animated(CustomCollapse); function TransitionComponent(props: TransitionProps) { const style = useSpring({ to: { opacity: props.in ? 1 : 0, transform: `translate3d(0,${props.in ? 0 : 20}px,0)`, }, }); return ; } interface CustomTreeItemProps extends Omit, Omit, 'onFocus'> { } interface Props { open: boolean onOpenChange: (open: boolean) => void currentRow: AccountModel } export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) { const [selectedItems, setSelectedItems] = React.useState([]); const [isSubmitting, setIsSubmitting] = useState(false); const [allIds, setAllIds] = useState([]); const [expandedItems, setExpandedItems] = useState([]); const [itemsWithChildren, setItemsWithChildren] = useState([]); const [mailboxes, setMailboxes] = useState([]); const [selectionPropagation, setSelectionPropagation] = React.useState({ parents: false, descendants: true, }); const [treeData, setTreeData] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); const queryClient = useQueryClient(); const { t } = useTranslation() const { theme } = useTheme() useEffect(() => { if (!open) return; let cancelled = false; const fetchMailboxes = async () => { setIsLoading(true); try { const data = await list_mailboxes(currentRow.id, true); if (!cancelled) { setMailboxes(data); const allIds = data.map(mailbox => String(mailbox.id)); setAllIds(allIds); const tree = buildTree(data); setTreeData(tree); const itemsWithChildren = getParentIds(tree); setItemsWithChildren(itemsWithChildren); setExpandedItems(itemsWithChildren); const sync_folders = data .filter(mailbox => currentRow.sync_folders.includes(mailbox.name)) .map(mailbox => mailbox.id.toString()); setSelectedItems(sync_folders); setError(undefined); } } catch (err: any) { if (axios.isAxiosError(err)) { const resData = err.response?.data; if (resData) { setError(`Error ${resData.code || ''}: ${resData.message || ''}`); } else { setError(err.message); } } } finally { if (!cancelled) setIsLoading(false); } }; fetchMailboxes(); return () => { cancelled = true; }; }, [currentRow, open]); const handleExpandedItemsChange = ( _event: React.SyntheticEvent | null, itemIds: string[], ) => { setExpandedItems(itemIds); }; const handleExpandClick = () => { setExpandedItems((oldExpanded) => oldExpanded.length === 0 ? itemsWithChildren : [], ); }; const CustomTreeItem = useMemo(() => { return React.forwardRef(function CustomTreeItem( props: CustomTreeItemProps, ref: React.Ref, ) { const { id, itemId, label, disabled, children, ...other } = props; const { getContextProviderProps, getRootProps, getContentProps, getIconContainerProps, getCheckboxProps, getLabelProps, getGroupTransitionProps, getDragAndDropOverlayProps, status, } = useTreeItem({ id, itemId, children, label, disabled, rootRef: ref }); const item = useTreeItemModel(itemId)!; return ( {children && } ); }); }, [theme]); const handleSelectAll = useCallback(() => { const selectedSet = new Set(allIds); const selectedAllMailbox = mailboxes.find((mb) => selectedSet.has(String(mb.id)) && mb.attributes?.some(a => a.attr === 'All') ); if (selectedAllMailbox) { toast({ title: t('accounts.allMailFolderSelected'), description: t('accounts.allMailFolderSelectedDesc'), action: {t('common.ok')}, }); } setSelectedItems(allIds); }, [allIds]); const handleDeselectAll = useCallback(() => { setSelectedItems([]); }, []); 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 handleSelectedItemsChange = ( _event: React.SyntheticEvent | null, newSelectedItems: string[], ) => { const selectedSet = new Set(newSelectedItems); const selectedAllMailbox = mailboxes.find((mb) => selectedSet.has(String(mb.id)) && mb.attributes?.some(a => a.attr === 'All') ); if (selectedAllMailbox) { toast({ title: t('accounts.allMailFolderSelected'), description: t('accounts.allMailFolderSelectedDesc'), action: {t('common.ok')}, }); } setSelectedItems(newSelectedItems); }; const handleSubmit = async () => { if (selectedItems.length === 0) { toast({ title: t('common.error'), description: t('accounts.selectAtLeastOneFolder'), variant: 'destructive', }); return; } setIsSubmitting(true); const selectedNames: string[] = []; const idSet = new Set(selectedItems); for (const mailbox of mailboxes) { if (idSet.has(String(mailbox.id))) { selectedNames.push(mailbox.name); } } updateMutation.mutate({ sync_folders: selectedNames, }); }; return ( {t('accounts.selectSyncFolders')} {t('accounts.chooseFoldersToSync', { "email": currentRow.email })}
{t('accounts.foldersSelected', { count: selectedItems.length })}
{isLoading && (
{t('accounts.folderSync.loadingMailboxFolders')}
{[...Array(8)].map((_, i) => ( ))}
)} {!isLoading && ( )} {error && (
{error}
)}
); }