release: prepare v1.2.30
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
Docker Release / Create GitHub release (push) Blocked by required conditions
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
Docker Release / Create GitHub release (push) Blocked by required conditions
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
- 修复邮箱侧栏切换目录后,紧凑列表顶部仍可能显示上一个目录名称的问题;收件箱、草稿箱、已发送、已归档、已删除、星标邮件、垃圾邮件、自定义文件夹和标签现在会与当前列表保持一致。
|
||||||
|
- 切换邮箱目录时按当前视图重新生成列表区域,避免星标邮件等旧标题或旧操作状态残留到其他目录。
|
||||||
|
- 修复“已删除”目录批量删除仍执行“移入已删除”的问题;现在会永久删除所选邮件,并在操作前显示不可恢复的二次确认。
|
||||||
|
- 区分普通目录与“已删除”的删除语义:普通目录显示“移入已删除”,已删除目录显示“永久删除”。
|
||||||
|
- 统一批量工具栏、邮件详情、右键菜单和列表快捷按钮的删除名称、确认文案与完成提示,避免显示“已移动”但实际执行删除等文字不一致。
|
||||||
|
- 优化批量归档、移回收件箱、移入垃圾邮件和移入已删除后的提示,准确显示目标目录及处理数量。
|
||||||
|
- 后台邮箱管理中将账号默认邮箱固定置顶,其余子邮箱继续按 A-Z / 0-9 排序,方便快速识别账号主邮箱。
|
||||||
|
- 修正邮箱管理表头与账号行的网格结构,“权限管理”和“子邮箱”列标题及内容统一居中对齐。
|
||||||
|
- 保留默认邮箱不可停用、不可删除保护;普通子邮箱继续支持启用、停用和二次确认删除。
|
||||||
@@ -703,8 +703,12 @@ function MailboxesSection({ mailboxes, users, domains }: { mailboxes: MailboxTyp
|
|||||||
const remove = useMutation({ mutationFn: api.deleteMailbox, onSuccess: () => { setPendingConfirm(null); invalidateAdmin(qc); toast({ title: "邮箱已删除" }) }, onError: (e) => toast({ title: "删除失败", description: e.message }) })
|
const remove = useMutation({ mutationFn: api.deleteMailbox, onSuccess: () => { setPendingConfirm(null); invalidateAdmin(qc); toast({ title: "邮箱已删除" }) }, onError: (e) => toast({ title: "删除失败", description: e.message }) })
|
||||||
const keyword = query.trim().toLowerCase()
|
const keyword = query.trim().toLowerCase()
|
||||||
const knownOwnerIDs = new Set(users.map((item) => item.id))
|
const knownOwnerIDs = new Set(users.map((item) => item.id))
|
||||||
|
const compareMailboxes = (left: MailboxType, right: MailboxType) => {
|
||||||
|
if (left.primary !== right.primary) return left.primary ? -1 : 1
|
||||||
|
return left.address.localeCompare(right.address, "en", { sensitivity: "base" })
|
||||||
|
}
|
||||||
const mailboxGroups: Array<{ owner?: AdminUser; mailboxes: MailboxType[] }> = [
|
const mailboxGroups: Array<{ owner?: AdminUser; mailboxes: MailboxType[] }> = [
|
||||||
...users.slice().sort(compareAdminUsers).map((owner) => ({ owner, mailboxes: mailboxes.filter((mailbox) => mailbox.userId === owner.id).sort((left, right) => left.address.localeCompare(right.address, "en")) })),
|
...users.slice().sort(compareAdminUsers).map((owner) => ({ owner, mailboxes: mailboxes.filter((mailbox) => mailbox.userId === owner.id).sort(compareMailboxes) })),
|
||||||
...mailboxes.filter((mailbox) => !knownOwnerIDs.has(mailbox.userId)).map((mailbox) => ({ owner: undefined, mailboxes: [mailbox] })),
|
...mailboxes.filter((mailbox) => !knownOwnerIDs.has(mailbox.userId)).map((mailbox) => ({ owner: undefined, mailboxes: [mailbox] })),
|
||||||
]
|
]
|
||||||
.filter((group) => group.mailboxes.length > 0)
|
.filter((group) => group.mailboxes.length > 0)
|
||||||
@@ -727,8 +731,11 @@ function MailboxesSection({ mailboxes, users, domains }: { mailboxes: MailboxTyp
|
|||||||
<Input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索账号或邮箱" className="pl-9" />
|
<Input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索账号或邮箱" className="pl-9" />
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y overflow-hidden rounded-md border">
|
<div className="divide-y overflow-hidden rounded-md border">
|
||||||
<div className="hidden grid-cols-[minmax(0,1fr)_9rem_8rem] gap-4 bg-muted/40 px-11 py-2 text-xs font-medium text-muted-foreground sm:grid">
|
<div className="hidden grid-cols-[1rem_minmax(0,1fr)_9rem_8rem] gap-3 bg-muted/40 px-4 py-2 text-xs font-medium text-muted-foreground sm:grid">
|
||||||
<span>归属账号</span><span>权限管理</span><span>子邮箱</span>
|
<span aria-hidden="true" />
|
||||||
|
<span>归属账号</span>
|
||||||
|
<span className="text-center">权限管理</span>
|
||||||
|
<span className="text-center">子邮箱</span>
|
||||||
</div>
|
</div>
|
||||||
{mailboxGroups.map((group) => {
|
{mailboxGroups.map((group) => {
|
||||||
const ownerID = group.owner?.id || group.mailboxes[0].userId
|
const ownerID = group.owner?.id || group.mailboxes[0].userId
|
||||||
@@ -741,8 +748,8 @@ function MailboxesSection({ mailboxes, users, domains }: { mailboxes: MailboxTyp
|
|||||||
<div className="truncate font-medium">{group.owner ? accountPrimaryEmail(group.owner) : group.mailboxes[0].userEmail || "未知账号"}</div>
|
<div className="truncate font-medium">{group.owner ? accountPrimaryEmail(group.owner) : group.mailboxes[0].userEmail || "未知账号"}</div>
|
||||||
<div className="truncate text-xs text-muted-foreground">{group.owner?.displayName || "账号信息不可用"}</div>
|
<div className="truncate text-xs text-muted-foreground">{group.owner?.displayName || "账号信息不可用"}</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="hidden sm:block">{group.owner ? <RoleBadge user={group.owner} /> : "-"}</span>
|
<span className="hidden items-center justify-center sm:flex">{group.owner ? <RoleBadge user={group.owner} /> : "-"}</span>
|
||||||
<span className="flex items-center justify-end gap-2 text-sm tabular-nums text-muted-foreground"><span>{group.mailboxes.length} 个</span><span className="hidden text-xs lg:inline">查看邮箱</span></span>
|
<span className="flex items-center justify-center gap-2 text-sm tabular-nums text-muted-foreground"><span>{group.mailboxes.length} 个</span><span className="hidden text-xs lg:inline">查看邮箱</span></span>
|
||||||
</Button>
|
</Button>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="border-t bg-muted/20 px-4 py-2 sm:pl-11">
|
<div className="border-t bg-muted/20 px-4 py-2 sm:pl-11">
|
||||||
|
|||||||
+41
-15
@@ -486,7 +486,16 @@ export function MailPage() {
|
|||||||
},
|
},
|
||||||
onSettled: () => { qc.invalidateQueries({ queryKey: ["labels"] }); qc.invalidateQueries({ queryKey: ["messages"] }) },
|
onSettled: () => { qc.invalidateQueries({ queryKey: ["labels"] }); qc.invalidateQueries({ queryKey: ["messages"] }) },
|
||||||
})
|
})
|
||||||
const del = useMutation({ mutationFn: (id: string) => api.delete(id), onSuccess: async () => { setSelectedId(null); setPendingConfirm(null); await qc.invalidateQueries({ queryKey: ["messages"] }); await qc.invalidateQueries({ queryKey: ["folders"] }); await qc.invalidateQueries({ queryKey: ["mailboxes"] }); await qc.invalidateQueries({ queryKey: ["mail-stats"] }); await qc.invalidateQueries({ queryKey: ["labels"] }); toast({ title: "已删除" }) }, onError: (error) => toast({ title: "删除失败", description: error.message }) })
|
const del = useMutation({
|
||||||
|
mutationFn: ({ id }: { id: string; permanent: boolean }) => api.delete(id),
|
||||||
|
onSuccess: async (_, { permanent }) => {
|
||||||
|
setSelectedId(null)
|
||||||
|
setPendingConfirm(null)
|
||||||
|
await refreshMailData()
|
||||||
|
toast({ title: permanent ? "邮件已永久删除" : "邮件已移入已删除" })
|
||||||
|
},
|
||||||
|
onError: (error) => toast({ title: "删除失败", description: error.message }),
|
||||||
|
})
|
||||||
const move = useMutation({ mutationFn: ({ id, folder }: { id: string; folder: string }) => api.move(id, folder), onSuccess: async () => { setSelectedId(null); await qc.invalidateQueries({ queryKey: ["messages"] }); await qc.invalidateQueries({ queryKey: ["folders"] }); await qc.invalidateQueries({ queryKey: ["mailboxes"] }); await qc.invalidateQueries({ queryKey: ["mail-stats"] }); await qc.invalidateQueries({ queryKey: ["labels"] }); toast({ title: "已移动" }) }, onError: (error) => toast({ title: "移动失败", description: error.message }) })
|
const move = useMutation({ mutationFn: ({ id, folder }: { id: string; folder: string }) => api.move(id, folder), onSuccess: async () => { setSelectedId(null); await qc.invalidateQueries({ queryKey: ["messages"] }); await qc.invalidateQueries({ queryKey: ["folders"] }); await qc.invalidateQueries({ queryKey: ["mailboxes"] }); await qc.invalidateQueries({ queryKey: ["mail-stats"] }); await qc.invalidateQueries({ queryKey: ["labels"] }); toast({ title: "已移动" }) }, onError: (error) => toast({ title: "移动失败", description: error.message }) })
|
||||||
const cancelScheduledSend = useMutation({
|
const cancelScheduledSend = useMutation({
|
||||||
mutationFn: (item: ScheduledSend) => api.cancelScheduledSend(item.id),
|
mutationFn: (item: ScheduledSend) => api.cancelScheduledSend(item.id),
|
||||||
@@ -814,10 +823,19 @@ export function MailPage() {
|
|||||||
await Promise.all(ids.map((id) => api.star(id, starred)))
|
await Promise.all(ids.map((id) => api.star(id, starred)))
|
||||||
} else if (action === "delete") {
|
} else if (action === "delete") {
|
||||||
await Promise.all(ids.map((id) => api.delete(id)))
|
await Promise.all(ids.map((id) => api.delete(id)))
|
||||||
|
completionTitle = `已永久删除 ${ids.length} 封邮件`
|
||||||
} else {
|
} else {
|
||||||
const target = action === "archive" ? "Archive" : action === "inbox" ? "Inbox" : action === "trash" ? "Trash" : "Spam"
|
const target = action === "archive" ? "Archive" : action === "inbox" ? "Inbox" : action === "trash" ? "Trash" : "Spam"
|
||||||
const result = await api.bulkMove(ids, target)
|
const result = await api.bulkMove(ids, target)
|
||||||
completionTitle = result.ok ? result.message : "批量移动部分失败"
|
completionTitle = result.ok
|
||||||
|
? target === "Trash"
|
||||||
|
? `已将 ${result.moved} 封邮件移入已删除`
|
||||||
|
: target === "Spam"
|
||||||
|
? `已将 ${result.moved} 封邮件移入垃圾邮件`
|
||||||
|
: target === "Inbox"
|
||||||
|
? `已将 ${result.moved} 封邮件移回收件箱`
|
||||||
|
: `已归档 ${result.moved} 封邮件`
|
||||||
|
: "批量移动部分失败"
|
||||||
completionDescription = result.ok ? undefined : result.message
|
completionDescription = result.ok ? undefined : result.message
|
||||||
}
|
}
|
||||||
if (selectedId && ids.includes(selectedId)) setSelectedId(null)
|
if (selectedId && ids.includes(selectedId)) setSelectedId(null)
|
||||||
@@ -849,11 +867,14 @@ export function MailPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function confirmDeleteMessage(message: MailMessage) {
|
function confirmDeleteMessage(message: MailMessage) {
|
||||||
|
const permanent = message.folder === "Trash"
|
||||||
setPendingConfirm({
|
setPendingConfirm({
|
||||||
title: "删除这封邮件?",
|
title: permanent ? "永久删除这封邮件?" : "将这封邮件移入已删除?",
|
||||||
description: `邮件“${message.subject || "无主题"}”将被删除。`,
|
description: permanent
|
||||||
confirmText: "删除邮件",
|
? `邮件“${message.subject || "无主题"}”将被永久删除,且无法恢复。`
|
||||||
onConfirm: () => del.mutate(message.id),
|
: `邮件“${message.subject || "无主题"}”将移入已删除。`,
|
||||||
|
confirmText: permanent ? "永久删除" : "移入已删除",
|
||||||
|
onConfirm: () => del.mutate({ id: message.id, permanent }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function openCompose(draft?: ComposeDraft) {
|
function openCompose(draft?: ComposeDraft) {
|
||||||
@@ -1669,7 +1690,9 @@ export function MailPage() {
|
|||||||
<MailQueryFailure error={mailMessagesError} onRetry={() => { void refetchMessages() }} />
|
<MailQueryFailure error={mailMessagesError} onRetry={() => { void refetchMessages() }} />
|
||||||
) : compactMailLayout ? (
|
) : compactMailLayout ? (
|
||||||
<CompactMailView
|
<CompactMailView
|
||||||
|
key={`${mailView}:${mailView === "folder" ? folder : mailView === "label" ? selectedLabelId : selectedExternalAccountId}`}
|
||||||
title={viewTitle}
|
title={viewTitle}
|
||||||
|
currentFolder={mailView === "folder" ? folder : undefined}
|
||||||
icon={mailView === "label" && selectedLabel ? <Badge variant="outline" className="gap-1.5 rounded-md font-normal"><span className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: labelDotColor(selectedLabel) }} />{selectedLabel.name}</Badge> : undefined}
|
icon={mailView === "label" && selectedLabel ? <Badge variant="outline" className="gap-1.5 rounded-md font-normal"><span className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: labelDotColor(selectedLabel) }} />{selectedLabel.name}</Badge> : undefined}
|
||||||
messages={visibleMessages}
|
messages={visibleMessages}
|
||||||
total={allMessages.length}
|
total={allMessages.length}
|
||||||
@@ -1778,12 +1801,12 @@ export function MailPage() {
|
|||||||
{selectedCountOnPage > 0 && canOrganizeMail && mailView !== "unknown" && (
|
{selectedCountOnPage > 0 && canOrganizeMail && mailView !== "unknown" && (
|
||||||
<div className="flex min-h-10 shrink-0 items-center gap-3 border-b px-3 py-1.5">
|
<div className="flex min-h-10 shrink-0 items-center gap-3 border-b px-3 py-1.5">
|
||||||
<span className="shrink-0 text-[13px] text-muted-foreground">已选 {selectedCountOnPage} 封</span>
|
<span className="shrink-0 text-[13px] text-muted-foreground">已选 {selectedCountOnPage} 封</span>
|
||||||
<BulkActionToolbar pending={bulkPending} currentFolder={folder} folders={folders.data?.items || []} readAction={bulkReadAction} onAction={runBulkAction} onMoveToFolder={runBulkMoveToFolder} />
|
<BulkActionToolbar pending={bulkPending} currentFolder={mailView === "folder" ? folder : undefined} folders={folders.data?.items || []} readAction={bulkReadAction} onAction={runBulkAction} onMoveToFolder={runBulkMoveToFolder} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ScrollArea className="min-h-0 flex-1">
|
<ScrollArea className="min-h-0 flex-1">
|
||||||
{mailMessagesLoading && <MessageSkeleton />}
|
{mailMessagesLoading && <MessageSkeleton />}
|
||||||
{visibleMessages.map((m) => <MessageRow key={m.id} message={m} active={selectedId === m.id} checked={compactSelectedIds.includes(m.id)} scheduled={scheduledDraftIds.has(m.id)} onCheckedChange={(checked) => toggleCompactSelect(m.id, checked)} onClick={() => openMessage(m.id)} onContextMenu={(event) => openMessageContextMenu(event, m)} onStar={() => star.mutate({ id: m.id, starred: !m.isStarred })} onArchive={() => move.mutate({ id: m.id, folder: m.folder === "Archive" ? "Inbox" : "Archive" })} onTrash={() => move.mutate({ id: m.id, folder: "Trash" })} onToggleRead={() => markRead.mutate({ id: m.id, read: !m.isRead })} canOrganize={canOrganizeMail && mailView !== "unknown"} />)}
|
{visibleMessages.map((m) => <MessageRow key={m.id} message={m} active={selectedId === m.id} checked={compactSelectedIds.includes(m.id)} scheduled={scheduledDraftIds.has(m.id)} onCheckedChange={(checked) => toggleCompactSelect(m.id, checked)} onClick={() => openMessage(m.id)} onContextMenu={(event) => openMessageContextMenu(event, m)} onStar={() => star.mutate({ id: m.id, starred: !m.isStarred })} onArchive={() => move.mutate({ id: m.id, folder: m.folder === "Archive" ? "Inbox" : "Archive" })} onTrash={() => m.folder === "Trash" ? confirmDeleteMessage(m) : move.mutate({ id: m.id, folder: "Trash" })} onToggleRead={() => markRead.mutate({ id: m.id, read: !m.isRead })} canOrganize={canOrganizeMail && mailView !== "unknown"} />)}
|
||||||
{!mailMessagesLoading && visibleMessages.length === 0 && <div className="grid min-h-[170px] place-items-center px-8 py-12 text-center text-base text-muted-foreground">{emptyMessage}</div>}
|
{!mailMessagesLoading && visibleMessages.length === 0 && <div className="grid min-h-[170px] place-items-center px-8 py-12 text-center text-base text-muted-foreground">{emptyMessage}</div>}
|
||||||
{!mailMessagesLoading && hasMoreMessages && (
|
{!mailMessagesLoading && hasMoreMessages && (
|
||||||
<div className="border-b p-4 text-center">
|
<div className="border-b p-4 text-center">
|
||||||
@@ -1827,7 +1850,7 @@ export function MailPage() {
|
|||||||
) : (
|
) : (
|
||||||
<Button variant="outline" size="sm" onClick={() => move.mutate({ id: selected.id, folder: "Archive" })}>归档</Button>
|
<Button variant="outline" size="sm" onClick={() => move.mutate({ id: selected.id, folder: "Archive" })}>归档</Button>
|
||||||
))}
|
))}
|
||||||
{mailView !== "external" && mailView !== "unknown" && canOrganizeMail && <Button variant="destructive" size="sm" onClick={() => confirmDeleteMessage(selected)}>删除</Button>}
|
{mailView !== "external" && mailView !== "unknown" && canOrganizeMail && <Button variant="destructive" size="sm" onClick={() => confirmDeleteMessage(selected)}>{selected.folder === "Trash" ? "永久删除" : "移入已删除"}</Button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MessageMetaPanel
|
<MessageMetaPanel
|
||||||
@@ -2574,6 +2597,7 @@ type BulkAction = "read" | "unread" | "star" | "unstar" | "archive" | "inbox" |
|
|||||||
|
|
||||||
function BulkActionToolbar({ pending, currentFolder, folders = [], readAction = "read", onAction, onMoveToFolder }: { pending: boolean; currentFolder?: string; folders?: MailFolder[]; readAction?: "read" | "unread"; onAction: (action: BulkAction) => void; onMoveToFolder?: (folderName: string) => void }) {
|
function BulkActionToolbar({ pending, currentFolder, folders = [], readAction = "read", onAction, onMoveToFolder }: { pending: boolean; currentFolder?: string; folders?: MailFolder[]; readAction?: "read" | "unread"; onAction: (action: BulkAction) => void; onMoveToFolder?: (folderName: string) => void }) {
|
||||||
const buttonClass = "h-7 w-7 rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
const buttonClass = "h-7 w-7 rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
|
const permanentlyDelete = currentFolder === "Trash"
|
||||||
const archiveAction: BulkAction = currentFolder === "Archive" ? "inbox" : "archive"
|
const archiveAction: BulkAction = currentFolder === "Archive" ? "inbox" : "archive"
|
||||||
const archiveLabel = currentFolder === "Archive" ? "移回收件箱" : "归档"
|
const archiveLabel = currentFolder === "Archive" ? "移回收件箱" : "归档"
|
||||||
const movableFolders = folders.filter((folder) => folder.name !== currentFolder && folder.name !== "Drafts")
|
const movableFolders = folders.filter((folder) => folder.name !== currentFolder && folder.name !== "Drafts")
|
||||||
@@ -2602,7 +2626,7 @@ function BulkActionToolbar({ pending, currentFolder, folders = [], readAction =
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
<Button type="button" variant="ghost" size="icon" className={cn(buttonClass, "text-destructive hover:bg-destructive/10 hover:text-destructive")} disabled={pending} onClick={() => onAction("trash")} title="移入已删除" aria-label="移入已删除">
|
<Button type="button" variant="ghost" size="icon" className={cn(buttonClass, "text-destructive hover:bg-destructive/10 hover:text-destructive")} disabled={pending} onClick={() => onAction(permanentlyDelete ? "delete" : "trash")} title={permanentlyDelete ? "永久删除" : "移入已删除"} aria-label={permanentlyDelete ? "永久删除" : "移入已删除"}>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2784,7 +2808,7 @@ function MessageContextMenu({ state, labels, folders, canSend, canOrganize, canM
|
|||||||
{canOrganize && (
|
{canOrganize && (
|
||||||
<>
|
<>
|
||||||
<div className="-mx-1 my-1 h-px bg-border" />
|
<div className="-mx-1 my-1 h-px bg-border" />
|
||||||
{item("删除", <Trash2 className="h-4 w-4" />, "delete", true)}
|
{item(message.folder === "Trash" ? "永久删除" : "移入已删除", <Trash2 className="h-4 w-4" />, "delete", true)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2882,6 +2906,7 @@ function CreateFolderDialog({ open, pending, onOpenChange, onCreate }: { open: b
|
|||||||
|
|
||||||
function CompactMailView({
|
function CompactMailView({
|
||||||
title,
|
title,
|
||||||
|
currentFolder,
|
||||||
icon,
|
icon,
|
||||||
messages,
|
messages,
|
||||||
total,
|
total,
|
||||||
@@ -2925,6 +2950,7 @@ function CompactMailView({
|
|||||||
tools,
|
tools,
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
|
currentFolder?: string
|
||||||
icon?: React.ReactNode
|
icon?: React.ReactNode
|
||||||
messages: MailMessage[]
|
messages: MailMessage[]
|
||||||
total: number
|
total: number
|
||||||
@@ -3016,7 +3042,7 @@ function CompactMailView({
|
|||||||
{selectedIds.length > 0 ? (
|
{selectedIds.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<span className="hidden text-xs text-muted-foreground min-[380px]:inline">已选 {selectedIds.length} 封</span>
|
<span className="hidden text-xs text-muted-foreground min-[380px]:inline">已选 {selectedIds.length} 封</span>
|
||||||
{canOrganize && <BulkActionToolbar pending={bulkPending} onAction={onBulkAction} />}
|
{canOrganize && <BulkActionToolbar pending={bulkPending} currentFolder={currentFolder} onAction={onBulkAction} />}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -3127,7 +3153,7 @@ function CompactMessageDetail({
|
|||||||
{canOrganize && <DropdownMenuItem onSelect={() => onStar(selected)}><Star className={cn("h-4 w-4", selected.isStarred && "fill-yellow-400 text-yellow-500")} />{selected.isStarred ? "取消星标" : "添加星标"}</DropdownMenuItem>}
|
{canOrganize && <DropdownMenuItem onSelect={() => onStar(selected)}><Star className={cn("h-4 w-4", selected.isStarred && "fill-yellow-400 text-yellow-500")} />{selected.isStarred ? "取消星标" : "添加星标"}</DropdownMenuItem>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{canOrganize && <DropdownMenuItem onSelect={() => onDelete(selected)} className="text-destructive"><Trash2 className="h-4 w-4" />删除</DropdownMenuItem>}
|
{canOrganize && <DropdownMenuItem onSelect={() => onDelete(selected)} className="text-destructive"><Trash2 className="h-4 w-4" />{selected.folder === "Trash" ? "永久删除" : "移入已删除"}</DropdownMenuItem>}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
@@ -3147,7 +3173,7 @@ function CompactMessageDetail({
|
|||||||
{selected && canOrganize && <Button variant="outline" size="sm" onClick={() => onStar(selected)}><Star className={cn("h-4 w-4", selected.isStarred && "fill-yellow-400 text-yellow-500")} />{selected.isStarred ? "取消星标" : "添加星标"}</Button>}
|
{selected && canOrganize && <Button variant="outline" size="sm" onClick={() => onStar(selected)}><Star className={cn("h-4 w-4", selected.isStarred && "fill-yellow-400 text-yellow-500")} />{selected.isStarred ? "取消星标" : "添加星标"}</Button>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{selected && canOrganize && <Button variant="outline" size="sm" onClick={() => onDelete(selected)}><Trash2 className="h-4 w-4" />删除</Button>}
|
{selected && canOrganize && <Button variant="outline" size="sm" onClick={() => onDelete(selected)}><Trash2 className="h-4 w-4" />{selected.folder === "Trash" ? "永久删除" : "移入已删除"}</Button>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="ghost" size="sm" disabled={!previousMessage} onClick={() => previousMessage && onSelect(previousMessage.id)}>上一封</Button>
|
<Button variant="ghost" size="sm" disabled={!previousMessage} onClick={() => previousMessage && onSelect(previousMessage.id)}>上一封</Button>
|
||||||
@@ -3782,7 +3808,7 @@ function MessageRow({
|
|||||||
<Button type="button" variant="ghost" size="icon" aria-label={archiveLabel} title={archiveLabel} className={quickButtonClass} onClick={(event) => { event.stopPropagation(); onArchive() }}>
|
<Button type="button" variant="ghost" size="icon" aria-label={archiveLabel} title={archiveLabel} className={quickButtonClass} onClick={(event) => { event.stopPropagation(); onArchive() }}>
|
||||||
<Archive className="h-3.5 w-3.5" />
|
<Archive className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="ghost" size="icon" aria-label="移入已删除" title="移入已删除" className={quickButtonClass} onClick={(event) => { event.stopPropagation(); onTrash() }}>
|
<Button type="button" variant="ghost" size="icon" aria-label={message.folder === "Trash" ? "永久删除" : "移入已删除"} title={message.folder === "Trash" ? "永久删除" : "移入已删除"} className={quickButtonClass} onClick={(event) => { event.stopPropagation(); onTrash() }}>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="ghost" size="icon" aria-label={message.isRead ? "标为未读" : "标为已读"} title={message.isRead ? "标为未读" : "标为已读"} className={quickButtonClass} onClick={(event) => { event.stopPropagation(); onToggleRead() }}>
|
<Button type="button" variant="ghost" size="icon" aria-label={message.isRead ? "标为未读" : "标为已读"} title={message.isRead ? "标为未读" : "标为已读"} className={quickButtonClass} onClick={(event) => { event.stopPropagation(); onToggleRead() }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user