Files
bichon/web/src/features/accounts/components/sync-folders.tsx
T

183 lines
6.7 KiB
TypeScript
Raw Normal View History

2025-11-19 02:14:37 +08:00
//
// 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 <http://www.gnu.org/licenses/>.
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 } 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'
interface Props {
open: boolean
onOpenChange: (open: boolean) => void
currentRow: AccountModel
}
export function SyncFoldersDialog({ currentRow, open, onOpenChange }: Props) {
const [selectedFolders, setSelectedFolders] = useState<string[]>(currentRow.sync_folders || []);
const [isSubmitting, setIsSubmitting] = useState(false);
const queryClient = useQueryClient();
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]);
// Convert data to tree structure
const treeData = useMemo(() => {
if (!mailboxes) return [];
return buildTree(mailboxes, undefined, true, true);
}, [mailboxes]);
const handleSelectItems = useCallback((selectedItems: TreeDataItem[]) => {
const selected = selectedItems
.map(item => mailboxes?.find(m => m.id === parseInt(item.id, 10))?.name)
.filter(Boolean) as string[];
setSelectedFolders(selected);
}, [mailboxes]);
const updateMutation = useMutation({
mutationFn: (data: Record<string, any>) => update_account(currentRow?.id ?? '', data),
onSuccess: handleSuccess,
onError: handleError
})
function handleSuccess() {
toast({
title: 'Account Sync Folders Updated',
description: 'Account has been successfully updated.',
action: <ToastAction altText="Close">Close</ToastAction>,
});
queryClient.invalidateQueries({ queryKey: ['account-list'] });
setIsSubmitting(false);
onOpenChange(false);
}
function handleError(error: AxiosError) {
const errorMessage = (error.response?.data as { message?: string })?.message ||
error.message ||
'Update failed, please try again later';
toast({
variant: "destructive",
title: 'Account Sync Folders Update Failed',
description: errorMessage as string,
action: <ToastAction altText="Try again">Try again</ToastAction>,
});
setIsSubmitting(false);
console.error(error);
}
const handleSubmit = async () => {
if (selectedFolders.length === 0) {
toast({
title: 'Error',
description: 'Please select at least one folder',
variant: 'destructive',
});
return;
}
setIsSubmitting(true);
updateMutation.mutate({
sync_folders: selectedFolders,
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Select Sync Folders</DialogTitle>
<DialogDescription>
Choose folders to sync for {currentRow.email}, Newly added folders will begin downloading during the next sync cycle.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center justify-end">
<div className="text-sm text-muted-foreground">
{selectedFolders.length} folder(s) selected
</div>
</div>
{isLoading && (
<div className="space-y-2">
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="h-8 w-full" />
))}
</div>
)}
{!isLoading && (
<TreeView
data={treeData}
multiple
expandAll
clickRowToSelect={false}
initialSelectedItemIds={initialSelectedItemIds}
onSelectItemsChange={handleSelectItems}
/>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting || isLoading}
>
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}