"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { detectWebhookFlavor } from "@openstatus/subscriptions/client"; import { Badge } from "@openstatus/ui/components/ui/badge"; import { Button } from "@openstatus/ui/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@openstatus/ui/components/ui/form"; import { Input } from "@openstatus/ui/components/ui/input"; import { Tabs, TabsList, TabsTrigger } from "@openstatus/ui/components/ui/tabs"; import { cn } from "@openstatus/ui/lib/utils"; import { isTRPCClientError } from "@trpc/client"; import { Plus, X } from "lucide-react"; import React, { useTransition } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { EmptyStateContainer, EmptyStateTitle, } from "@/components/content/empty-state"; import { FormCardContent, FormCardSeparator, } from "@/components/forms/form-card"; import { useFormSheetDirty } from "@/components/forms/form-sheet"; import { CheckboxTree, type CheckboxTreeItem, } from "@/components/ui/checkbox-tree"; const headerSchema = z.object({ key: z.string().min(1, "Key is required"), value: z.string(), }); // Form schema: a single flat shape with optional fields. The submit handler // narrows to email-only or webhook-only payload based on `channelType`. const formSchema = z .object({ channelType: z.enum(["email", "webhook"]), name: z.string().max(255), email: z.string(), webhookUrl: z.string(), headers: z.array(headerSchema), componentIds: z.array(z.number()), }) .superRefine((data, ctx) => { if (data.channelType === "email") { const result = z.email().safeParse(data.email); if (!result.success) { ctx.addIssue({ code: "custom", path: ["email"], message: "Please enter a valid email", }); } } else { const result = z.url().safeParse(data.webhookUrl); if (!result.success) { ctx.addIssue({ code: "custom", path: ["webhookUrl"], message: "Please enter a valid URL", }); } } }); export type SubscriberFormValues = z.infer; export type SubmitPayload = | { channelType: "email"; name: string; email: string; componentIds: number[]; } | { channelType: "webhook"; name: string; webhookUrl: string; headers: { key: string; value: string }[]; componentIds: number[]; }; function toPayload(values: SubscriberFormValues): SubmitPayload { if (values.channelType === "email") { return { channelType: "email", name: values.name, email: values.email, componentIds: values.componentIds, }; } return { channelType: "webhook", name: values.name, webhookUrl: values.webhookUrl, headers: values.headers, componentIds: values.componentIds, }; } const emptyDefaults: SubscriberFormValues = { channelType: "email", name: "", email: "", webhookUrl: "", headers: [], componentIds: [], }; export function FormSubscriber({ defaultValues, onSubmit, className, items, editMode = false, ...props }: Omit, "onSubmit"> & { defaultValues?: Partial; items: CheckboxTreeItem[]; onSubmit: (values: SubmitPayload) => Promise; /** When true, channel type is locked (editing an existing subscriber). */ editMode?: boolean; }) { const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { ...emptyDefaults, ...defaultValues }, }); const channelType = form.watch("channelType"); const [isPending, startTransition] = useTransition(); const { setIsDirty } = useFormSheetDirty(); const formIsDirty = form.formState.isDirty; React.useEffect(() => { setIsDirty(formIsDirty); }, [formIsDirty, setIsDirty]); const { fields, append, remove } = useFieldArray({ control: form.control, name: "headers", }); function submitAction(values: SubscriberFormValues) { if (isPending) return; startTransition(async () => { try { const promise = onSubmit(toPayload(values)); toast.promise(promise, { loading: "Saving...", success: () => "Saved", error: (error) => { if (isTRPCClientError(error)) { return error.message; } return "Failed to save"; }, }); await promise; } catch (error) { console.error(error); } }); } return (
( Channel { if (editMode) return; if (v === "email" || v === "webhook") { field.onChange(v); } }} > Email Webhook )} /> ( Display label (optional) Shown in place of the raw destination in the dashboard. )} /> {channelType === "email" ? ( ( Email {editMode ? ( Email address is immutable. Delete and re-add to change it. ) : ( By adding this email, you confirm this contact has consented to receive status updates. We'll not be sending a confirmation email. )} )} /> ) : ( <> ( Webhook URL Slack and Discord URLs receive channel-native messages; any other URL receives a generic JSON payload. {/^https?:\/\//.test(field.value) ? ( {detectWebhookFlavor(field.value)} ) : null} )} /> Request Headers Custom headers to include in every webhook request. {fields.map((f, idx) => (
( )} /> ( )} />
))}
)} ( Page Components Leave empty to notify for the entire page. Select components to only notify on matching reports. {items.length ? ( ) : ( No page components found )} )} /> ); }