"use client"; import React, { useState } from "react"; import { useTranslations } from "next-intl"; import { useSettingsStore, KEYWORD_PALETTE, DEFAULT_KEYWORDS, getKeywordVisibility, type KeywordDefinition, type KeywordVisibility, } from "@/stores/settings-store"; import { useAuthStore } from "@/stores/auth-store"; import { useEmailStore } from "@/stores/email-store"; import { SettingsSection, SettingItem, ToggleSwitch, Select } from "./settings-section"; import { Plus, Pencil, Trash2, GripVertical, Check, X, RotateCcw, Loader2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { KEYWORD_PREFIX } from "@/lib/thread-utils"; import { buildKeywordTree, composeKeywordId, getParentKeywordId, hasChildKeywords, isKeywordDescendant, keywordLevels, type KeywordNode, MAX_KEYWORD_ID_LENGTH, } from "@/lib/keyword-nesting"; import { formatKeyword, formatKeywordLabels, keywordRenderings } from "@/lib/keyword-format"; import { useShortenedText } from "@/hooks/use-shortened-text"; const PALETTE_KEYS = Object.keys(KEYWORD_PALETTE); function KeywordColorPicker({ value, onChange, }: { value: string; onChange: (color: string) => void; }) { return (
{PALETTE_KEYS.map((colorKey) => (
); } function KeywordRow({ keyword, keywords, nestedTags, onEdit, onDelete, onVisibilityChange, onDragStart, onDragOver, onDrop, onDragEnd, isDragOver, isDragging, }: { keyword: KeywordDefinition; keywords: KeywordDefinition[]; nestedTags: boolean; onEdit: () => void; onDelete: () => void; onVisibilityChange: (visibility: KeywordVisibility) => void; onDragStart: () => void; onDragOver: (e: React.DragEvent) => void; onDrop: () => void; onDragEnd: () => void; isDragOver: boolean; isDragging: boolean; }) { const t = useTranslations("settings.keywords"); const palette = KEYWORD_PALETTE[keyword.color]; const hasChildren = hasChildKeywords(keyword.id, keywords); const nameCandidates = keywordRenderings(formatKeywordLabels(keyword.id, keywords, nestedTags)); const [nameRef, shortenedName] = useShortenedText(nameCandidates); // Measured with the prefix attached, since that is what occupies the column. const keywordCandidates = (nestedTags ? keywordRenderings(keywordLevels(keyword.id)) : [keyword.id]) .map((rendering) => KEYWORD_PREFIX + rendering); const [keywordRef, shortenedKeyword] = useShortenedText(keywordCandidates); const visibilityOptions = [ { value: "show", label: t("visibility.show") }, { value: "unread", label: t("visibility.unread") }, { value: "hide", label: t("visibility.hide") }, ]; return (
{shortenedName} {shortenedKeyword}
)}
setLabel(e.target.value)} disabled={isLocked} className="w-full px-2.5 py-1.5 text-sm rounded-md border border-border bg-background focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-60" placeholder={t("label_placeholder")} autoFocus maxLength={30} onKeyDown={(e) => e.key === "Enter" && handleSave()} /> {nestedTags && normalizedId.length > 0 && (

{KEYWORD_PREFIX + normalizedId}

)} {isLocked && (

{t("has_children_locked")}

)} {isDuplicate && (

{t("id_exists")}

)} {isTooLong && (

{t("too_long", { max: MAX_KEYWORD_ID_LENGTH })}

)}
); } export function KeywordSettings() { const t = useTranslations("settings.keywords"); const { emailKeywords, nestedTags, addKeyword, updateKeyword, renameKeyword, removeKeyword, reorderKeywords, updateSetting } = useSettingsStore(); const { client } = useAuthStore(); const { fetchTagCounts } = useEmailStore(); const [editingId, setEditingId] = useState(null); const [isAdding, setIsAdding] = useState(false); const [isMigrating, setIsMigrating] = useState(false); const [dragIndex, setDragIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); const existingIds = emailKeywords.map((k) => k.id); const handleDragStart = (index: number) => { setDragIndex(index); }; const handleDragOver = (e: React.DragEvent, index: number) => { e.preventDefault(); if (index !== dragOverIndex) setDragOverIndex(index); }; const handleDrop = (index: number) => { if (dragIndex === null || dragIndex === index) { setDragIndex(null); setDragOverIndex(null); return; } const reordered = [...emailKeywords]; const [moved] = reordered.splice(dragIndex, 1); reordered.splice(index, 0, moved); reorderKeywords(reordered); setDragIndex(null); setDragOverIndex(null); }; const handleDragEnd = () => { setDragIndex(null); setDragOverIndex(null); }; const handleAdd = (keyword: KeywordDefinition) => { addKeyword(keyword); setIsAdding(false); }; const handleEdit = async (keyword: KeywordDefinition) => { const oldId = editingId; if (!oldId) return; const idChanged = oldId !== keyword.id; if (idChanged && client) { setIsMigrating(true); try { const oldJmapKeyword = `$label:${oldId}`; const newJmapKeyword = `$label:${keyword.id}`; await client.migrateKeyword(oldJmapKeyword, newJmapKeyword); renameKeyword(oldId, keyword); fetchTagCounts(client); } catch (error) { console.error("Failed to migrate keyword:", error); const toastModule = await import('sonner'); toastModule.toast.error(t("migration_error")); setIsMigrating(false); return; } setIsMigrating(false); } else { updateKeyword(oldId, { label: keyword.label, color: keyword.color }); } setEditingId(null); }; const handleDelete = (id: string) => { removeKeyword(id); }; const handleVisibilityChange = (id: string, visibility: KeywordVisibility) => { updateKeyword(id, { visibility }); }; const handleResetDefaults = () => { reorderKeywords(DEFAULT_KEYWORDS); }; return ( updateSetting("nestedTags", checked)} />
{isMigrating && (
{t("migrating")}
)} {emailKeywords.map((keyword, index) => editingId === keyword.id ? ( id !== keyword.id)} nestedTags={nestedTags} onSave={handleEdit} onCancel={() => setEditingId(null)} /> ) : ( { setEditingId(keyword.id); setIsAdding(false); }} onDelete={() => handleDelete(keyword.id)} onVisibilityChange={(visibility) => handleVisibilityChange(keyword.id, visibility)} onDragStart={() => handleDragStart(index)} onDragOver={(e) => handleDragOver(e, index)} onDrop={() => handleDrop(index)} onDragEnd={handleDragEnd} isDragOver={dragOverIndex === index && dragIndex !== index} isDragging={dragIndex === index} /> ) )} {isAdding ? ( setIsAdding(false)} /> ) : (
)}
); }