import { zodResolver } from "@hookform/resolvers/zod"; import { t } from "@lingui/core/macro"; import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { HiXMark } from "react-icons/hi2"; import { z } from "zod"; import { webhookEvents } from "@kan/db/schema"; import Button from "~/components/Button"; import Input from "~/components/Input"; import { useModal } from "~/providers/modal"; import { usePopup } from "~/providers/popup"; import { api } from "~/utils/api"; const newWebhookSchema = z.object({ name: z .string() .min(1, { message: t`Webhook name is required` }) .max(255, { message: t`Webhook name cannot exceed 255 characters` }), url: z .string() .min(1, { message: t`Webhook URL is required` }) .url({ message: t`Please enter a valid URL` }) .max(2048, { message: t`URL cannot exceed 2048 characters` }), secret: z .string() .max(512, { message: t`Secret cannot exceed 512 characters` }) .optional(), events: z .array(z.enum(webhookEvents)) .min(1, { message: t`Select at least one event` }), active: z.boolean(), }); type WebhookFormData = z.infer; interface NewWebhookModalProps { workspacePublicId: string; isEdit?: boolean; } export function NewWebhookModal({ workspacePublicId, isEdit = false, }: NewWebhookModalProps) { const { closeModal, entityId: webhookPublicId, getModalState, clearModalState, } = useModal(); const { showPopup } = usePopup(); const [isTestingWebhook, setIsTestingWebhook] = useState(false); const modalState = isEdit ? getModalState("EDIT_WEBHOOK") : null; const utils = api.useUtils(); const { register, handleSubmit, control, reset, formState: { errors }, } = useForm({ resolver: zodResolver(newWebhookSchema), defaultValues: { name: "", url: "", secret: "", events: [...webhookEvents], active: true, }, }); useEffect(() => { if (isEdit && webhookPublicId && modalState) { reset({ name: modalState.name ?? "", url: modalState.url ?? "", secret: "", events: modalState.events ?? ["card.created"], active: modalState.active ?? true, }); } else if (!isEdit) { reset({ name: "", url: "", secret: "", events: [...webhookEvents], active: true, }); } }, [isEdit, webhookPublicId, modalState, reset]); // Clear modal state when closing useEffect(() => { return () => { if (isEdit) { clearModalState("EDIT_WEBHOOK"); } }; }, [isEdit, clearModalState]); const createWebhookMutation = api.webhook.create.useMutation({ onSuccess: () => { void utils.webhook.list.invalidate({ workspacePublicId }); showPopup({ header: t`Webhook created`, message: t`Webhook created successfully`, icon: "success", }); closeModal(); }, onError: (error) => { showPopup({ header: t`Unable to create webhook`, message: error.message || t`Failed to create webhook`, icon: "error", }); }, }); const updateWebhookMutation = api.webhook.update.useMutation({ onSuccess: () => { void utils.webhook.list.invalidate({ workspacePublicId }); showPopup({ header: t`Webhook updated`, message: t`Webhook updated successfully`, icon: "success", }); closeModal(); }, onError: (error) => { showPopup({ header: t`Unable to update webhook`, message: error.message || t`Failed to update webhook`, icon: "error", }); }, }); const testWebhookMutation = api.webhook.test.useMutation({ onSuccess: (result) => { if (result.success) { showPopup({ header: t`Test sent`, message: t`Test webhook sent successfully!`, icon: "success", }); } else { showPopup({ header: t`Test failed`, message: result.error || t`Webhook test failed`, icon: "error", }); } setIsTestingWebhook(false); }, onError: (error) => { showPopup({ header: t`Unable to test webhook`, message: error.message || t`Failed to test webhook`, icon: "error", }); setIsTestingWebhook(false); }, }); const onSubmit = (data: WebhookFormData) => { if (isEdit && webhookPublicId) { updateWebhookMutation.mutate({ workspacePublicId, webhookPublicId: webhookPublicId, name: data.name, url: data.url, secret: data.secret || undefined, events: data.events, active: data.active, }); } else { createWebhookMutation.mutate({ workspacePublicId, name: data.name, url: data.url, secret: data.secret || undefined, events: data.events, }); } }; const handleTestWebhook = () => { if (!webhookPublicId) return; setIsTestingWebhook(true); testWebhookMutation.mutate({ workspacePublicId, webhookPublicId: webhookPublicId, }); }; const isPending = createWebhookMutation.isPending || updateWebhookMutation.isPending; return (

{isEdit ? t`Edit webhook` : t`New webhook`}

{t`Used to sign webhook payloads for verification. Leave blank to keep existing secret.`}

(
{webhookEvents.map((event) => ( ))}
)} /> {errors.events && (

{errors.events.message}

)}
{isEdit && (
)}
{isEdit && webhookPublicId && ( )}
); }