import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { usePacks } from "@/hooks/usePacks"; import { useTriggers, useTrigger } from "@/hooks/useTriggers"; import { useActions, useAction } from "@/hooks/useActions"; import { useCreateRule, useUpdateRule } from "@/hooks/useRules"; import ParamSchemaForm, { validateParamSchema, type ParamSchema, } from "@/components/common/ParamSchemaForm"; import type { RuleResponse } from "@/types/api"; import { labelToRef, extractLocalRef, combineRefs } from "@/lib/format-utils"; interface RuleFormProps { rule?: RuleResponse; onSuccess?: () => void; onCancel?: () => void; } export default function RuleForm({ rule, onSuccess, onCancel }: RuleFormProps) { const navigate = useNavigate(); const isEditing = !!rule; // Form state const [packId, setPackId] = useState(rule?.pack || 0); const [localRef, setLocalRef] = useState( rule?.ref ? extractLocalRef(rule.ref) : "", ); const [label, setLabel] = useState(rule?.label || ""); const [description, setDescription] = useState(rule?.description || ""); const [triggerId, setTriggerId] = useState(rule?.trigger || 0); const [actionId, setActionId] = useState(rule?.action || 0); const [conditions, setConditions] = useState( rule?.conditions ? JSON.stringify(rule.conditions, null, 2) : "", ); const [triggerParameters, setTriggerParameters] = useState< Record >(rule?.trigger_params || {}); const [actionParameters, setActionParameters] = useState>( rule?.action_params || {}, ); const [enabled, setEnabled] = useState(rule?.enabled ?? true); const [errors, setErrors] = useState>({}); const [triggerParamErrors, setTriggerParamErrors] = useState< Record >({}); const [actionParamErrors, setActionParamErrors] = useState< Record >({}); // Data fetching const { data: packsData } = usePacks({ pageSize: 1000 }); const packs = packsData?.data || []; const selectedPack = packs.find((p) => p.id === packId); // Fetch ALL triggers and actions from all packs, not just the selected pack // This allows rules in ad-hoc packs to reference triggers/actions from other packs const { data: triggersData } = useTriggers({ pageSize: 1000 }); const { data: actionsData } = useActions({ pageSize: 1000 }); const triggers = triggersData?.data || []; const actions = actionsData?.data || []; // Get selected trigger and action refs for detail fetching const selectedTriggerSummary = triggers.find((t) => t.id === triggerId); const selectedActionSummary = actions.find((a: any) => a.id === actionId); // Fetch full trigger details (including param_schema) when a trigger is selected const { data: triggerDetailsData } = useTrigger( selectedTriggerSummary?.ref || "", ); const selectedTrigger = triggerDetailsData?.data; // Fetch full action details (including param_schema) when an action is selected const { data: actionDetailsData } = useAction( selectedActionSummary?.ref || "", ); const selectedAction = actionDetailsData?.data; // Extract param schemas from full details const triggerParamSchema: ParamSchema = ((selectedTrigger as any)?.param_schema as ParamSchema) || {}; const actionParamSchema: ParamSchema = ((selectedAction as any)?.param_schema as ParamSchema) || {}; // Mutations const createRule = useCreateRule(); const updateRule = useUpdateRule(); // Reset triggers, actions, and parameters when pack changes useEffect(() => { if (!isEditing) { setTriggerId(0); setActionId(0); setTriggerParameters({}); setActionParameters({}); } }, [packId, isEditing]); // Reset trigger parameters when trigger changes useEffect(() => { if (!isEditing) { setTriggerParameters({}); } }, [triggerId, isEditing]); // Reset action parameters when action changes useEffect(() => { if (!isEditing) { setActionParameters({}); } }, [actionId, isEditing]); const validateForm = (): boolean => { const newErrors: Record = {}; if (!localRef.trim()) { newErrors.ref = "Reference is required"; } if (!label.trim()) { newErrors.label = "Label is required"; } if (!description.trim()) { newErrors.description = "Description is required"; } if (!packId) { newErrors.pack = "Pack is required"; } if (!triggerId) { newErrors.trigger = "Trigger is required"; } if (!actionId) { newErrors.action = "Action is required"; } // Validate conditions JSON if provided if (conditions.trim()) { try { JSON.parse(conditions); } catch (e) { newErrors.conditions = "Invalid JSON format"; } } // Validate trigger parameters (allow templates in rule context) const triggerErrors = validateParamSchema( triggerParamSchema, triggerParameters, true, ); setTriggerParamErrors(triggerErrors); // Validate action parameters (allow templates in rule context) const actionErrors = validateParamSchema( actionParamSchema, actionParameters, true, ); setActionParamErrors(actionErrors); setErrors(newErrors); return ( Object.keys(newErrors).length === 0 && Object.keys(triggerErrors).length === 0 && Object.keys(actionErrors).length === 0 ); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) { return; } // Get the selected pack, trigger, and action const selectedPackData = packs.find((p) => p.id === packId); // Combine pack ref and local ref to create full ref const fullRef = combineRefs(selectedPackData?.ref || "", localRef.trim()); const formData: any = { pack_ref: selectedPackData?.ref || "", ref: fullRef, label: label.trim(), description: description.trim(), trigger_ref: selectedTrigger?.ref || "", action_ref: selectedAction?.ref || "", enabled, }; // Only add optional fields if they have values if (conditions.trim()) { formData.conditions = JSON.parse(conditions); } // Add trigger parameters if any if (Object.keys(triggerParameters).length > 0) { formData.trigger_params = triggerParameters; } // Add action parameters if any if (Object.keys(actionParameters).length > 0) { formData.action_params = actionParameters; } try { if (isEditing && rule) { await updateRule.mutateAsync({ ref: rule.ref, data: formData }); } else { const newRuleResponse = await createRule.mutateAsync(formData); if (!onSuccess) { navigate(`/rules/${newRuleResponse.data.ref}`); } } if (onSuccess) { onSuccess(); } } catch (err) { console.error("Failed to save rule:", err); setErrors({ submit: err instanceof Error ? err.message : "Failed to save rule", }); } }; const handleCancel = () => { if (onCancel) { onCancel(); } else { navigate("/rules"); } }; return (
{errors.submit && (

{errors.submit}

)} {/* Basic Information */}

Basic Information

{/* Pack Selection */}
{errors.pack && (

{errors.pack}

)}
{/* Label - MOVED FIRST */}
setLabel(e.target.value)} onBlur={() => { // Auto-populate localRef from label if localRef is empty and not editing if (!isEditing && !localRef.trim() && label.trim()) { setLocalRef(labelToRef(label)); } }} placeholder="e.g., Notify on Error" className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 ${ errors.label ? "border-red-500" : "border-gray-300" }`} /> {errors.label && (

{errors.label}

)}

Human-readable name for display

{/* Reference - MOVED AFTER LABEL with Pack Prefix */}
{selectedPack?.ref || "pack"}. setLocalRef(e.target.value)} placeholder="e.g., notify_on_error" disabled={isEditing} className={errors.ref ? "error" : ""} />
{errors.ref && (

{errors.ref}

)}

Local identifier within the pack. Auto-populated from label.

{/* Description */}