release: prepare v1.2.27

This commit is contained in:
zxyszx
2026-08-11 23:40:55 +08:00
parent ce98978ebd
commit 2dc609322d
3 changed files with 38 additions and 15 deletions
+6
View File
@@ -0,0 +1,6 @@
- 修复收信规则选择“移动到自定义文件夹”后立即跳回归档的问题。
- 移除移动动作右侧显示 `Archive` 等英文内部值的旧输入框,避免系统值与中文选项重复出现。
- 将移动目标整合为一个文件夹选择器,系统文件夹统一显示中文名称。
- 文件夹选择器新增“系统文件夹”和“自定义文件夹”分组,自动列出当前邮箱或全部邮箱中已经创建的自定义文件夹。
- 选择 Netflix、amazon 等自定义文件夹后会直接保存真实文件夹名称,不再错误保存为归档。
- 已验证规则创建、列表摘要和后端数据均保留所选自定义文件夹。
+1 -1
View File
@@ -1 +1 @@
1.2.26 1.2.27
+31 -14
View File
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useNavigate, useSearchParams } from "react-router-dom" import { useNavigate, useSearchParams } from "react-router-dom"
import { ArrowLeft, BarChart3, Ban, Bell, BellOff, BookOpen, ChevronDown, ChevronUp, Clock3, Code2, Contact, Copy, HardDrive, Image, Info, KeyRound, LogOut, Mail, MailCheck, MailX, Moon, PanelLeftOpen, PencilLine, PlayCircle, Plus, RefreshCcw, Search, SendHorizontal, Settings, ShieldCheck, SlidersHorizontal, Sun, Trash2, Users, X } from "lucide-react" import { ArrowLeft, BarChart3, Ban, Bell, BellOff, BookOpen, ChevronDown, ChevronUp, Clock3, Code2, Contact, Copy, HardDrive, Image, Info, KeyRound, LogOut, Mail, MailCheck, MailX, Moon, PanelLeftOpen, PencilLine, PlayCircle, Plus, RefreshCcw, Search, SendHorizontal, Settings, ShieldCheck, SlidersHorizontal, Sun, Trash2, Users, X } from "lucide-react"
import { QRCodeSVG } from "qrcode.react" import { QRCodeSVG } from "qrcode.react"
import { api, APIToken, ExternalImapAccount, ExternalImapAccountPayload, ExternalImapFolder, ExternalImapOAuthProvider, ExternalImapStorageMode, ExternalImapSyncRun, ExternalImapTlsMode, ForwardingSettings, ForwardingVerifiedEmail, MailLabel, MailRule, MailRuleAction, MailRuleCondition, Mailbox, MailboxApplyOptions, MailSignature, MailStats, PermissionLimits } from "@/lib/api" import { api, APIToken, ExternalImapAccount, ExternalImapAccountPayload, ExternalImapFolder, ExternalImapOAuthProvider, ExternalImapStorageMode, ExternalImapSyncRun, ExternalImapTlsMode, ForwardingSettings, ForwardingVerifiedEmail, MailFolder, MailLabel, MailRule, MailRuleAction, MailRuleCondition, Mailbox, MailboxApplyOptions, MailSignature, MailStats, PermissionLimits } from "@/lib/api"
import { cn, formatBytes } from "@/lib/utils" import { cn, formatBytes } from "@/lib/utils"
import { applyTheme, getInitialTheme } from "@/lib/theme" import { applyTheme, getInitialTheme } from "@/lib/theme"
import { DisplayMode, useDisplayMode } from "@/lib/display-mode" import { DisplayMode, useDisplayMode } from "@/lib/display-mode"
@@ -23,7 +23,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet" import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { ConfirmDialog } from "@/components/confirm-dialog" import { ConfirmDialog } from "@/components/confirm-dialog"
@@ -2292,7 +2292,10 @@ function RuleDialog({ open, onOpenChange, mailboxes, labels, verifiedEmails, pen
const [stopProcessing, setStopProcessing] = React.useState(false) const [stopProcessing, setStopProcessing] = React.useState(false)
const selectedMailboxId = mailboxId === "all" ? "" : mailboxId const selectedMailboxId = mailboxId === "all" ? "" : mailboxId
const labelQuery = useQuery({ queryKey: ["labels", "rule-dialog", selectedMailboxId], queryFn: () => api.labels(selectedMailboxId), enabled: !!selectedMailboxId }) const labelQuery = useQuery({ queryKey: ["labels", "rule-dialog", selectedMailboxId], queryFn: () => api.labels(selectedMailboxId), enabled: !!selectedMailboxId })
const folderQueryMailboxId = selectedMailboxId || "all"
const folderQuery = useQuery({ queryKey: ["folders", "rule-dialog", folderQueryMailboxId], queryFn: () => api.folders(folderQueryMailboxId), enabled: open })
const availableLabels = selectedMailboxId ? (labelQuery.data?.items || []) : labels const availableLabels = selectedMailboxId ? (labelQuery.data?.items || []) : labels
const availableFolders = folderQuery.data?.items || []
React.useEffect(() => { React.useEffect(() => {
if (!open) return if (!open) return
@@ -2383,7 +2386,7 @@ function RuleDialog({ open, onOpenChange, mailboxes, labels, verifiedEmails, pen
<SelectTrigger className="h-11 md:h-9"><SelectValue /></SelectTrigger> <SelectTrigger className="h-11 md:h-9"><SelectValue /></SelectTrigger>
<SelectContent>{(Object.keys(ruleActionLabels) as MailRuleAction["type"][]).map((value) => <SelectItem key={value} value={value}>{ruleActionLabels[value]}</SelectItem>)}</SelectContent> <SelectContent>{(Object.keys(ruleActionLabels) as MailRuleAction["type"][]).map((value) => <SelectItem key={value} value={value}>{ruleActionLabels[value]}</SelectItem>)}</SelectContent>
</Select> </Select>
<div className="min-w-0"><RuleActionValue action={action} labels={availableLabels} verifiedEmails={verifiedEmails} onChange={(patch) => updateAction(index, patch)} /></div> <div className="min-w-0"><RuleActionValue action={action} folders={availableFolders} labels={availableLabels} verifiedEmails={verifiedEmails} onChange={(patch) => updateAction(index, patch)} /></div>
<div className="flex justify-end gap-2 md:contents"> <div className="flex justify-end gap-2 md:contents">
<Button type="button" variant="ghost" size="icon" className="size-11 text-muted-foreground md:size-9" aria-label={`删除第 ${index + 1} 个操作`} title="删除操作" onClick={() => removeAction(index)} disabled={actions.length === 1}><X className="h-4 w-4" /></Button> <Button type="button" variant="ghost" size="icon" className="size-11 text-muted-foreground md:size-9" aria-label={`删除第 ${index + 1} 个操作`} title="删除操作" onClick={() => removeAction(index)} disabled={actions.length === 1}><X className="h-4 w-4" /></Button>
<Button type="button" variant="outline" size="icon" className="size-11 md:size-9" aria-label="添加操作" title="添加操作" onClick={addAction}><Plus className="h-4 w-4" /></Button> <Button type="button" variant="outline" size="icon" className="size-11 md:size-9" aria-label="添加操作" title="添加操作" onClick={addAction}><Plus className="h-4 w-4" /></Button>
@@ -2414,7 +2417,7 @@ function RuleDialog({ open, onOpenChange, mailboxes, labels, verifiedEmails, pen
) )
} }
function RuleActionValue({ action, labels, verifiedEmails, onChange }: { action: MailRuleAction; labels: MailLabel[]; verifiedEmails: string[]; onChange: (patch: Partial<MailRuleAction>) => void }) { function RuleActionValue({ action, folders, labels, verifiedEmails, onChange }: { action: MailRuleAction; folders: MailFolder[]; labels: MailLabel[]; verifiedEmails: string[]; onChange: (patch: Partial<MailRuleAction>) => void }) {
if (action.type === "label") { if (action.type === "label") {
if (labels.length > 0) { if (labels.length > 0) {
return ( return (
@@ -2428,20 +2431,26 @@ function RuleActionValue({ action, labels, verifiedEmails, onChange }: { action:
} }
if (action.type === "move") { if (action.type === "move") {
const value = action.value || "Archive" const value = action.value || "Archive"
const customFolders = ruleCustomFolders(folders, value)
return ( return (
<div className="grid gap-2 md:grid-cols-[180px_minmax(0,1fr)]"> <Select value={value} onValueChange={(next) => onChange({ value: next })}>
<Select value={commonRuleFolders.includes(value) ? value : "__custom"} onValueChange={(next) => onChange({ value: next === "__custom" ? "" : next })}> <SelectTrigger className="h-11 md:h-9"><SelectValue placeholder="选择文件夹" /></SelectTrigger>
<SelectTrigger><SelectValue /></SelectTrigger> <SelectContent>
<SelectContent> <SelectGroup>
<SelectLabel className="text-xs font-medium text-muted-foreground"></SelectLabel>
<SelectItem value="Inbox"></SelectItem> <SelectItem value="Inbox"></SelectItem>
<SelectItem value="Archive"></SelectItem> <SelectItem value="Archive"></SelectItem>
<SelectItem value="Spam"></SelectItem> <SelectItem value="Spam"></SelectItem>
<SelectItem value="Trash"></SelectItem> <SelectItem value="Trash"></SelectItem>
<SelectItem value="__custom"></SelectItem> </SelectGroup>
</SelectContent> <SelectSeparator />
</Select> <SelectGroup>
<Input value={value} onChange={(event) => onChange({ value: event.target.value })} placeholder="输入或选择文件夹名" /> <SelectLabel className="text-xs font-medium text-muted-foreground"></SelectLabel>
</div> {customFolders.map((folder) => <SelectItem key={folder} value={folder}>{folder}</SelectItem>)}
{customFolders.length === 0 && <div className="px-2 py-2 text-sm text-muted-foreground"></div>}
</SelectGroup>
</SelectContent>
</Select>
) )
} }
if (action.type === "forward") { if (action.type === "forward") {
@@ -2450,6 +2459,14 @@ function RuleActionValue({ action, labels, verifiedEmails, onChange }: { action:
return <Input value="无需填写" readOnly /> return <Input value="无需填写" readOnly />
} }
function ruleCustomFolders(folders: MailFolder[], selectedValue: string) {
const names = folders
.map((folder) => folder.name.trim())
.filter((name) => name && !commonRuleFolders.some((systemName) => systemName.toLowerCase() === name.toLowerCase()) && !["Sent", "Drafts"].some((systemName) => systemName.toLowerCase() === name.toLowerCase()))
if (selectedValue && !commonRuleFolders.includes(selectedValue) && !names.some((name) => name.toLowerCase() === selectedValue.toLowerCase())) names.push(selectedValue)
return Array.from(new Set(names)).sort((a, b) => a.localeCompare(b, "zh-CN", { numeric: true, sensitivity: "base" }))
}
function RuleForwardTargets({ value, emails, onChange }: { value: string; emails: string[]; onChange: (value: string) => void }) { function RuleForwardTargets({ value, emails, onChange }: { value: string; emails: string[]; onChange: (value: string) => void }) {
const selected = React.useMemo(() => verifiedRuleForwardTargets(value, emails), [emails, value]) const selected = React.useMemo(() => verifiedRuleForwardTargets(value, emails), [emails, value])
return <ForwardingTargetPicker emails={emails} selected={selected} onChange={(targets) => onChange(targets.join(", "))} placement="top" /> return <ForwardingTargetPicker emails={emails} selected={selected} onChange={(targets) => onChange(targets.join(", "))} placement="top" />