feat(web): add workflow graph editor and try-mode JSON tree

Add a Graph tab (React Flow) as the default workflow editor view, drop the Mermaid YAML preview, auto-assign step-N ids on add, and show test results as a JSON tree.

Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
Cursor Agent
2026-08-22 05:43:58 +00:00
co-authored by nsrb
parent f9f21052d9
commit 527f9ac869
18 changed files with 1263 additions and 1070 deletions
@@ -9,12 +9,28 @@ import { FormInput, FormSelect, FormTextarea } from "../FormControls.jsx";
import { LogViewer } from "../LogViewer.jsx";
import { StatusBadge } from "../../lib/format";
import { contextFromMeta, prettyJson } from "../../lib/script.js";
import { needsMode } from "../../lib/workflow-doc.js";
import { ConfigFields } from "./ConfigFields.jsx";
import { ConfigTooltip, configValueText, FieldLabel, previewConfigValue, SchemaTooltip } from "./FieldHelp.jsx";
import { ScriptIcon } from "../ScriptIcon.jsx";
import { configHasOverlay, mergeProfileConfig } from "../../lib/profile.js";
export function ScriptCard({
export function ScriptCard(props) {
if (props.sortable === false) {
return <ScriptCardView {...props} drag={null} />;
}
return <ScriptCardSortable {...props} />;
}
function ScriptCardSortable(props) {
const drag = useSortable({
id: props.step.uiId,
disabled: props.disabled,
});
return <ScriptCardView {...props} drag={drag} />;
}
function ScriptCardView({
step,
index,
otherSteps,
@@ -26,18 +42,25 @@ export function ScriptCard({
workflows,
owner,
excludeFile,
sortable = true,
defaultExpanded = false,
drag,
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: step.uiId,
disabled,
});
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = drag ?? {
attributes: {},
listeners: {},
setNodeRef: undefined,
transform: null,
transition: undefined,
isDragging: false,
};
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.6 : 1,
};
const [tryOpen, setTryOpen] = useState(false);
const [expanded, setExpanded] = useState(false);
const [expanded, setExpanded] = useState(defaultExpanded);
const profile = step.profile && profilesByName?.get(step.profile);
const scriptName = profile?.script || step.script;
const listed = scriptsByName?.get(scriptName);
@@ -68,16 +91,18 @@ export function ScriptCard({
>
<div className={`card-body gap-3 ${expanded ? "p-4" : "p-3"}`}>
<div className="flex items-center gap-2">
<button
type="button"
className="btn btn-ghost btn-xs btn-square cursor-grab active:cursor-grabbing"
aria-label={`Drag step ${index + 1}`}
disabled={disabled}
{...attributes}
{...listeners}
>
<LuGripVertical className="size-4 opacity-60" />
</button>
{sortable !== false ? (
<button
type="button"
className="btn btn-ghost btn-xs btn-square cursor-grab active:cursor-grabbing"
aria-label={`Drag step ${index + 1}`}
disabled={disabled}
{...attributes}
{...listeners}
>
<LuGripVertical className="size-4 opacity-60" />
</button>
) : null}
{step.kind === "script" && scriptName ? (
<ScriptIcon name={scriptName} hasIcon={listed?.hasIcon} className="size-8 shrink-0" />
) : null}
@@ -384,12 +409,6 @@ function NeedsMap({ needs, ids, disabled, onChange }) {
);
}
function needsMode(needs) {
if (needs == null) return "none";
if (Array.isArray(needs)) return "list";
return "map";
}
function ScriptTryDialog({ script, config, meta, owner, onClose }) {
const existing = useScript(script);
const dryRun = useDryRunScript();