"use client"; import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, Webhook } from "lucide-react"; import { Button } from "@/components/ui/button"; import { SettingsCardActions } from "@/components/settings/_card-actions"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { PasswordInput } from "@/components/ui/password-input"; import { NativeSelect } from "@/components/ui/native-select"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { TestConnectionButton } from "@/components/settings/test-connection-button"; import { isWebhookFormDirty } from "@/components/settings/channel-form-dirty"; import { toastWrittenOutcome } from "@/components/outcome/outcome-toast"; import { useChannelEnabledToggle } from "@/components/settings/notification-channel-toggle"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { apiFetchRaw, apiGet } from "@/lib/api/api-fetch"; import type { WebhookPayloadFormat } from "@/lib/notifications/types"; import { translateCodeOrNull } from "@/lib/i18n/translate-code"; interface WebhookSettings { enabled: boolean; url: string; headerName: string; hasHeaderValue: boolean; format?: WebhookPayloadFormat; } export function WebhookCard({ isAuthenticated }: { isAuthenticated: boolean }) { const { t } = useTranslations(); const queryClient = useQueryClient(); const [url, setUrl] = useState(""); const [headerName, setHeaderName] = useState(""); const [headerValue, setHeaderValue] = useState(""); const [format, setFormat] = useState("generic"); const [saveError, setSaveError] = useState(null); const { data: settings } = useQuery({ queryKey: queryKeys.settingsWebhook(), queryFn: async () => { return apiGet("/api/settings/webhook"); }, enabled: isAuthenticated, }); // React-recommended sync-from-server pattern (no setState-in-effect). const settingsKey = settings ? `${settings.url}|${settings.headerName}|${settings.format ?? "generic"}` : null; const [seededKey, setSeededKey] = useState(null); if (settingsKey && settingsKey !== seededKey) { setSeededKey(settingsKey); setUrl(settings!.url); setHeaderName(settings!.headerName); setFormat(settings!.format === "gotify" ? "gotify" : "generic"); } const dirty = isWebhookFormDirty( { url, headerName, headerValue, format }, settings, ); // The enable switch saves on tap; the form's Save covers the fields below. const toggle = useChannelEnabledToggle( "/api/settings/webhook", queryKeys.settingsWebhook(), ); const save = useMutation({ mutationFn: async () => { const res = await apiFetchRaw("/api/settings/webhook", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url, headerName: headerName || undefined, headerValue: headerValue || undefined, format, enabled: settings?.enabled ?? false, }), }); if (!res.ok) { const json = (await res.json()) as { error?: string; meta?: { errorCode?: string }; }; // A coded refusal (the private-origin policy) translates like the // test button's; anything else shows the server sentence. throw new Error( translateCodeOrNull( t, "settings.testConnection.errors", json.meta?.errorCode, ) ?? (json.error || t("common.error")), ); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.settingsWebhook() }); setSaveError(null); toastWrittenOutcome("success", t("settings.saved")); setHeaderValue(""); }, // A form shows its refusal inline, next to the fields it is about. onError: (err: Error) => { setSaveError(err.message); }, }); return (
toggle.mutate(checked)} disabled={!settings || save.isPending || toggle.isPending} />
{ e.preventDefault(); save.mutate(); }} className="space-y-4" >
setUrl(e.target.value)} />

{t("settings.webhookUrlHint")}

setFormat(e.target.value === "gotify" ? "gotify" : "generic") } >

{format === "gotify" ? t("settings.webhookFormatGotifyHint") : t("settings.webhookFormatGenericHint")}

setHeaderName(e.target.value)} />
setHeaderValue(e.target.value)} />
{saveError && (

{saveError}

)}
); }