import React, { useEffect } from "react"; import { Modal, TextInput, Textarea, Select, NumberInput, Button, Stack, Group, Divider, Switch, Text, Alert, Code, } from "@mantine/core"; import { useForm } from "@mantine/form"; import { useTranslation } from "react-i18next"; import { SlashCommandDto, CreateSlashCommandPayload } from "../services/slash-commands-client"; const ACTION_TYPE_OPTIONS = [ { value: "insert-template", label: "Insert Template" }, { value: "insert-table", label: "Insert Table" }, { value: "embed-url", label: "Embed URL" }, { value: "run-webhook", label: "Run Webhook" }, { value: "insert-snippet", label: "Insert Code Snippet" }, ]; interface Props { opened: boolean; onClose: () => void; onSubmit: (payload: CreateSlashCommandPayload) => void; initialValues?: SlashCommandDto | null; isLoading?: boolean; } interface FormValues { keyword: string; label: string; description: string; icon: string; actionType: string; isEnabled: boolean; // insert-template template: string; // insert-table rows: number; cols: number; withHeaderRow: boolean; // embed-url url: string; // run-webhook webhookUrl: string; webhookHeaders: string; // insert-snippet language: string; code: string; } /** * Polymorphic create/edit form for custom slash commands. * * The action_config section renders different fields depending on the selected * actionType (discriminated union pattern). Validation is done client-side * before submission; backend validates again with Zod. */ export function SlashCommandForm({ opened, onClose, onSubmit, initialValues, isLoading, }: Props) { const { t } = useTranslation(); const form = useForm({ initialValues: { keyword: "", label: "", description: "", icon: "", actionType: "insert-template", isEnabled: true, template: "# Title\n\n", rows: 3, cols: 3, withHeaderRow: true, url: "", webhookUrl: "", webhookHeaders: "", language: "typescript", code: "", }, validate: { keyword: (v) => /^[a-z0-9-]+$/.test(v) ? null : t("slash_commands.keyword_format_error"), label: (v) => (v.trim().length > 0 ? null : t("slash_commands.label_required")), url: (v, values) => values.actionType === "embed-url" && !v.startsWith("http") ? t("slash_commands.url_required") : null, webhookUrl: (v, values) => values.actionType === "run-webhook" && !v.startsWith("https://") ? t("slash_commands.webhook_https_required") : null, language: (v, values) => values.actionType === "insert-snippet" && !v.trim() ? t("slash_commands.language_required") : null, }, }); // Populate form when editing an existing command useEffect(() => { if (!initialValues) { form.reset(); return; } const cfg = initialValues.actionConfig as Record; form.setValues({ keyword: initialValues.keyword, label: initialValues.label, description: initialValues.description ?? "", icon: initialValues.icon ?? "", actionType: initialValues.actionType, isEnabled: initialValues.isEnabled, template: typeof cfg["template"] === "string" ? cfg["template"] : JSON.stringify(cfg["template"], null, 2), rows: (cfg["rows"] as number) ?? 3, cols: (cfg["cols"] as number) ?? 3, withHeaderRow: (cfg["withHeaderRow"] as boolean) ?? true, url: (cfg["url"] as string) ?? "", webhookUrl: (cfg["webhookUrl"] as string) ?? "", webhookHeaders: cfg["headers"] ? JSON.stringify(cfg["headers"], null, 2) : "", language: (cfg["language"] as string) ?? "typescript", code: (cfg["code"] as string) ?? "", }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialValues?.id]); function buildActionConfig(values: FormValues): Record { switch (values.actionType) { case "insert-template": return { template: values.template }; case "insert-table": return { rows: values.rows, cols: values.cols, withHeaderRow: values.withHeaderRow, }; case "embed-url": return { url: values.url }; case "run-webhook": { let headers: Record | undefined; if (values.webhookHeaders.trim()) { try { headers = JSON.parse(values.webhookHeaders) as Record; } catch { // Headers JSON is invalid — backend Zod will reject cleanly } } return { webhookUrl: values.webhookUrl, ...(headers ? { headers } : {}) }; } case "insert-snippet": return { language: values.language, code: values.code }; default: return {}; } } function handleSubmit(values: FormValues) { onSubmit({ keyword: values.keyword, label: values.label, description: values.description || undefined, icon: values.icon || undefined, actionType: values.actionType as CreateSlashCommandPayload["actionType"], actionConfig: buildActionConfig(values), isEnabled: values.isEnabled, }); } const actionType = form.values.actionType; return (