"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { useTranslations } from "next-intl"; import { SettingsSection, ToggleSwitch } from "./settings-section"; import { Button } from "@/components/ui/button"; import { FilterRuleModal } from "@/components/filters/filter-rule-modal"; import { SieveEditorModal } from "@/components/filters/sieve-editor-modal"; import { useFilterStore } from "@/stores/filter-store"; import { useAuthStore } from "@/stores/auth-store"; import { useEmailStore } from "@/stores/email-store"; import { toast } from "@/stores/toast-store"; import type { FilterRule } from "@/lib/jmap/sieve-types"; import { Plus, GripVertical, X, Code, AlertTriangle, Loader2, Filter, RotateCcw, } from "lucide-react"; function RuleSummary({ rule }: { rule: FilterRule }) { const t = useTranslations("settings.filters"); const conditionSummary = rule.conditions .slice(0, 2) .map((c) => { const field = t(`condition_fields.${c.field}`); const comparator = t(`comparators.${c.comparator}`); return `${field} ${comparator} "${c.value}"`; }) .join(rule.matchType === "all" ? ` ${t("and")} ` : ` ${t("or")} `); const extra = rule.conditions.length > 2 ? ` (+${rule.conditions.length - 2})` : ""; const actionSummary = rule.actions .slice(0, 2) .map((a) => { const action = t(`action_types.${a.type}`); return a.value ? `${action} "${a.value}"` : action; }) .join(", "); return ( {conditionSummary}{extra} → {actionSummary} ); } export function FilterSettings() { const t = useTranslations("settings.filters"); const tNotifications = useTranslations("notifications"); const { client } = useAuthStore(); const mailboxes = useEmailStore((s) => s.mailboxes); const { rules, isLoading, isSaving, error, isSupported, isOpaque, rawScript, fetchFilters, saveFilters, addRule, updateRule, deleteRule, reorderRules, toggleRule, setRawScript, resetToVisualBuilder, validateScript, } = useFilterStore(); const [editingRule, setEditingRule] = useState(); const [showRuleModal, setShowRuleModal] = useState(false); const [showSieveEditor, setShowSieveEditor] = useState(false); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [showResetConfirm, setShowResetConfirm] = useState(false); const [dragOverIndex, setDragOverIndex] = useState(null); const draggedIndexRef = useRef(null); useEffect(() => { if (client && isSupported) { void fetchFilters(client); } }, [client, isSupported, fetchFilters]); const handleToggle = useCallback( async (ruleId: string) => { toggleRule(ruleId); if (client) { try { await saveFilters(client); } catch { toggleRule(ruleId); toast.error(tNotifications("filters_save_failed")); } } }, [client, toggleRule, saveFilters, tNotifications] ); const handleDelete = useCallback( async (ruleId: string) => { const deletedRule = rules.find((r) => r.id === ruleId); deleteRule(ruleId); setDeleteConfirmId(null); if (client) { try { await saveFilters(client); toast.success(tNotifications("filters_deleted")); } catch { if (deletedRule) addRule(deletedRule); toast.error(tNotifications("filters_save_failed")); } } }, [client, rules, deleteRule, addRule, saveFilters, tNotifications] ); const handleSaveRule = useCallback( async (rule: FilterRule) => { const previousRules = [...rules]; if (editingRule) { updateRule(rule.id, rule); } else { addRule(rule); } setShowRuleModal(false); setEditingRule(undefined); if (client) { try { await saveFilters(client); } catch { useFilterStore.setState({ rules: previousRules }); toast.error(tNotifications("filters_save_failed")); } } }, [editingRule, updateRule, addRule, rules, client, saveFilters, tNotifications] ); const handleSaveSieve = useCallback( async (content: string) => { setRawScript(content); useFilterStore.setState({ isOpaque: true, rules: [] }); if (client) { try { await saveFilters(client); toast.success(tNotifications("filters_saved")); setShowSieveEditor(false); } catch { toast.error(tNotifications("filters_save_failed")); } } }, [client, setRawScript, saveFilters, tNotifications] ); const handleResetToVisual = useCallback(async () => { if (!showResetConfirm) { setShowResetConfirm(true); return; } resetToVisualBuilder(); setShowResetConfirm(false); if (client) { try { await saveFilters(client); toast.success(tNotifications("filters_saved")); } catch { toast.error(tNotifications("filters_save_failed")); } } }, [showResetConfirm, resetToVisualBuilder, client, saveFilters, tNotifications]); const handleValidate = useCallback( async (content: string) => { if (!client) return { isValid: false, errors: ["No client"] }; return validateScript(client, content); }, [client, validateScript] ); const handleDragStart = useCallback( (e: React.DragEvent, index: number) => { draggedIndexRef.current = index; e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(index)); }, [] ); const handleDragOver = useCallback( (e: React.DragEvent, index: number) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOverIndex(index); }, [] ); const handleDrop = useCallback( async (e: React.DragEvent, dropIndex: number) => { e.preventDefault(); setDragOverIndex(null); const fromIndex = draggedIndexRef.current; if (fromIndex === null || fromIndex === dropIndex) return; const previousOrder = rules.map((r) => r.id); const newOrder = [...previousOrder]; const [moved] = newOrder.splice(fromIndex, 1); newOrder.splice(dropIndex, 0, moved); reorderRules(newOrder); if (client) { try { await saveFilters(client); } catch { reorderRules(previousOrder); toast.error(tNotifications("filters_save_failed")); } } }, [rules, reorderRules, client, saveFilters, tNotifications] ); const handleDragEnd = useCallback(() => { draggedIndexRef.current = null; setDragOverIndex(null); }, []); if (!isSupported) { return (
{t("not_supported")}
); } if (isLoading) { return (
{t("loading")}
); } if (error) { return (
{t("fetch_error")}
); } return (
{isOpaque && (

{t("opaque_warning")}

{showResetConfirm ? ( {t("reset_warning")} ) : ( )}
)} {!isOpaque && rules.length === 0 && (

{t("no_rules")}

)} {!isOpaque && rules.length > 0 && (
{rules.map((rule, index) => (
handleDragStart(e, index)} onDragOver={(e) => handleDragOver(e, index)} onDrop={(e) => handleDrop(e, index)} onDragEnd={handleDragEnd} className={`flex items-center gap-3 p-3 rounded-md border transition-colors ${ dragOverIndex === index ? "border-primary bg-primary/5" : "border-border hover:bg-muted/50" } ${!rule.enabled ? "opacity-60" : ""}`} >
handleToggle(rule.id)} />
{ setEditingRule(rule); setShowRuleModal(true); }} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setEditingRule(rule); setShowRuleModal(true); } }} >

{rule.name}

{deleteConfirmId === rule.id ? (
) : ( )}
))}
)}
{!isOpaque && ( )}
{isSaving && (
{t("saving")}
)}
{showRuleModal && ( { setShowRuleModal(false); setEditingRule(undefined); }} /> )} {showSieveEditor && ( setShowSieveEditor(false)} onValidate={handleValidate} /> )}
); }