diff --git a/frontend-web/src/components/workspace/admin/tag-management-page.tsx b/frontend-web/src/components/workspace/admin/tag-management-page.tsx new file mode 100644 index 00000000..388cd1c3 --- /dev/null +++ b/frontend-web/src/components/workspace/admin/tag-management-page.tsx @@ -0,0 +1,293 @@ +"use client"; + +import { Loader2, PencilIcon, PlusIcon, Trash2Icon } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import { SettingsSection } from "@/components/workspace/settings/settings-section"; +import { TagChip } from "@/components/workspace/tags/tag-chip"; +import { TAG_COLORS } from "@/components/workspace/tags/tag-colors"; +import { useI18n } from "@/core/i18n/hooks"; +import { + useCreateTag, + useDeleteTag, + useTags, + useUpdateTag, + type Tag, + type TagType, +} from "@/core/tags"; +import { cn } from "@/lib/utils"; + +function TagFormDialog({ + open, + onOpenChange, + tagType, + editing, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + tagType: TagType; + editing: Tag | null; +}) { + const { t } = useI18n(); + const createTag = useCreateTag(); + const updateTag = useUpdateTag(); + const [name, setName] = useState(""); + const [color, setColor] = useState("gray"); + const [description, setDescription] = useState(""); + + // Sync form state whenever the dialog (re)opens. + const [syncedFor, setSyncedFor] = useState(null); + const syncKey = `${open}:${editing?.id ?? "new"}`; + if (open && syncedFor !== syncKey) { + setName(editing?.name ?? ""); + setColor(editing?.color ?? "gray"); + setDescription(editing?.description ?? ""); + setSyncedFor(syncKey); + } + if (!open && syncedFor !== null) setSyncedFor(null); + + const pending = createTag.isPending || updateTag.isPending; + + const handleSave = async () => { + const trimmed = name.trim(); + if (!trimmed) return; + try { + if (editing) { + await updateTag.mutateAsync({ + id: editing.id, + input: { name: trimmed, color, description }, + }); + } else { + await createTag.mutateAsync({ + name: trimmed, + tag_type: tagType, + color, + description, + }); + } + onOpenChange(false); + } catch (e) { + toast.error(String(e instanceof Error ? e.message : e)); + } + }; + + return ( + + + + + {editing ? t.adminTags.editTitle : t.adminTags.createTitle} + + +
+
+ + setName(e.target.value)} + placeholder={t.adminTags.namePlaceholder} + maxLength={256} + /> +
+
+ +
+ {TAG_COLORS.map((c) => ( +
+
+
+ +