"use client"; import { useState } from "react"; import { Modal } from "@/shared/components"; import { Step1ChooseIntegration } from "./steps/Step1ChooseIntegration"; import { Step2ConfigureIntegration, type SlackConfig, type TelegramConfig, type DiscordConfig, type CustomConfig, } from "./steps/Step2ConfigureIntegration"; import { Step3EventsAndTest } from "./steps/Step3EventsAndTest"; import { HowItWorksSidebar } from "./HowItWorksSidebar"; import type { WebhookKind } from "./shared/IntegrationCard"; interface AddWebhookWizardProps { isOpen: boolean; onClose: () => void; onCreated: () => void; t: (key: string, opts?: Record) => string; } const STEPS = [1, 2, 3] as const; interface WizardState { kind: WebhookKind; slack: SlackConfig; telegram: TelegramConfig; discord: DiscordConfig; custom: CustomConfig; events: string[]; enabled: boolean; description: string; } const INITIAL: WizardState = { kind: "slack", slack: { webhookUrl: "" }, telegram: { botToken: "", chatId: "" }, discord: { webhookUrl: "" }, custom: { endpointUrl: "", secretKey: "" }, events: ["*"], enabled: true, description: "", }; function step2Valid(state: WizardState): boolean { const { kind } = state; if (kind === "slack") return state.slack.webhookUrl.trim().length > 0; if (kind === "telegram") return state.telegram.botToken.trim().length > 0 && state.telegram.chatId.trim().length > 0; if (kind === "discord") return state.discord.webhookUrl.trim().length > 0; if (kind === "custom") return state.custom.endpointUrl.trim().length > 0; return false; } export function AddWebhookWizard({ isOpen, onClose, onCreated, t }: AddWebhookWizardProps) { const [step, setStep] = useState(1); const [state, setState] = useState(INITIAL); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [createdId, setCreatedId] = useState(null); const handleClose = () => { if (saving) return; setStep(1); setState(INITIAL); setError(null); setCreatedId(null); onClose(); }; // Builds the config payload (URL + kind + metadata) — sent when entering step 3. const buildConfigPayload = () => { const { kind } = state; if (kind === "slack") return { kind, url: state.slack.webhookUrl }; if (kind === "discord") return { kind, url: state.discord.webhookUrl }; if (kind === "telegram") { return { kind, url: state.telegram.chatId, metadata: { botToken: state.telegram.botToken } }; } const payload: Record = { kind, url: state.custom.endpointUrl }; if (state.custom.secretKey.trim()) payload.secret = state.custom.secretKey.trim(); return payload; }; // Called when Next is clicked on step 2: create (or update) the webhook so the // test button is available at step 3 before the user clicks Finish. const handleNextFromStep2 = async () => { setSaving(true); setError(null); try { if (createdId) { const res = await fetch(`/api/webhooks/${createdId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(buildConfigPayload()), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || t("saveFailed")); } else { const res = await fetch("/api/webhooks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(buildConfigPayload()), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || t("saveFailed")); setCreatedId(data.webhook?.id ?? null); } setStep(3); } catch (err) { setError(err instanceof Error ? err.message : t("saveFailed")); } finally { setSaving(false); } }; // Finish: update events/enabled/description on the already-created webhook. const finish = async () => { if (!createdId) return; setSaving(true); setError(null); try { const res = await fetch(`/api/webhooks/${createdId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ events: state.events, enabled: state.enabled, description: state.description, }), }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || t("saveFailed")); onCreated(); handleClose(); } catch (err) { setError(err instanceof Error ? err.message : t("saveFailed")); } finally { setSaving(false); } }; const canGoNext = step === 1 ? true : step === 2 ? step2Valid(state) : true; const stepTitle = step === 1 ? t("wizard.step1Title") : step === 2 ? t("wizard.step2Title") : t("wizard.step3Title"); return (
{STEPS.map((s) => ( ))}
{step > 1 && ( )} {step < 3 ? ( ) : ( )}
} >
{error && (
{error}
)} {step === 1 && ( setState((s) => ({ ...s, kind }))} t={t} /> )} {step === 2 && ( setState((s) => ({ ...s, slack: v }))} onChangeTelegram={(v) => setState((s) => ({ ...s, telegram: v }))} onChangeDiscord={(v) => setState((s) => ({ ...s, discord: v }))} onChangeCustom={(v) => setState((s) => ({ ...s, custom: v }))} t={t} /> )} {step === 3 && ( setState((s) => ({ ...s, events }))} onChangeEnabled={(enabled) => setState((s) => ({ ...s, enabled }))} onChangeDescription={(description) => setState((s) => ({ ...s, description }))} t={t} /> )}
); }