import { Checkbox } from "@unom/ui/form/checkbox"; import { type FC, useEffect, useState } from "react"; import type { HookEntry } from "@/api/gen/model/hookEntry"; 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 { m } from "@/paraglide/messages"; /** The event kinds the host publishes, plus the `domain.*` wildcards the hook filter accepts. * Same vocabulary as the SSE `?kinds=` filter, so the two stay learnable together. */ export const EVENT_KINDS = [ "client.*", "client.connected", "client.disconnected", "session.*", "session.started", "session.ended", "stream.*", "stream.started", "stream.stopped", "game.*", "game.running", "game.exited", "pairing.*", "pairing.pending", "pairing.completed", "pairing.denied", "display.*", "display.created", "display.released", "library.changed", "update.available", "update.applied", "host.started", "host.stopping", ] as const; const EMPTY: HookEntry = { on: "session.started", run: "" }; /** * Add or edit one hook. * * A hook is either a shell command or a webhook — never both in this form, because "run this AND * post that" is two hooks and pretending otherwise makes the failure modes impossible to reason * about. The action kind is therefore a choice, not two optional fields. */ export const HookForm: FC<{ /** The hook being edited, `EMPTY`-seeded for a new one, or null when closed. */ value: HookEntry | null; onCancel: () => void; onSave: (hook: HookEntry) => void; }> = ({ value, onCancel, onSave }) => { const [draft, setDraft] = useState(EMPTY); const [kind, setKind] = useState<"run" | "webhook">("run"); const [filtered, setFiltered] = useState(false); // Re-seed whenever a different hook is opened (the dialog stays mounted between edits). useEffect(() => { if (!value) return; setDraft(value); setKind(value.webhook ? "webhook" : "run"); setFiltered(!!value.filter); }, [value]); const set = (patch: Partial) => setDraft((d) => ({ ...d, ...patch })); const action = kind === "run" ? (draft.run ?? "") : (draft.webhook ?? ""); const ready = draft.on.trim().length > 0 && action.trim().length > 0; const commit = () => { // Emit exactly one action field, and drop an unticked filter entirely — leaving `{}` behind // would read as "filter on nothing" to anyone reading the config file later. const out: HookEntry = { on: draft.on.trim(), ...(kind === "run" ? { run: action.trim(), webhook: null } : { webhook: action.trim(), run: null }), ...(filtered && draft.filter ? { filter: draft.filter } : {}), ...(draft.debounce_ms ? { debounce_ms: draft.debounce_ms } : {}), ...(draft.timeout_s ? { timeout_s: draft.timeout_s } : {}), ...(kind === "webhook" && draft.hmac_secret_file ? { hmac_secret_file: draft.hmac_secret_file } : {}), }; onSave(out); }; return ( !o && onCancel()}> {m.automation_hook_title()} {m.automation_hook_help()}

{m.automation_field_on_help()}

{m.automation_field_action()}
{(["run", "webhook"] as const).map((k) => ( ))}
set( kind === "run" ? { run: e.target.value } : { webhook: e.target.value }, ) } />

{kind === "run" ? m.automation_action_run_help() : m.automation_action_webhook_help()}

{kind === "webhook" && (
set({ hmac_secret_file: e.target.value })} />

{m.automation_field_hmac_help()}

)} {filtered && (
set({ filter: { ...draft.filter, client: e.target.value } }) } />
set({ filter: { ...draft.filter, app: e.target.value } }) } />
)}
set({ debounce_ms: Number(e.target.value) || 0 }) } />
{kind === "run" && (
set({ timeout_s: Number(e.target.value) || 30 }) } />
)}
); };