diff --git a/packages/web/src/components/workflow/ConfigFields.jsx b/packages/web/src/components/workflow/ConfigFields.jsx index 365a4f4..373fcd1 100644 --- a/packages/web/src/components/workflow/ConfigFields.jsx +++ b/packages/web/src/components/workflow/ConfigFields.jsx @@ -1,10 +1,13 @@ import { useEffect, useState } from "react"; -import { LuPlus, LuTrash2 } from "react-icons/lu"; +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 { FieldLabel } from "./FieldHelp.jsx"; const MULTILINE_KEYS = new Set(["expression", "jsonata"]); +const VAR_PEEK_MAX = 48; const CONFIG_REF_PREFIXES = [ { prefix: "$SECRET_", label: "secret" }, @@ -17,19 +20,131 @@ function describeConfigRef(value) { 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) }; + return { label, name: trimmed.slice(prefix.length), kind: prefix }; } } return null; } -function ConfigRefHint({ value }) { +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 {ref.label} {ref.name} -
+