feat(mailbox): support mailbox cleanup #96

This commit is contained in:
rustmailer
2026-01-05 18:27:40 +08:00
parent c69ada32ef
commit e56fe5ebea
29 changed files with 455 additions and 55 deletions
@@ -0,0 +1,105 @@
//
// 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 { IconAlertTriangle } from '@tabler/icons-react';
import { toast } from '@/hooks/use-toast';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { ConfirmDialog } from '@/components/confirm-dialog';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMailboxContext } from '../context';
import { useTranslation } from 'react-i18next';
import { delete_mailbox } from '@/api/mailbox/api';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function MailBoxDeleteDialog({ open, onOpenChange }: Props) {
const queryClient = useQueryClient();
const { selectedAccountId, deleteMailboxId, setDeleteMailboxId } = useMailboxContext();
const { t } = useTranslation();
const deleteMutation = useMutation({
mutationFn: ({ accountId, mailboxId }: { accountId: number; mailboxId: string }) =>
delete_mailbox(accountId, mailboxId),
retry: false,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['account-mailboxes', `${selectedAccountId}`] });
onOpenChange(false);
setDeleteMailboxId(undefined);
toast({
title: t('mailbox.deleteMailboxDialog.successTitle'),
description: t('mailbox.deleteMailboxDialog.successDesc'),
});
},
onError: (error: any) => {
toast({
title: t('mailbox.deleteMailboxDialog.errorTitle'),
description: error.message || "Delete failed",
variant: 'destructive',
});
},
});
const handleDelete = () => {
if (selectedAccountId && deleteMailboxId) {
deleteMutation.mutate({
accountId: selectedAccountId,
mailboxId: deleteMailboxId
});
}
};
const isLoading = deleteMutation.isPending;
return (
<ConfirmDialog
open={open}
onOpenChange={(isOpen) => {
onOpenChange(isOpen);
if (!isOpen) setDeleteMailboxId(undefined);
}}
handleConfirm={handleDelete}
className="max-w-xl"
isLoading={isLoading}
title={
<span className="text-destructive">
<IconAlertTriangle
className="mr-1 inline-block stroke-destructive"
size={18}
/>{' '}
{t('mailbox.deleteMailboxDialog.title')}
</span>
}
desc={
<div className="space-y-4">
<p className="mb-2">
{t('mailbox.deleteMailboxDialog.desc')}
</p>
<Alert variant="destructive">
<AlertTitle>{t('mailbox.deleteMailboxDialog.warningTitle')}</AlertTitle>
<AlertDescription>{t('mailbox.deleteMailboxDialog.warningDesc')}</AlertDescription>
</Alert>
</div>
}
confirmText={t('mailbox.deleteMailboxDialog.confirm')}
destructive
/>
);
}
+71 -28
View File
@@ -49,8 +49,12 @@ import { styled } from "@mui/material/styles"
import { animated, useSpring } from "@react-spring/web"
import { TransitionProps } from "@mui/material/transitions"
import Collapse from "@mui/material/Collapse"
import { FolderIcon } from "lucide-react"
import { FolderIcon, MoreVertical, Trash2 } from "lucide-react"
import { RestoreMessageDialog } from "./restore-message-dialog"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { useTranslation } from "react-i18next"
import { MailBoxDeleteDialog } from "./delete-mailbox-dialog"
interface MailProps {
@@ -79,15 +83,14 @@ const useListMessages = ({ accountId, mailboxId, page, page_size }: ListMessages
});
};
interface CustomLabelProps {
exists?: number;
attributes?: { attr: string; extension: string | null }[],
children: React.ReactNode;
id: string;
icon?: React.ElementType;
expandable?: boolean;
onDelete: (id: string) => void;
}
function CustomLabel({
@@ -95,8 +98,11 @@ function CustomLabel({
exists,
attributes,
children,
id,
onDelete,
...other
}: CustomLabelProps) {
const { t } = useTranslation()
return (
<TreeItemLabel
{...other}
@@ -109,27 +115,39 @@ function CustomLabel({
<span className="font-medium text-sm text-inherit">
{children}
</span>
{/* <div className="flex gap-2 ml-auto mr-3 opacity-70 text-xs">
{attributes?.map((attr) => {
const text =
attr.attr === 'Extension'
? attr.extension
: attr.attr;
return (
<span key={attr.attr} className="text-inherit">
{text}
</span>
);
})}
<div className="ml-auto flex items-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 p-0 hover:bg-muted rounded-md"
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-20">
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={(e) => {
e.stopPropagation();
}}
onSelect={(e) => {
e.preventDefault();
onDelete(id);
}}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>{t('common.delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{exists !== undefined && (
<span
className="text-sm opacity-60 min-w-[40px] text-right text-inherit"
>
{exists}
</span>
)} */}
</TreeItemLabel>
);
}
@@ -172,6 +190,8 @@ export function Mail({
const [pageSize, setPageSize] = React.useState(30);
const [deleteIds, setDeleteIds] = React.useState<Set<number>>(() => new Set());
const [selected, setSelected] = React.useState<Set<number>>(() => new Set());
const [deleteMailboxId, setDeleteMailboxId] = React.useState<string | undefined>(undefined);
const { theme } = useTheme()
const { data: mailboxes, isLoading: isMailboxesLoading } = useQuery({
@@ -225,6 +245,11 @@ export function Mail({
}
};
const handleDeleteClick = (id: string) => {
setDeleteMailboxId(id);
setOpen('delete');
};
const CustomTreeItem = React.useMemo(() => {
return React.forwardRef(function CustomTreeItem(
props: CustomTreeItemProps,
@@ -257,6 +282,8 @@ export function Mail({
<CustomLabel
{...getLabelProps({
exists: item.exists,
id: item.id,
onDelete: handleDeleteClick,
attributes: item.attributes,
expandable: status.expandable && status.expanded,
})}
@@ -270,11 +297,22 @@ export function Mail({
});
}, [theme]);
return (
<MailboxProvider value={{ open, setOpen, currentMailbox: selectedMailbox, selectedAccountId, setCurrentMailbox: setSelectedMailbox, currentEnvelope: selectedEvelope, setCurrentEnvelope: setSelectedEvelope, deleteIds, setDeleteIds, selected, setSelected }}>
<MailboxProvider value={{
open,
setOpen,
currentMailbox: selectedMailbox,
selectedAccountId,
setCurrentMailbox: setSelectedMailbox,
currentEnvelope: selectedEvelope,
setCurrentEnvelope: setSelectedEvelope,
deleteIds,
setDeleteIds,
selected,
setSelected,
deleteMailboxId,
setDeleteMailboxId
}}>
<TooltipProvider delayDuration={0}>
<ResizablePanelGroup
direction="horizontal"
@@ -409,6 +447,11 @@ export function Mail({
open={open === 'restore'}
onOpenChange={() => setOpen('restore')}
/>
<MailBoxDeleteDialog
key='mailbox-delete'
open={open === 'delete'}
onOpenChange={() => setOpen('delete')}
/>
</MailboxProvider >
)
+3 -1
View File
@@ -21,7 +21,7 @@ import React from 'react'
import { MailboxData } from '@/api/mailbox/api'
import { EmailEnvelope } from '@/api'
export type MailboxDialogType = 'mailbox' | 'display' | 'move-to-trash' | 'filters' | 'restore'
export type MailboxDialogType = 'mailbox' | 'display' | 'move-to-trash' | 'filters' | 'restore' | 'delete'
interface MailboxContextType {
open: MailboxDialogType | null
@@ -30,6 +30,8 @@ interface MailboxContextType {
currentMailbox: MailboxData | undefined
currentEnvelope: EmailEnvelope | undefined
setCurrentMailbox: React.Dispatch<React.SetStateAction<MailboxData | undefined>>
deleteMailboxId: string | undefined,
setDeleteMailboxId: React.Dispatch<React.SetStateAction<string | undefined>>
setCurrentEnvelope: React.Dispatch<React.SetStateAction<EmailEnvelope | undefined>>
deleteIds: Set<number>
setDeleteIds: React.Dispatch<React.SetStateAction<Set<number>>>