import { useEffect, useState } from "react"; import { useBoard } from "@/board/store"; import { api } from "@/api"; import type { Item } from "@/types"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import type { components } from "@dash/api-client"; type WidgetType = components["schemas"]["WidgetType"]; // One generic dialog for every widget type — the registry's field list // drives the inputs, so a new integration needs no dialog code. export function WidgetDialog({ open, onOpenChange, item, }: { open: boolean; onOpenChange: (o: boolean) => void; item?: Item | undefined; }) { const board = useBoard(); const editing = Boolean(item); const [types, setTypes] = useState([]); const [type, setType] = useState(""); const [name, setName] = useState(""); const [sectionId, setSectionId] = useState(""); const [values, setValues] = useState>({}); const [monitors, setMonitors] = useState<{ id?: string; name?: string }[]>([]); const [domains, setDomains] = useState<{ id?: string; name?: string }[]>([]); const [systems, setSystems] = useState<{ id?: string; name?: string }[]>([]); const [busy, setBusy] = useState(false); const [err, setErr] = useState(""); useEffect(() => { if (!open) return; setErr(""); // SAFETY: raw fetch — the generated client types only cover typed endpoints we call through api.ts. fetch("/api/widgets/types") .then((r) => r.json() as Promise) .then(setTypes) .catch(() => setTypes([])); void api.GET("/api/monitors").then(({ data }) => setMonitors(data ?? [])); void api.GET("/api/domains").then(({ data }) => setDomains(data ?? [])); void api.GET("/api/systems").then(({ data }) => setSystems(data ?? [])); const cfg = item?.config ?? null; setType(cfg && typeof cfg.type === "string" ? cfg.type : ""); setName(item?.name ?? ""); setSectionId(item?.sectionId ?? board.sections[0]?.id ?? ""); const init: Record = {}; if (cfg) { for (const [k, v] of Object.entries(cfg)) { if (k !== "type" && typeof v === "string") init[k] = v; } } setValues(init); }, [open, item, board.sections]); const meta = types.find((t) => t.type === type); async function submit() { if (!type) return setErr("Pick a widget type"), undefined; if (!name.trim()) return setErr("Name is required"), undefined; if (meta?.fields.some((f) => f.required && !values[f.key]?.trim())) { return setErr("Fill in the required fields"), undefined; } setBusy(true); try { const config: NonNullable = { type }; for (const f of meta?.fields ?? []) { const v = values[f.key]?.trim(); if (v) config[f.key] = v; } if (editing && item) { await board.patchItem(item.id, { name: name.trim(), config }); } else { let sid = sectionId; if (!sid) { const sec = await board.addSection("Widgets"); if (!sec) throw new Error("could not create section"); sid = sec.id; } await board.addItem({ sectionId: sid, kind: "widget", name: name.trim(), config, urls: [] }); } onOpenChange(false); } catch (e) { setErr(e instanceof Error ? e.message : "save failed"); } finally { setBusy(false); } } return ( {editing ? "Edit widget" : "Add widget"} {editing ? "Update widget settings." : "Pick a type and point it at your service."}
setName(e.target.value)} placeholder="Pi-hole" autoFocus />
{!editing && board.sections.length > 0 && (
)}
{meta?.description ? (

{meta.description}

) : null}
{meta?.fields.map((f) => (
{f.key === "monitorId" || f.key === "domainId" || f.key === "systemId" ? ( ) : ( setValues((v) => ({ ...v, [f.key]: e.target.value }))} className="font-mono text-[12px]" /> )}
))} {err ?

{err}

: null}
); }