diff --git a/packages/server/scripts/fetch-binary.js b/packages/server/scripts/fetch-binary.js
index 5f6209a..8712dda 100644
--- a/packages/server/scripts/fetch-binary.js
+++ b/packages/server/scripts/fetch-binary.js
@@ -71,6 +71,7 @@ async function fetchBinary(ctx) {
fetchBinary.meta = {
description: "Download a binary URL into ctx.data as a Buffer",
+ previewConfigKey: "url",
config: {
url: { type: "string", required: false, description: "Direct download URL" },
urlVar: { type: "string", required: false, description: "Key in ctx.data that holds the URL" },
diff --git a/packages/server/scripts/fetch-html.js b/packages/server/scripts/fetch-html.js
index f86a0ed..6947e8f 100644
--- a/packages/server/scripts/fetch-html.js
+++ b/packages/server/scripts/fetch-html.js
@@ -86,6 +86,7 @@ async function fetchHtml(ctx) {
fetchHtml.meta = {
description: "Fetch HTML and optionally select elements or transform with JSONata",
+ previewConfigKey: "url",
config: {
url: { type: "string", required: true, description: "Page URL" },
selector: { type: "string", required: false, description: "CSS selector" },
diff --git a/packages/server/scripts/fetch-http.js b/packages/server/scripts/fetch-http.js
index 76ea114..63d5af9 100644
--- a/packages/server/scripts/fetch-http.js
+++ b/packages/server/scripts/fetch-http.js
@@ -115,6 +115,7 @@ async function fetchHttp(ctx) {
fetchHttp.meta = {
description: "Fetch a URL and store the response body.",
+ previewConfigKey: "url",
config: {
url: { type: "string", required: true, description: "Request URL" },
method: {
diff --git a/packages/server/scripts/fetch-rss-feed.js b/packages/server/scripts/fetch-rss-feed.js
index 64af8ef..ecce454 100644
--- a/packages/server/scripts/fetch-rss-feed.js
+++ b/packages/server/scripts/fetch-rss-feed.js
@@ -68,6 +68,7 @@ async function fetchRssFeed(ctx) {
fetchRssFeed.meta = {
description: "Fetch an RSS/Atom feed and optionally transform it with JSONata",
+ previewConfigKey: "url",
config: {
url: { type: "string", required: false, description: "Feed URL (or pass data.url)" },
outputVar: {
diff --git a/packages/server/scripts/fingerprint.js b/packages/server/scripts/fingerprint.js
index b02424c..1baf860 100644
--- a/packages/server/scripts/fingerprint.js
+++ b/packages/server/scripts/fingerprint.js
@@ -56,6 +56,7 @@ async function fingerprint(ctx) {
fingerprint.meta = {
description:
"Hash a value, compare it to the last stored fingerprint, and skip remaining steps when unchanged",
+ previewConfigKey: "key",
config: {
key: {
type: "string",
diff --git a/packages/server/scripts/get-secret.js b/packages/server/scripts/get-secret.js
index 6091c2f..2212955 100644
--- a/packages/server/scripts/get-secret.js
+++ b/packages/server/scripts/get-secret.js
@@ -22,6 +22,7 @@ async function getSecret(ctx) {
getSecret.meta = {
description:
"Load a named secret for this workflow owner into ctx.data. The value is wrapped and redacted in logs.",
+ previewConfigKey: "name",
config: {
name: {
type: "string",
diff --git a/packages/server/scripts/jsonata.js b/packages/server/scripts/jsonata.js
index ba4f363..5948104 100644
--- a/packages/server/scripts/jsonata.js
+++ b/packages/server/scripts/jsonata.js
@@ -11,6 +11,7 @@ async function jsonataFn(ctx) {
jsonataFn.meta = {
description: "Evaluate a JSONata expression against ctx.data and return the result as the next context",
+ previewConfigKey: "expression",
config: {
expression: { type: "string", required: true, description: "JSONata expression" },
},
diff --git a/packages/server/scripts/ntfy.js b/packages/server/scripts/ntfy.js
index 7555ce8..892db01 100644
--- a/packages/server/scripts/ntfy.js
+++ b/packages/server/scripts/ntfy.js
@@ -131,6 +131,7 @@ async function ntfy(ctx) {
ntfy.meta = {
description: "Send a message or file to an ntfy topic",
+ previewConfigKey: "url",
config: {
url: {
type: "string",
diff --git a/packages/server/scripts/render-template.js b/packages/server/scripts/render-template.js
index 7870055..2c066a5 100644
--- a/packages/server/scripts/render-template.js
+++ b/packages/server/scripts/render-template.js
@@ -79,6 +79,7 @@ async function renderTemplate(ctx) {
renderTemplate.meta = {
description:
"Render an HTML template from Responses (kind: template) with Mustache and return html + plain-text fallback",
+ previewConfigKey: "template",
config: {
template: {
type: "string",
diff --git a/packages/server/scripts/send-email.js b/packages/server/scripts/send-email.js
index e56960e..840bf94 100644
--- a/packages/server/scripts/send-email.js
+++ b/packages/server/scripts/send-email.js
@@ -230,6 +230,7 @@ async function sendEmail(ctx) {
sendEmail.meta = {
description: "Send an email via SMTP (nodemailer); plain text, HTML, or both",
+ previewConfigKey: "from",
config: {
service: {
type: "string",
diff --git a/packages/server/scripts/trigger-workflow.js b/packages/server/scripts/trigger-workflow.js
index 5e78d6b..ff8bdab 100644
--- a/packages/server/scripts/trigger-workflow.js
+++ b/packages/server/scripts/trigger-workflow.js
@@ -28,6 +28,7 @@ async function triggerWorkflow(ctx) {
triggerWorkflow.meta = {
description:
"Fire-and-forget another workflow by YAML name (same owner). Destination must declare triggers: [{ type: workflow }]. Optionally reshape ctx.data with JSONata before sending.",
+ previewConfigKey: "name",
config: {
name: {
type: "string",
diff --git a/packages/server/workflows/default/track.yaml b/packages/server/workflows/default/track.yaml
index c4a70f2..e2f21a7 100644
--- a/packages/server/workflows/default/track.yaml
+++ b/packages/server/workflows/default/track.yaml
@@ -30,3 +30,4 @@ triggers:
- type: HTTP
method: GET
path: /new
+enabled: false
diff --git a/packages/web/src/components/workflow/FieldHelp.jsx b/packages/web/src/components/workflow/FieldHelp.jsx
index cd1edb2..b965868 100644
--- a/packages/web/src/components/workflow/FieldHelp.jsx
+++ b/packages/web/src/components/workflow/FieldHelp.jsx
@@ -4,7 +4,7 @@ export function FieldHelp({ description }) {
if (!description) return null;
const long = String(description).length > 80;
return (
-
+
{long ? (
{description}
@@ -27,3 +27,140 @@ export function FieldLabel({ name, required, description, children }) {
);
}
+
+function fieldNotes(field) {
+ return [
+ field.required ? "required" : null,
+ field.default !== undefined ? `default ${JSON.stringify(field.default)}` : null,
+ Array.isArray(field.enum)
+ ? field.enum.map((x) => (x && typeof x === "object" ? x.value : x)).join(" | ")
+ : Array.isArray(field.options)
+ ? field.options.map((x) => (x && typeof x === "object" ? x.value : x)).join(" | ")
+ : null,
+ field.description,
+ ]
+ .filter(Boolean)
+ .join(" · ");
+}
+
+const TITLE_PREVIEW_MAX = 40;
+const TOOLTIP_VALUE_MAX = 120;
+
+function stringifyConfigValue(val) {
+ if (typeof val === "string") return val.replace(/\s+/g, " ").trim();
+ if (typeof val === "number" || typeof val === "boolean") return String(val);
+ try {
+ return JSON.stringify(val);
+ } catch {
+ return String(val);
+ }
+}
+
+function truncateConfigValue(val, maxLen) {
+ const text = stringifyConfigValue(val);
+ if (!text) return "";
+ if (text.length <= maxLen) return text;
+ return `${text.slice(0, Math.max(0, maxLen - 1))}…`;
+}
+
+/** Collapsed card title suffix from `meta.previewConfigKey`. */
+export function previewConfigValue(config, key, maxLen = TITLE_PREVIEW_MAX) {
+ const text = configValueText(config, key);
+ if (!text) return "";
+ if (text.length <= maxLen) return text;
+ return `${text.slice(0, Math.max(0, maxLen - 1))}…`;
+}
+
+export function configValueText(config, key) {
+ if (typeof key !== "string" || !key.trim()) return "";
+ if (config == null || typeof config !== "object") return "";
+ const val = config[key];
+ if (val == null || val === "") return "";
+ return stringifyConfigValue(val);
+}
+
+/** Formatted DaisyUI tooltip for a step's current config. */
+export function ConfigTooltip({ config, label = "config" }) {
+ const entries =
+ config && typeof config === "object" && !Array.isArray(config) ? Object.entries(config) : [];
+
+ return (
+
+
+
+ {entries.length === 0 ? (
+
No config
+ ) : (
+
+
+
+ | Key |
+ Value |
+
+
+
+ {entries.map(([name, val]) => (
+
+ | {name} |
+
+ {val == null || val === "" ? (
+ —
+ ) : (
+ truncateConfigValue(val, TOOLTIP_VALUE_MAX)
+ )}
+ |
+
+ ))}
+
+
+ )}
+
+
+
+
+ );
+}
+
+/** Formatted DaisyUI tooltip for script.meta.input (or similar field maps). */
+export function SchemaTooltip({ label = "Input", fields }) {
+ const entries = Object.entries(fields ?? {});
+ if (entries.length === 0) return null;
+
+ return (
+
+
+
+
+
+
+ | Field |
+ Type |
+ Notes |
+
+
+
+ {entries.map(([name, spec]) => {
+ const field = spec && typeof spec === "object" ? spec : { description: String(spec) };
+ return (
+
+ |
+ {name}
+ {field.required ? * : null}
+ |
+ {field.type ?? ""} |
+ {fieldNotes(field)} |
+
+ );
+ })}
+
+
+
+
+
+
+ );
+}
diff --git a/packages/web/src/components/workflow/ScriptCard.jsx b/packages/web/src/components/workflow/ScriptCard.jsx
index 06b8131..c4f1630 100644
--- a/packages/web/src/components/workflow/ScriptCard.jsx
+++ b/packages/web/src/components/workflow/ScriptCard.jsx
@@ -1,17 +1,15 @@
import { useEffect, useMemo, useState } from "react";
-import { Link } from "react-router-dom";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
-import { LuGripVertical, LuMaximize2, LuMinimize2, LuPencil, LuPlay, LuTrash2 } from "react-icons/lu";
+import { LuChevronDown, LuGripVertical, LuMaximize2, LuMinimize2, LuPlay, LuTrash2 } from "react-icons/lu";
import { useDryRunScript, useScript } from "../../api/hooks.js";
import { errorMessage } from "../../api/client.js";
import { CodeEditor } from "../CodeEditor.jsx";
import { LogViewer } from "../LogViewer.jsx";
-import { ScriptMetaPanel } from "../ScriptMetaPanel.jsx";
import { StatusBadge } from "../../lib/format.jsx";
import { contextFromMeta, prettyJson } from "../../lib/script.js";
import { ConfigFields } from "./ConfigFields.jsx";
-import { FieldLabel } from "./FieldHelp.jsx";
+import { ConfigTooltip, configValueText, FieldLabel, previewConfigValue, SchemaTooltip } from "./FieldHelp.jsx";
export function ScriptCard({
step,
@@ -35,24 +33,32 @@ export function ScriptCard({
opacity: isDragging ? 0.6 : 1,
};
const [tryOpen, setTryOpen] = useState(false);
+ const [expanded, setExpanded] = useState(false);
const listed = scriptsByName?.get(step.script);
const meta = listed?.meta ?? null;
- const metaError = listed?.metaError ?? null;
const duplicateId =
step.id && otherSteps.some((s) => s.id === step.id && s.uiId !== step.uiId);
+ const preview = previewConfigValue(step.config, meta?.previewConfigKey);
+ const previewFull = configValueText(step.config, meta?.previewConfigKey);
+ const baseName = step.kind === "set" ? `set:${step.as || "…"}` : step.script || "untitled";
+ const titleFull = previewFull ? `${baseName} (${previewFull})` : baseName;
+ const setConfig =
+ step.kind === "set" ? { as: step.as ?? "", expression: step.expression ?? "" } : null;
return (
-
-
+
+
-
-
- {step.kind === "set" ? `set:${step.as || "…"}` : step.script || "untitled"}
-
- {step.id ?
{step.id} : null}
- {step.kind === "script" && meta?.description ? (
+
+
+ {step.kind === "script" ? (
+
+ ) : null}
+ {!expanded ? (
+
+ ) : null}
{step.kind === "script" && step.script ? (
- <>
-
-
-
-
- >
+
) : null}
+
- {step.kind === "script" && (meta?.input || meta?.output) ? (
-
-
- Input / output
-
-
-
-
-
- ) : null}
-
- {step.kind === "set" ? (
-
-
- onChange({ ...step, as: e.target.value })}
- />
-
-
- ) : (
-
onChange({ ...step, config })}
- />
- )}
-
-
- Advanced
-
-
+
+
+ Advanced
+
+
+ {step.kind === "set" ? null : (
+
+ )}
+
+
+
+ >
+ ) : null}
{tryOpen ? (
-
-
+
+
+
-
-
{typeLabel(type)}
-
+
+
- {type === "HTTP" ? (
-
- ) : null}
- {type === "cron" ? (
-
- ) : null}
- {type === "workflow" ? (
-
- Other workflows can call this one with trigger-workflow.js.
- No extra fields.
-
- ) : null}
- {type !== "HTTP" && type !== "cron" && type !== "workflow" ? (
-
Unknown trigger type. Edit it in the YAML tab.
+ {expanded ? (
+ <>
+ {type === "HTTP" ? (
+
+ ) : null}
+ {type === "cron" ? (
+
+ ) : null}
+ {type === "workflow" ? (
+
+ Other workflows can call this one with trigger-workflow.js.
+ No extra fields.
+
+ ) : null}
+ {type !== "HTTP" && type !== "cron" && type !== "workflow" ? (
+
Unknown trigger type. Edit it in the YAML tab.
+ ) : null}
+ >
) : null}
);
}
+function triggerSummary(trigger, owner) {
+ const type = trigger?.type;
+ if (type === "HTTP") {
+ return `${trigger.method || "POST"} ${namespacedPath(owner || "owner", trigger.path || "/")}`;
+ }
+ if (type === "cron") return trigger.schedule || "";
+ if (type === "workflow") return "callable";
+ return "";
+}
+
function typeLabel(type) {
if (type === "HTTP") return "HTTP";
if (type === "cron") return "Cron";
diff --git a/packages/web/src/components/workflow/WorkflowTestPanel.jsx b/packages/web/src/components/workflow/WorkflowTestPanel.jsx
index d3fa041..ec4e6d4 100644
--- a/packages/web/src/components/workflow/WorkflowTestPanel.jsx
+++ b/packages/web/src/components/workflow/WorkflowTestPanel.jsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
-import { LuPlay } from "react-icons/lu";
+import { LuMaximize2, LuMinimize2, LuPlay } from "react-icons/lu";
import { errorMessage } from "../../api/client.js";
import { useRunWorkflow } from "../../api/hooks.js";
import { CodeEditor } from "../CodeEditor.jsx";
@@ -13,12 +13,14 @@ export function WorkflowTestPanel({
defaultData,
disabled,
disabledReason,
- testKey = 0,
+ open,
+ onClose,
}) {
const run = useRunWorkflow();
const [dataJson, setDataJson] = useState(() => prettyJson(defaultData ?? {}));
const [parseError, setParseError] = useState(null);
const [last, setLast] = useState(null);
+ const [expanded, setExpanded] = useState(false);
useEffect(() => {
setDataJson(prettyJson(defaultData ?? {}));
@@ -61,28 +63,52 @@ export function WorkflowTestPanel({
? prettyJson(last.error ? { error: last.error } : last.result)
: "";
+ function close() {
+ setExpanded(false);
+ onClose?.();
+ }
+
return (
-
0 ? true : undefined}
- >
-
- Test
-
-
-
- Runs the saved YAML and writes an Event. Manual run works even when the
- workflow is disabled. Save before testing unsaved edits.
-
- {disabledReason ?
{disabledReason}
: null}
- {parseError ?
{parseError}
: null}
-
-
-
data
-
+