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 }))}
);
}