"use client"; /** * Automations editor — CONDITION section (0..1 condition tree). * * Minimum required by the spec: a "No condition" toggle + a single * leaf-predicate row (field dropdown record-property/event-field, operator, * value). Advanced mode is a raw-JSON textarea for the full tree * ({ op: AND|OR, children[] } / { op: NOT, child } / predicate), validated * client-side with zod (mirroring @factory/shared/automation's * CONDITION_EXPRESSION_SCHEMA) before save; server 422 details are surfaced * below the editor. */ import { useState } from "react"; import { Plus, Trash2 } from "lucide-react"; import { z } from "zod"; import { inputCls } from "@/components/settings/settings-shell"; import { CONDITION_OPERATORS, type ConditionDraft, type ConditionOperator } from "./lib"; /** Local mirror of the shared condition expression schema (zod 3, client-safe). */ const PREDICATE = z .object({ field: z.object({ ref: z.enum(["event", "property"]), name: z.string().min(1).max(200) }).strict(), operator: z.enum(["eq", "neq", "contains", "not_contains", "starts_with", "ends_with", "gt", "gte", "lt", "lte", "in", "not_in", "is_empty", "is_not_empty"]), value: z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.union([z.string(), z.number(), z.boolean()]))]).optional() }) .strict(); export const CONDITION_TREE: z.ZodType = z.lazy(() => z.union([ z .object({ op: z.enum(["AND", "OR"]), children: z.array(CONDITION_TREE).min(1).max(100) }) .strict(), z.object({ op: z.literal("NOT"), child: CONDITION_TREE }).strict(), PREDICATE ]) ); /** Validate a raw-JSON condition string. Returns the first issue message or null. */ export function validateConditionJson(raw: string): string | null { let parsed: unknown; try { parsed = JSON.parse(raw); } catch (e) { return `Invalid JSON: ${(e as Error).message}`; } const r = CONDITION_TREE.safeParse(parsed); if (r.success) return null; const first = r.error.issues[0]; return first ? `Invalid condition: ${first.path.join(".") || "condition"} — ${first.message}` : "Invalid condition."; } interface ConditionEditorProps { draft: ConditionDraft; onChange: (d: ConditionDraft) => void; } export function ConditionEditor({ draft, onChange }: ConditionEditorProps) { const [mode, setMode] = useState<"simple" | "json">(draft.json.trim() ? "json" : "simple"); const [localError, setLocalError] = useState(null); const simpleMode = mode === "simple"; const showValue = simpleMode && draft.operator !== "exists"; const setField = (patch: Partial) => onChange({ ...draft, ...(simpleMode ? { json: "" } : {}), ...patch }); const switchMode = (m: "simple" | "json") => { setLocalError(null); // Leaving JSON mode discards the raw text so the simple row drives save // (no stale-tree shadowing). Entering JSON mode seeds the editor below. onChange({ ...draft, ...(m === "simple" ? { json: "" } : {}) }); setMode(m); }; const toJsonText = () => { const trimmed = draft.json.trim(); if (trimmed) return trimmed; const opMap: Record = { "=": "eq", "!=": "neq", ">": "gt", "<": "lt", contains: "contains", exists: "is_not_empty" }; const predicate: Record = { field: { ref: draft.fieldRef, name: draft.fieldName || "property" }, operator: opMap[draft.operator] }; if (draft.operator !== "exists" && draft.value.trim() !== "") predicate.value = draft.value; return JSON.stringify(predicate, null, 2); }; return (
{!draft.hasCondition ? ( ) : ( <>
{simpleMode ? (
) : (