import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { LuEye, LuEyeOff, LuExternalLink, LuPlus, LuTrash2 } from "react-icons/lu"; import { useVariables } from "../../api/hooks.js"; import { triggerDestinations } from "../../lib/workflow-doc.js"; import { prettyJson } from "../../lib/script.js"; import { FormInput, FormSelect, FormTextarea } from "../FormControls.jsx"; import { FieldLabel } from "./FieldHelp.jsx"; const MULTILINE_KEYS = new Set(["expression", "jsonata"]); const VAR_PEEK_MAX = 48; const CONFIG_REF_PREFIXES = [ { prefix: "$SECRET_", label: "secret" }, { prefix: "$CONTEXT_", label: "context" }, { prefix: "$VAR_", label: "variable" }, ]; function describeConfigRef(value) { if (typeof value !== "string") return null; const trimmed = value.trim(); for (const { prefix, label } of CONFIG_REF_PREFIXES) { if (trimmed.startsWith(prefix) && trimmed.length > prefix.length) { return { label, name: trimmed.slice(prefix.length), kind: prefix }; } } return null; } function formatVarDisplay(value) { if (typeof value === "string") return value === "" ? '""' : value; return String(value); } function truncatePeek(text, maxLen = VAR_PEEK_MAX) { if (!text) return ""; if (text.length <= maxLen) return text; return `${text.slice(0, Math.max(0, maxLen - 1))}…`; } /** Edit-time lookup for `$VAR_` against workflow owner (not a runtime guarantee). */ function lookupVariable(variables, owner, name) { const list = Array.isArray(variables) ? variables : []; const match = list.find((v) => v.owner === owner && v.name === name); if (match) { return { status: "found", value: match.value, type: match.type }; } const otherOwners = [ ...new Set(list.filter((v) => v.name === name && v.owner !== owner).map((v) => v.owner)), ]; if (otherOwners.length > 0) { return { status: "other_owner", otherOwners }; } return { status: "missing" }; } function variablesDeepLink({ owner, name, missing }) { if (missing) { const params = new URLSearchParams(); if (owner) params.set("owner", owner); if (name) params.set("name", name); const q = params.toString(); return q ? `/variables/new?${q}` : "/variables/new"; } return `/variables/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/edit`; } function ConfigRefHint({ value, owner }) { const ref = describeConfigRef(value); const isVar = ref?.kind === "$VAR_"; const { data: variables = [], isPending } = useVariables(undefined, { enabled: isVar }); const [revealed, setRevealed] = useState(false); useEffect(() => { setRevealed(false); }, [value, owner]); if (!ref) return null; if (!isVar) { return (

from {ref.label} {ref.name}

); } const lookup = !owner ? { status: "missing" } : isPending ? { status: "loading" } : lookupVariable(variables, owner, ref.name); const linkTo = variablesDeepLink({ owner: owner || "", name: ref.name, missing: lookup.status !== "found", }); let statusNote = null; if (lookup.status === "missing") { statusNote = (missing); } else if (lookup.status === "other_owner") { statusNote = ( (missing · other owner: {lookup.otherOwners.join(", ")}) ); } const fullText = lookup.status === "found" ? formatVarDisplay(lookup.value) : ""; const peek = truncatePeek(fullText); const masked = lookup.status === "found" ? "******" : null; return (
from variable {ref.name} {lookup.status === "found" ? ":" : null} {lookup.status === "found" ? ( <> {revealed ? peek : masked} ) : null} {statusNote} {lookup.status === "loading" ? null : ( {lookup.status === "found" ? "Open Variables" : "Create on Variables"} )}
); } function fieldSpec(meta, key) { const spec = meta?.config?.[key]; if (spec && typeof spec === "object") return spec; return {}; } /** Support meta `enum` or `options`: string[], or { value, label }[]. */ function enumOptions(spec) { const raw = spec?.enum ?? spec?.options; if (!Array.isArray(raw) || raw.length === 0) return null; return raw .map((item) => { if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") { return { value: item, label: String(item) }; } if (item && typeof item === "object" && item.value != null) { return { value: item.value, label: item.label != null ? String(item.label) : String(item.value) }; } return null; }) .filter(Boolean); } function EditableText({ value, onCommit, className = "", placeholder = "…", disabled }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(value ?? ""); useEffect(() => { setDraft(value ?? ""); }, [value]); if (!editing) { return ( ); } return ( setDraft(e.target.value)} onBlur={() => { onCommit(draft); setEditing(false); }} onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); if (e.key === "Escape") { setDraft(value ?? ""); setEditing(false); } }} /> ); } function ValueEditor({ value, onChange, spec, script, fieldKey, workflows, owner, excludeFile, disabled }) { const type = spec?.type ?? (value != null && typeof value === "object" ? "object" : "string"); if (script === "trigger-workflow.js" && fieldKey === "name") { return ( ); } const options = enumOptions(spec); if (options) { const fallback = spec?.default ?? (spec?.required ? options[0].value : ""); const current = value === undefined || value === null ? fallback : value; const known = options.some((o) => o.value === current); return ( { const next = e.target.value; if (next === "") { onChange(undefined); return; } const match = options.find((o) => String(o.value) === next); onChange(match ? match.value : next); }} disabled={disabled} > {spec?.required ? null : } {options.map((o) => ( ))} {known || current === "" || current == null ? null : ( )} ); } if (type === "boolean") { return ( onChange(e.target.checked)} /> ); } if (type === "number") { return ( { const raw = e.target.value; if (raw === "") { onChange(undefined); return; } const n = Number(raw); onChange(Number.isNaN(n) ? raw : n); }} /> ); } if (type === "object" || (value != null && typeof value === "object" && !Array.isArray(value) && type !== "any")) { const obj = value && typeof value === "object" && !Array.isArray(value) ? value : {}; return ; } if (type === "any") { const text = typeof value === "string" ? value : value === undefined ? "" : prettyJson(value) || ""; return ( { if (next.trim() === "") { onChange(undefined); return; } try { onChange(JSON.parse(next)); } catch { onChange(next); } }} disabled={disabled} /> ); } const str = value == null ? "" : String(value); const multiline = MULTILINE_KEYS.has(fieldKey) || str.includes("\n"); if (multiline) { return ( onChange(e.target.value)} /> ); } return ; } function JsonOrTextArea({ text, onCommit, disabled }) { const [draft, setDraft] = useState(text); useEffect(() => { setDraft(text); }, [text]); return ( setDraft(e.target.value)} onBlur={() => onCommit(draft)} /> ); } function ObjectFields({ value, onChange, owner }) { const entries = Object.entries(value ?? {}); function setKey(oldKey, nextKey) { if (!nextKey || nextKey === oldKey) return; const next = {}; for (const [k, v] of Object.entries(value ?? {})) { next[k === oldKey ? nextKey : k] = v; } onChange(next); } function setVal(key, val) { onChange({ ...value, [key]: val }); } function remove(key) { const next = { ...value }; delete next[key]; onChange(next); } function add() { let key = "key"; let i = 1; while (key in (value ?? {})) { key = `key${i}`; i += 1; } onChange({ ...value, [key]: "" }); } return (
{entries.map(([k, v]) => (
setKey(k, nk)} className="font-mono text-xs" />
{v != null && typeof v === "object" ? ( setVal(k, nv)} owner={owner} /> ) : (
setVal(k, nv)} className="font-mono text-xs" />
)}
))}
); } function WorkflowNamePicker({ value, onChange, workflows, owner, excludeFile, disabled }) { const dest = triggerDestinations(workflows, { owner, excludeFile }); const names = dest.map((w) => w.name); const known = names.includes(value); const [custom, setCustom] = useState(!known && Boolean(value)); const selectValue = custom || (!known && value) ? "__custom__" : value || ""; return (
{ const v = e.target.value; if (v === "__custom__") { setCustom(true); return; } setCustom(false); onChange(v); }} > {dest.map((w) => ( ))} {selectValue === "__custom__" ? ( onChange(e.target.value)} placeholder="workflow name" /> ) : null}
); } export function ConfigFields({ script, config, meta, onChange, workflows, owner, excludeFile, disabled, inheritedConfig = null, }) { const cfg = config && typeof config === "object" && !Array.isArray(config) ? config : {}; const inherited = inheritedConfig && typeof inheritedConfig === "object" && !Array.isArray(inheritedConfig) ? inheritedConfig : null; const metaKeys = Object.keys(meta?.config ?? {}); const extraKeys = Object.keys(cfg).filter((k) => !metaKeys.includes(k)); const inheritedExtraKeys = inherited ? Object.keys(inherited).filter((k) => !metaKeys.includes(k) && !(k in cfg)) : []; function setField(key, value) { const spec = fieldSpec(meta, key); const next = { ...cfg }; const required = Boolean(spec.required) && !inherited; if (value === undefined || (value === "" && !required)) { delete next[key]; } else { next[key] = value; } onChange(next); } function resetField(key) { const next = { ...cfg }; delete next[key]; onChange(next); } function renameExtra(oldKey, nextKey) { if (!nextKey || nextKey === oldKey) return; const next = {}; for (const [k, v] of Object.entries(cfg)) { next[k === oldKey ? nextKey : k] = v; } onChange(next); } function addExtra() { let key = "key"; let i = 1; while (key in cfg || (inherited && key in inherited)) { key = `key${i}`; i += 1; } onChange({ ...cfg, [key]: "" }); } function renderField(key, { extra = false, inheritedOnly = false } = {}) { const spec = extra ? { type: "string" } : fieldSpec(meta, key); const overridden = Boolean(inherited && Object.prototype.hasOwnProperty.call(cfg, key)); const displayValue = inheritedOnly ? inherited?.[key] : overridden || !inherited ? cfg[key] : inherited[key]; const unknown = extra && metaKeys.length > 0 && !Object.prototype.hasOwnProperty.call(meta?.config ?? {}, key); return (
{overridden ? ( overridden ) : inherited && !inheritedOnly ? ( from profile ) : null} {unknown ? ( not in schema ) : null} {overridden ? ( ) : null} setField(key, v)} spec={extra ? { type: displayValue != null && typeof displayValue === "object" ? "object" : "string" } : spec} script={script} fieldKey={key} workflows={workflows} owner={owner} excludeFile={excludeFile} disabled={disabled} />
); } return (
{metaKeys.map((key) => renderField(key))} {extraKeys.map((key) => (
renameExtra(key, nk)} className="font-mono text-sm" disabled={disabled} /> {inherited ? overridden : null} {metaKeys.length > 0 ? ( not in schema ) : null}
setField(key, v)} spec={{ type: cfg[key] != null && typeof cfg[key] === "object" ? "object" : "string" }} script={script} fieldKey={key} workflows={workflows} owner={owner} excludeFile={excludeFile} disabled={disabled} />
))} {inheritedExtraKeys.map((key) => renderField(key, { extra: true, inheritedOnly: true }))}
); }