import { useState, useEffect } from 'react'; import { Webhook, CreateWebhookRequest, UpdateWebhookRequest, WebhookEvent } from '@/services/api'; import { X, Plus, Trash2, Eye, EyeOff, Copy, Check } from 'lucide-react'; import clsx from 'clsx'; /** * Props for the WebhookForm component. */ interface WebhookFormProps { /** Existing webhook data for editing, or null/undefined for creating a new webhook */ webhook?: Webhook | null; /** List of available webhook events that can be subscribed to */ events: WebhookEvent[]; /** Callback invoked when the form is submitted with valid data */ onSubmit: (data: CreateWebhookRequest | UpdateWebhookRequest) => void; /** Callback invoked when the user cancels the form */ onCancel: () => void; /** Whether the form submission is in progress */ isLoading?: boolean; } /** * Form component for creating or editing webhook configurations. * * @description Provides a comprehensive form for webhook management including: * - Name and description fields * - HTTPS endpoint URL with validation * - Event selection checkboxes * - Custom HTTP headers management * - Signing secret display (edit mode only) * * @param props - The component props * @param props.webhook - Existing webhook data for editing, or null for creating * @param props.events - List of available webhook events to subscribe to * @param props.onSubmit - Callback when form is submitted with valid data * @param props.onCancel - Callback when user cancels the form * @param props.isLoading - Whether submission is in progress (disables form) * * @example * ```tsx * // Creating a new webhook * createWebhook(data)} * onCancel={() => closeModal()} * /> * * // Editing an existing webhook * updateWebhook(existingWebhook.id, data)} * onCancel={() => closeModal()} * isLoading={isSaving} * /> * ``` */ export function WebhookForm({ webhook, events, onSubmit, onCancel, isLoading, }: WebhookFormProps) { const [name, setName] = useState(webhook?.name || ''); const [description, setDescription] = useState(webhook?.description || ''); const [url, setUrl] = useState(webhook?.url || ''); const [selectedEvents, setSelectedEvents] = useState(webhook?.events || []); const [headers, setHeaders] = useState<{ key: string; value: string }[]>( webhook?.headers ? Object.entries(webhook.headers).map(([key, value]) => ({ key, value })) : [] ); const [showSecret, setShowSecret] = useState(false); const [secretCopied, setSecretCopied] = useState(false); const isEditing = !!webhook; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const headersObj = headers.reduce((acc, { key, value }) => { if (key.trim()) { acc[key.trim()] = value; } return acc; }, {} as Record); const data = { name, description: description || undefined, url, events: selectedEvents, headers: Object.keys(headersObj).length > 0 ? headersObj : undefined, }; onSubmit(data); }; const toggleEvent = (eventName: string) => { setSelectedEvents((prev) => prev.includes(eventName) ? prev.filter((e) => e !== eventName) : [...prev, eventName] ); }; const addHeader = () => { setHeaders((prev) => [...prev, { key: '', value: '' }]); }; const removeHeader = (index: number) => { setHeaders((prev) => prev.filter((_, i) => i !== index)); }; const updateHeader = (index: number, field: 'key' | 'value', value: string) => { setHeaders((prev) => prev.map((h, i) => (i === index ? { ...h, [field]: value } : h)) ); }; const copySecret = async () => { if (webhook?.secret) { await navigator.clipboard.writeText(webhook.secret); setSecretCopied(true); setTimeout(() => setSecretCopied(false), 2000); } }; const isValid = name.trim() && url.trim() && url.startsWith('https://') && selectedEvents.length > 0; return (
{/* Name */}
setName(e.target.value)} placeholder="My Webhook" className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-secondary-100 focus:ring-2 focus:ring-primary-500" required />
{/* Description */}
setDescription(e.target.value)} placeholder="Optional description" className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-secondary-100 focus:ring-2 focus:ring-primary-500" />
{/* URL */}
setUrl(e.target.value)} placeholder="https://example.com/webhook" className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-secondary-100 focus:ring-2 focus:ring-primary-500" required /> {url && !url.startsWith('https://') && (

URL must use HTTPS

)}
{/* Secret (only for editing) */} {isEditing && webhook?.secret && (

Use this secret to verify webhook signatures

)} {/* Events */}
{events.map((event) => ( ))}
{selectedEvents.length === 0 && (

Select at least one event

)}
{/* Custom Headers */}
{headers.length > 0 && (
{headers.map((header, index) => (
updateHeader(index, 'key', e.target.value)} placeholder="Header name" className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-secondary-100 text-sm" /> updateHeader(index, 'value', e.target.value)} placeholder="Value" className="flex-1 px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-lg bg-white dark:bg-secondary-800 text-secondary-900 dark:text-secondary-100 text-sm" />
))}
)} {headers.length === 0 && (

No custom headers

)}
{/* Actions */}
); }