Redesign workflow editor

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-13 14:11:51 -07:00
parent 75019b0906
commit b883ac557b
5 changed files with 467 additions and 207 deletions
@@ -153,8 +153,8 @@ export function WorkflowDialog({
return (
<>
<Dialog onOpenChange={handleOpenChange} open={open}>
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-xl">
<DialogHeader className="flex-shrink-0">
<DialogContent className="flex h-[88vh] max-h-[88vh] w-[calc(100vw-2rem)] max-w-6xl flex-col gap-0 overflow-hidden p-0">
<DialogHeader className="flex-shrink-0 border-b border-border px-6 py-5 pr-14">
<DialogTitle>{TITLES[mode]}</DialogTitle>
<DialogDescription>
{mode === "edit"
@@ -165,7 +165,7 @@ export function WorkflowDialog({
</DialogDescription>
</DialogHeader>
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto">
<div className="min-h-0 flex-1">
<WorkflowFormBuilder
activationLabel={
mode === "edit" ? "Workflow enabled" : "Enable after creation"
@@ -206,15 +206,15 @@ export function WorkflowDialog({
}
yaml={yamlDefinition}
/>
{mutation.error instanceof Error ? (
<p className="rounded-xl border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{mutation.error.message}
</p>
) : null}
</div>
<div className="flex flex-shrink-0 justify-end gap-2 border-t border-border pt-4">
{mutation.error instanceof Error ? (
<p className="mx-6 mb-3 rounded-xl border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{mutation.error.message}
</p>
) : null}
<div className="flex flex-shrink-0 justify-end gap-2 border-t border-border px-6 py-4">
<Button
onClick={() => handleOpenChange(false)}
type="button"
@@ -1,7 +1,25 @@
import { Code, Plus, Zap } from "lucide-react";
import {
ChevronDown,
Code,
GitBranch,
Plus,
Trash2,
X,
Zap,
} from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import * as React from "react";
import { Button } from "@/shared/ui/button";
import { cn } from "@/shared/lib/cn";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { Input } from "@/shared/ui/input";
import { Switch } from "@/shared/ui/switch";
import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
@@ -10,6 +28,8 @@ import { WorkflowStepCard } from "./WorkflowStepCard";
import { FieldLabel, FormSelect } from "./workflowFormPrimitives";
import {
DEFAULT_FORM_STATE,
ACTION_LABELS,
ACTION_TYPES,
TRIGGER_LABELS,
TRIGGER_TYPES,
formStateToYaml,
@@ -17,6 +37,7 @@ import {
yamlToFormState,
} from "./workflowFormTypes";
import type {
ActionType,
StepFormState,
TriggerConfig,
TriggerType,
@@ -128,41 +149,109 @@ type WorkflowFormBuilderProps = {
yaml: string;
};
function FormSection({
children,
type SelectedNode =
| { type: "trigger" }
| { type: "step"; index: number }
| null;
function WorkflowNode({
connectsToNext,
description,
disabled,
icon,
label,
onAddAfter,
onClick,
selected,
title,
}: {
children: React.ReactNode;
description?: string;
connectsToNext: boolean;
description: string;
disabled?: boolean;
icon: React.ReactNode;
label: string;
onAddAfter: (action: ActionType) => void;
onClick: () => void;
selected: boolean;
title: string;
}) {
return (
<section className="space-y-3 border-t border-border/70 pt-4">
<div className="space-y-0.5">
<h3 className="text-sm font-semibold text-foreground">{title}</h3>
{description ? (
<p className="text-xs text-muted-foreground">{description}</p>
) : null}
</div>
{children}
</section>
);
}
<li className="flex flex-col items-center">
<button
aria-label={label}
aria-pressed={selected}
className={cn(
"flex w-full items-center gap-3 rounded-xl border bg-background px-4 py-3 text-left shadow-sm transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
selected
? "border-foreground/60 bg-muted/50 ring-1 ring-foreground/10"
: "border-border hover:border-muted-foreground/50 hover:bg-muted/30",
)}
disabled={disabled}
onClick={onClick}
type="button"
>
<span
className={cn(
"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border bg-muted/40 text-muted-foreground",
selected && "border-foreground/30 text-foreground",
)}
>
{icon}
</span>
<span className="min-w-0 flex-1">
<span className="block text-xs font-medium uppercase tracking-wide text-muted-foreground">
{title}
</span>
<span className="block truncate text-sm font-semibold text-foreground">
{description}
</span>
</span>
</button>
function FlowNode({
children,
marker,
}: {
children: React.ReactNode;
marker: React.ReactNode;
}) {
return (
<li className="relative grid grid-cols-[2rem_minmax(0,1fr)] gap-3 pb-4 last:pb-0">
<div className="relative z-10 flex h-8 w-8 items-center justify-center rounded-full border border-muted-foreground/50 bg-background text-xs font-semibold text-foreground">
{marker}
</div>
<div className="min-w-0">{children}</div>
<span className="relative flex h-14 items-center justify-center">
<span
aria-hidden
className={cn(
"absolute left-1/2 top-0 w-px -translate-x-1/2 bg-muted-foreground/40",
connectsToNext ? "bottom-0" : "h-1/2",
)}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label={
title === "Trigger" ? "Add step" : `Add after ${title}`
}
className="relative z-10 h-7 w-7 rounded-full bg-background shadow-sm"
disabled={disabled}
size="icon"
type="button"
variant="outline"
>
<Plus className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center" side="right" sideOffset={8}>
<DropdownMenuLabel>Add action</DropdownMenuLabel>
<DropdownMenuSeparator />
{ACTION_TYPES.map((action) => (
<DropdownMenuItem
key={action}
onSelect={() => onAddAfter(action)}
>
{ACTION_LABELS[action]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
{connectsToNext ? (
<ChevronDown
aria-hidden
className="absolute -bottom-1 left-1/2 h-4 w-4 -translate-x-1/2 text-muted-foreground"
/>
) : null}
</span>
</li>
);
}
@@ -191,6 +280,8 @@ export function WorkflowFormBuilder({
? initialParseRef.current.error
: null,
);
const [selectedNode, setSelectedNode] = React.useState<SelectedNode>(null);
const shouldReduceMotion = useReducedMotion();
const updateFormState = React.useCallback(
(next: WorkflowFormState) => {
@@ -207,6 +298,7 @@ export function WorkflowFormBuilder({
if (nextMode === "yaml") {
setMode("yaml");
setParseError(null);
setSelectedNode(null);
return;
}
@@ -224,15 +316,25 @@ export function WorkflowFormBuilder({
[mode, yaml],
);
const addStep = React.useCallback(() => {
updateFormState({
...formState,
steps: [
...formState.steps,
{ id: nextStepId(formState.steps), action: "delay" },
],
});
}, [formState, updateFormState]);
const insertStep = React.useCallback(
(index: number, action: ActionType) => {
const nextSteps = [...formState.steps];
const newStep: StepFormState = {
id: nextStepId(formState.steps),
action,
};
if (action === "call_webhook") {
newStep.method = "POST";
}
nextSteps.splice(index, 0, newStep);
updateFormState({
...formState,
steps: nextSteps,
});
setSelectedNode({ type: "step", index });
},
[formState, updateFormState],
);
const removeStep = React.useCallback(
(index: number) => {
@@ -240,6 +342,14 @@ export function WorkflowFormBuilder({
...formState,
steps: formState.steps.filter((_, i) => i !== index),
});
setSelectedNode((current) => {
if (current?.type !== "step") return current;
if (current.index === index) return null;
if (current.index > index) {
return { type: "step", index: current.index - 1 };
}
return current;
});
},
[formState, updateFormState],
);
@@ -253,9 +363,17 @@ export function WorkflowFormBuilder({
[formState, updateFormState],
);
const selectedStep =
selectedNode?.type === "step"
? formState.steps[selectedNode.index]
: undefined;
return (
<div className="space-y-5">
<div className="flex items-center justify-end">
<div className="flex h-full min-h-0 flex-col">
<div className="flex flex-shrink-0 items-center justify-between border-b border-border px-6 py-3">
<p className="text-xs text-muted-foreground">
Build the sequence, then select a node to configure it.
</p>
<Tabs onValueChange={handleModeChange} value={mode}>
<TabsList aria-label="Workflow editor mode" className="h-8 p-0.5">
<TabsTrigger
@@ -278,19 +396,19 @@ export function WorkflowFormBuilder({
</div>
{parseError ? (
<p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
<p className="mx-6 mt-3 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
Cannot switch to form view: {parseError}
</p>
) : null}
{mode === "yaml" ? (
<div className="space-y-4">
{scopeField}
<div className="space-y-1.5">
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-6">
<div className="max-w-md">{scopeField}</div>
<div className="flex h-full min-h-[320px] flex-col space-y-1.5">
<Textarea
aria-label="Workflow YAML"
autoCapitalize="off"
className="min-h-[240px] resize-y font-mono text-xs"
className="min-h-[320px] flex-1 resize-none font-mono text-xs"
disabled={disabled}
onChange={(event) => onChange(event.target.value)}
value={yaml}
@@ -301,11 +419,9 @@ export function WorkflowFormBuilder({
</div>
</div>
) : (
<div className="space-y-5">
<FormSection title="Basics">
<div
className={scopeField ? "grid gap-3 sm:grid-cols-2" : undefined}
>
<div className="flex min-h-0 flex-1">
<div className="flex min-w-0 flex-1 flex-col">
<div className="grid flex-shrink-0 grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_auto] items-end gap-3 border-b border-border bg-muted/10 px-6 py-4">
<div className="space-y-1.5">
<FieldLabel htmlFor="wf-name">Workflow name</FieldLabel>
<Input
@@ -320,142 +436,241 @@ export function WorkflowFormBuilder({
value={formState.name}
/>
</div>
{scopeField}
</div>
<div className="space-y-1.5">
<FieldLabel htmlFor="wf-description">
Description (optional)
</FieldLabel>
<Textarea
autoCapitalize="off"
className="min-h-[60px] resize-y text-sm"
disabled={disabled}
id="wf-description"
onChange={(event) =>
updateFormState({
...formState,
description: event.target.value,
})
}
placeholder="What does this workflow do?"
value={formState.description}
/>
</div>
</FormSection>
<div className="space-y-1.5">
<FieldLabel htmlFor="wf-description">
Description (optional)
</FieldLabel>
<Input
autoCapitalize="off"
disabled={disabled}
id="wf-description"
onChange={(event) =>
updateFormState({
...formState,
description: event.target.value,
})
}
placeholder="What does this workflow do?"
value={formState.description}
/>
</div>
<FormSection
description="Events move from the trigger through each step in order."
title="Workflow"
>
<div className="relative">
<div
aria-hidden
className="absolute bottom-4 left-4 top-4 w-0.5 -translate-x-1/2 bg-muted-foreground/40"
/>
<ol aria-label="Workflow sequence" className="relative">
<FlowNode marker={<Zap className="h-4 w-4" />}>
<div className="space-y-3 rounded-lg border border-border/70 bg-muted/10 p-3">
<div className="space-y-0.5">
<p className="text-sm font-medium text-foreground">
Trigger
</p>
<p className="text-xs text-muted-foreground">
Starts the workflow
</p>
</div>
<div className="space-y-1.5">
<FieldLabel htmlFor="wf-trigger-type">Event</FieldLabel>
<FormSelect
disabled={disabled}
id="wf-trigger-type"
onChange={(value) =>
updateFormState({
...formState,
trigger: { on: value as TriggerType },
})
}
value={formState.trigger.on}
>
{TRIGGER_TYPES.map((type) => (
<option key={type} value={type}>
{TRIGGER_LABELS[type]}
</option>
))}
</FormSelect>
</div>
<TriggerConfigFields
onUpdate={(trigger) =>
updateFormState({ ...formState, trigger })
}
trigger={formState.trigger}
/>
</div>
</FlowNode>
{formState.steps.map((step, index) => (
<FlowNode key={step.id} marker={index + 1}>
<WorkflowStepCard
disabled={disabled}
index={index}
onRemove={() => removeStep(index)}
onUpdate={(updated) => updateStep(index, updated)}
step={step}
triggerType={formState.trigger.on}
/>
</FlowNode>
))}
<FlowNode marker={<Plus className="h-4 w-4" />}>
<Button
className="h-auto w-full justify-start whitespace-normal border-dashed px-3 py-3 text-left"
<div className="space-y-1.5">
<p className="text-xs font-medium text-foreground">
Activation
</p>
<div className="flex h-9 items-center justify-between gap-3 rounded-md border border-input bg-background px-3">
<label
className="text-sm text-foreground"
htmlFor="wf-enabled"
>
{activationLabel}
</label>
<Switch
checked={formState.enabled}
disabled={disabled}
onClick={addStep}
type="button"
variant="outline"
>
<span className="space-y-0.5">
<span className="block text-sm font-medium">
Add step
</span>
<span className="block text-xs font-normal text-muted-foreground">
Runs after{" "}
{formState.steps.length > 0
? `step ${formState.steps.length}`
: "the trigger"}
</span>
</span>
</Button>
</FlowNode>
</ol>
</div>
</FormSection>
<FormSection title="Activation">
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.5">
<label
className="block text-sm font-medium text-foreground"
htmlFor="wf-enabled"
>
{activationLabel}
</label>
<p className="text-xs text-muted-foreground">
Turn this off to keep the workflow paused.
</p>
id="wf-enabled"
onCheckedChange={(checked) =>
updateFormState({ ...formState, enabled: checked })
}
/>
</div>
</div>
<Switch
checked={formState.enabled}
disabled={disabled}
id="wf-enabled"
onCheckedChange={(checked) =>
updateFormState({ ...formState, enabled: checked })
}
/>
</div>
</FormSection>
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-5">
<div className="mx-auto w-full max-w-sm">
<div className="mb-4 flex items-center justify-between gap-4">
<div>
<h3 className="text-sm font-semibold text-foreground">
Workflow sequence
</h3>
<p className="text-xs text-muted-foreground">
Steps run from top to bottom.
</p>
</div>
{!selectedNode ? (
<p className="text-xs text-muted-foreground">
Select a node to edit
</p>
) : null}
</div>
<ol aria-label="Workflow sequence">
<WorkflowNode
connectsToNext={formState.steps.length > 0}
description={TRIGGER_LABELS[formState.trigger.on]}
disabled={disabled}
icon={<Zap className="h-4 w-4" />}
label={`Trigger: ${TRIGGER_LABELS[formState.trigger.on]}`}
onAddAfter={(action) => insertStep(0, action)}
onClick={() => setSelectedNode({ type: "trigger" })}
selected={selectedNode?.type === "trigger"}
title="Trigger"
/>
{formState.steps.map((step, index) => {
const nodeTitle =
step.name?.trim() || ACTION_LABELS[step.action];
return (
<WorkflowNode
connectsToNext
description={nodeTitle}
disabled={disabled}
icon={<GitBranch className="h-4 w-4" />}
key={step.id}
label={`Step ${index + 1}: ${nodeTitle}`}
onAddAfter={(action) => insertStep(index + 1, action)}
onClick={() => setSelectedNode({ type: "step", index })}
selected={
selectedNode?.type === "step" &&
selectedNode.index === index
}
title={`Step ${index + 1}`}
/>
);
})}
{formState.steps.length > 0 ? (
<li className="flex justify-center">
<span className="rounded-full border border-border bg-muted/30 px-5 py-1.5 text-xs font-medium text-muted-foreground">
End
</span>
</li>
) : null}
</ol>
</div>
</div>
</div>
<AnimatePresence initial={false}>
{selectedNode ? (
<motion.aside
animate={{ opacity: 1, width: "24rem", x: 0 }}
className="flex flex-shrink-0 flex-col overflow-hidden border-l border-border bg-background"
data-testid="workflow-node-inspector"
exit={{ opacity: 0, width: 0, x: 24 }}
initial={{ opacity: 0, width: 0, x: 24 }}
key="workflow-node-inspector"
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: 0.24, ease: [0.22, 1, 0.36, 1] }
}
>
<div className="flex w-96 min-w-96 flex-shrink-0 items-start justify-between gap-3 border-b border-border px-5 py-4">
<div className="min-w-0">
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
{selectedNode.type === "trigger"
? "Trigger"
: `Step ${selectedNode.index + 1}`}
</p>
<h3 className="truncate text-base font-semibold text-foreground">
{selectedNode.type === "trigger"
? TRIGGER_LABELS[formState.trigger.on]
: selectedStep?.name?.trim() ||
(selectedStep
? ACTION_LABELS[selectedStep.action]
: "Step")}
</h3>
</div>
<div className="flex items-center gap-1">
{selectedNode.type === "step" && selectedStep ? (
<Button
aria-label="Remove step"
className="h-8 w-8"
disabled={disabled}
onClick={() => removeStep(selectedNode.index)}
size="icon"
type="button"
variant="ghost"
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
) : null}
<Button
aria-label="Close inspector"
className="h-8 w-8"
onClick={() => setSelectedNode(null)}
size="icon"
type="button"
variant="ghost"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<div className="min-h-0 w-96 min-w-96 flex-1 overflow-y-auto p-5">
<AnimatePresence initial={false} mode="wait">
<motion.div
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -8 }}
initial={{ opacity: 0, x: 8 }}
key={
selectedNode.type === "trigger"
? "trigger"
: `step-${selectedNode.index}`
}
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: 0.15, ease: "easeOut" }
}
>
{selectedNode.type === "trigger" ? (
<div className="space-y-4">
<div className="space-y-1.5">
<FieldLabel htmlFor="wf-trigger-type">
Event
</FieldLabel>
<FormSelect
disabled={disabled}
id="wf-trigger-type"
onChange={(value) =>
updateFormState({
...formState,
trigger: { on: value as TriggerType },
})
}
value={formState.trigger.on}
>
{TRIGGER_TYPES.map((type) => (
<option key={type} value={type}>
{TRIGGER_LABELS[type]}
</option>
))}
</FormSelect>
</div>
<TriggerConfigFields
onUpdate={(trigger) =>
updateFormState({ ...formState, trigger })
}
trigger={formState.trigger}
/>
</div>
) : selectedStep ? (
<WorkflowStepCard
bare
disabled={disabled}
index={selectedNode.index}
onRemove={() => removeStep(selectedNode.index)}
onUpdate={(updated) =>
updateStep(selectedNode.index, updated)
}
showHeader={false}
step={selectedStep}
triggerType={formState.trigger.on}
/>
) : null}
</motion.div>
</AnimatePresence>
</div>
</motion.aside>
) : null}
</AnimatePresence>
</div>
)}
</div>
@@ -1,5 +1,6 @@
import { Trash2 } from "lucide-react";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import { Textarea } from "@/shared/ui/textarea";
@@ -295,6 +296,8 @@ function StepConfigFields({
}
export function WorkflowStepCard({
bare = false,
showHeader = true,
index,
disabled,
onRemove,
@@ -302,6 +305,8 @@ export function WorkflowStepCard({
step,
triggerType,
}: {
bare?: boolean;
showHeader?: boolean;
index: number;
disabled?: boolean;
onRemove: () => void;
@@ -312,23 +317,30 @@ export function WorkflowStepCard({
const prefix = `wf-step-${index}`;
return (
<div className="space-y-3 rounded-lg border border-border/70 bg-muted/10 p-3">
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium text-muted-foreground">
Step {index + 1}
</span>
<Button
aria-label="Remove step"
className="h-7 w-7"
disabled={disabled}
onClick={onRemove}
size="icon"
type="button"
variant="ghost"
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
<div
className={cn(
"space-y-3",
!bare && "rounded-lg border border-border/70 bg-muted/10 p-3",
)}
>
{showHeader ? (
<div className="flex items-center justify-between gap-2">
<span className="text-xs font-medium text-muted-foreground">
Step {index + 1}
</span>
<Button
aria-label="Remove step"
className="h-7 w-7"
disabled={disabled}
onClick={onRemove}
size="icon"
type="button"
variant="ghost"
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
) : null}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1.5">
+1
View File
@@ -28,6 +28,7 @@ async function createWorkflow(
await expect(dialog).toBeVisible();
await dialog.getByLabel("Workflow name").fill(name);
await dialog.getByRole("button", { name: "Add step" }).click();
await page.getByRole("menuitem", { name: "Delay" }).click();
await dialog.getByRole("button", { name: "Create workflow" }).click();
await expect(dialog).not.toBeVisible();
}
+32
View File
@@ -37,10 +37,12 @@ async function createWorkflow(
await dialog.getByLabel("Enable after creation").click();
}
if (options?.trigger) {
await dialog.getByRole("button", { name: /^Trigger:/ }).click();
await dialog.getByLabel("Event").selectOption(options.trigger);
}
await dialog.getByRole("button", { name: "Add step" }).click();
await page.getByRole("menuitem", { name: "Delay" }).click();
if (options?.stepName) {
await dialog.getByLabel("Step name (optional)").fill(options.stepName);
}
@@ -93,12 +95,42 @@ test("disables autocapitalization in the workflow form", async ({ page }) => {
);
await dialog.getByRole("button", { name: "Add step" }).click();
await page.getByRole("menuitem", { name: "Delay" }).click();
await expect(dialog.getByLabel("Step name (optional)")).toHaveAttribute(
"autocapitalize",
"off",
);
});
test("opens node configuration in a contextual inspector", async ({ page }) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog");
const inspector = dialog.getByTestId("workflow-node-inspector");
await expect(inspector).not.toBeVisible();
await expect(dialog.getByText("End", { exact: true })).not.toBeVisible();
await dialog.getByRole("button", { name: /^Trigger:/ }).click();
await expect(inspector).toBeVisible();
await expect(inspector.getByLabel("Event")).toBeVisible();
await dialog.getByRole("button", { name: "Add step" }).click();
await expect(page.getByText("Add action", { exact: true })).toBeVisible();
await page.getByRole("menuitem", { name: "Send Message" }).click();
await expect(inspector.getByLabel("Step ID")).toHaveValue("step_1");
await expect(inspector.getByLabel("Action")).toHaveValue("send_message");
await expect(inspector.getByLabel("Message text")).toBeVisible();
await expect(dialog.getByText("End", { exact: true })).toBeVisible();
await expect(
dialog.getByRole("button", { name: /^Step 1:/ }),
).toHaveAttribute("aria-pressed", "true");
await inspector.getByRole("button", { name: "Close inspector" }).click();
await expect(inspector).not.toBeVisible();
});
test("switches between the form and YAML editors", async ({ page }) => {
await navigateToWorkflows(page);