import { useState, type ReactNode } from "react"; import { GripVertical, Loader2, Plus, Trash2 } from "lucide-react"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import type { CreateDropdownOptionDto, DropdownSetting, } from "@/types/dropdownSettings"; import { useMutation } from "@tanstack/react-query"; import { api } from "@/services/api"; export interface ManageDropdownOptionsDialogProps { setting: DropdownSetting; children?: ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void; } /** * Local draft used by the editor — uses a stable client-only `key` so React * keys remain stable across reorders. On save we strip `key` and POST the * remainder as CreateDropdownOptionDto[]. */ interface DraftOption extends CreateDropdownOptionDto { key: string; } let draftCounter = 0; const nextKey = () => `draft-${Date.now()}-${++draftCounter}`; function makeEmptyDraft(idx: number): DraftOption { return { key: nextKey(), value: "", label: "", disabled: false, order: idx + 1, meta: {}, }; } export default function ManageDropdownOptionsDialog({ setting, children, open: openProp, onOpenChange, }: ManageDropdownOptionsDialogProps) { const isControlled = openProp !== undefined; const [internalOpen, setInternalOpen] = useState(false); const open = isControlled ? openProp : internalOpen; const setOpen = (next: boolean) => { if (!isControlled) setInternalOpen(next); onOpenChange?.(next); }; const [error, setError] = useState(null); const seed = (): DraftOption[] => [...(setting.children ?? [])] .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) .map((o, idx) => ({ key: o.id, value: o.value, label: o.label, note: o.note ?? undefined, disabled: o.disabled, order: o.order ?? idx + 1, meta: { ...(o.meta?.icon ? { icon: o.meta.icon } : {}), ...(o.meta?.color ? { color: o.meta.color } : {}), ...(o.meta?.badge ? { badge: o.meta.badge } : {}), }, })); const [options, setOptions] = useState(seed); const replaceMutation = useMutation( api.dropdownSettings.replaceOptions.mutationOptions(), ); const update = (i: number, patch: Partial) => setOptions((prev) => prev.map((o, idx) => (idx === i ? { ...o, ...patch } : o)), ); const updateMeta = ( i: number, patch: Partial>, ) => setOptions((prev) => prev.map((o, idx) => idx === i ? { ...o, meta: { ...(o.meta ?? {}), ...patch } } : o, ), ); const remove = (i: number) => setOptions((prev) => prev.filter((_, idx) => idx !== i)); const add = () => setOptions((prev) => [...prev, makeEmptyDraft(prev.length)]); const move = (i: number, dir: -1 | 1) => setOptions((prev) => { const next = [...prev]; const target = i + dir; if (target < 0 || target >= next.length) return prev; const a = next[i] as DraftOption; const b = next[target] as DraftOption; next[i] = { ...b, order: i + 1 }; next[target] = { ...a, order: target + 1 }; return next; }); const handleSave = () => { setError(null); const invalid = options.findIndex( (o) => !o.label.trim() || !o.value.trim(), ); if (invalid >= 0) { setError(`Option ${invalid + 1} is missing a label or value.`); return; } const payload: CreateDropdownOptionDto[] = options.map((o, idx) => { const meta: NonNullable = {}; if (o.meta?.icon?.trim()) meta.icon = o.meta.icon.trim(); if (o.meta?.color?.trim()) meta.color = o.meta.color.trim(); if (o.meta?.badge?.trim()) meta.badge = o.meta.badge.trim(); return { value: o.value.trim(), label: o.label.trim(), note: o.note?.trim() || undefined, disabled: o.disabled ?? false, order: idx + 1, ...(Object.keys(meta).length > 0 ? { meta } : {}), }; }); replaceMutation.mutate( { id: setting.id, options: payload }, { onSuccess: () => setOpen(false), onError: (err) => setError( err instanceof Error ? err.message : "Failed to save options. Try again.", ), }, ); }; return ( { setOpen(next); if (next) setOptions(seed()); if (!next) setError(null); }} > {children ? {children} : null} Manage Options · {setting.label} Add, edit, reorder, or remove options for{" "} {setting.code}.

{options.length} option{options.length === 1 ? "" : "s"}

{options.length === 0 ? (
No options yet. Click{" "} Add Option to start.
) : (
{options.map((opt, i) => (
update(i, { label: e.target.value })} placeholder="Display label" />
update(i, { value: e.target.value })} placeholder="Stored value" className="font-mono" />
update(i, { note: e.target.value })} placeholder="Helper text" />
updateMeta(i, { badge: e.target.value })} placeholder="—" className="w-20" />
updateMeta(i, { color: e.target.value })} placeholder="#…" className="w-24 font-mono" />
))}
)}
{error ? (

{error}

) : null}
); }