mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Polish workflow creation form
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -43,21 +43,21 @@ function getInitialYaml(
|
||||
}
|
||||
|
||||
const TITLES: Record<DialogMode, string> = {
|
||||
create: "Create Workflow",
|
||||
edit: "Edit Workflow",
|
||||
duplicate: "Duplicate Workflow",
|
||||
create: "Create workflow",
|
||||
edit: "Edit workflow",
|
||||
duplicate: "Duplicate workflow",
|
||||
};
|
||||
|
||||
const SUBMIT_LABELS: Record<DialogMode, string> = {
|
||||
create: "Create",
|
||||
edit: "Save",
|
||||
duplicate: "Create Copy",
|
||||
create: "Create workflow",
|
||||
edit: "Save changes",
|
||||
duplicate: "Create copy",
|
||||
};
|
||||
|
||||
const PENDING_LABELS: Record<DialogMode, string> = {
|
||||
create: "Creating...",
|
||||
edit: "Saving...",
|
||||
duplicate: "Creating...",
|
||||
create: "Creating…",
|
||||
edit: "Saving…",
|
||||
duplicate: "Creating…",
|
||||
};
|
||||
|
||||
export function WorkflowDialog({
|
||||
@@ -153,56 +153,57 @@ export function WorkflowDialog({
|
||||
return (
|
||||
<>
|
||||
<Dialog onOpenChange={handleOpenChange} open={open}>
|
||||
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-lg">
|
||||
<DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-xl">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>{TITLES[mode]}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{mode === "edit"
|
||||
? "Modify the workflow definition."
|
||||
: channels.length === 1
|
||||
? "Create a workflow scoped to this channel."
|
||||
: "Define a workflow and assign it to a channel."}
|
||||
? "Update when this workflow runs and what it does."
|
||||
: mode === "duplicate"
|
||||
? "Copy this workflow and adjust its details."
|
||||
: "Automate actions when something happens in a channel."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto">
|
||||
{showChannelSelector ? (
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel htmlFor="wf-channel-select">Channel</FieldLabel>
|
||||
<ChannelCombobox
|
||||
channels={channels}
|
||||
disabled={mutation.isPending}
|
||||
id="wf-channel-select"
|
||||
onChange={(value) => {
|
||||
mutation.reset();
|
||||
setSelectedChannelId(value);
|
||||
}}
|
||||
value={selectedChannelId}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedChannel
|
||||
? `New workflows will belong to ${selectedChannel.name}.`
|
||||
: "Join or create a channel before adding a workflow."}
|
||||
</p>
|
||||
</div>
|
||||
) : (showChannelInfo || mode === "edit") && selectedChannel ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{mode === "edit"
|
||||
? "Editing workflow in"
|
||||
: "This workflow will be created in"}{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{selectedChannel.name}
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<WorkflowFormBuilder
|
||||
activationLabel={
|
||||
mode === "edit" ? "Workflow enabled" : "Enable after creation"
|
||||
}
|
||||
disabled={mutation.isPending}
|
||||
onChange={(yaml) => {
|
||||
mutation.reset();
|
||||
setYamlDefinition(yaml);
|
||||
}}
|
||||
scopeField={
|
||||
showChannelSelector ? (
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel htmlFor="wf-channel-select">Channel</FieldLabel>
|
||||
<ChannelCombobox
|
||||
channels={channels}
|
||||
disabled={mutation.isPending}
|
||||
id="wf-channel-select"
|
||||
onChange={(value) => {
|
||||
mutation.reset();
|
||||
setSelectedChannelId(value);
|
||||
}}
|
||||
value={selectedChannelId}
|
||||
/>
|
||||
{!selectedChannel ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Join or create a channel before adding a workflow.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : (showChannelInfo || mode === "edit") && selectedChannel ? (
|
||||
<div className="space-y-1">
|
||||
<FieldLabel>Channel</FieldLabel>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{selectedChannel.name}
|
||||
</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
yaml={yamlDefinition}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Code, Plus } from "lucide-react";
|
||||
import { Code, Plus, Zap } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Checkbox } from "@/shared/ui/checkbox";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Switch } from "@/shared/ui/switch";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { WorkflowStepCard } from "./WorkflowStepCard";
|
||||
import { FieldLabel, FormSelect } from "./workflowFormPrimitives";
|
||||
@@ -35,7 +36,7 @@ function TriggerConfigFields({
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel htmlFor="wf-trigger-filter">
|
||||
Filter expression (optional)
|
||||
Condition (optional)
|
||||
</FieldLabel>
|
||||
<Input
|
||||
autoCapitalize="off"
|
||||
@@ -47,7 +48,8 @@ function TriggerConfigFields({
|
||||
value={trigger.filter ?? ""}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Evalexpr filter — leave empty to trigger on all matching events.
|
||||
Run only when this evalexpr expression matches. Leave empty to run
|
||||
for every matching event.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -119,14 +121,57 @@ function TriggerConfigFields({
|
||||
}
|
||||
|
||||
type WorkflowFormBuilderProps = {
|
||||
activationLabel: string;
|
||||
disabled?: boolean;
|
||||
onChange: (yaml: string) => void;
|
||||
scopeField?: React.ReactNode;
|
||||
yaml: string;
|
||||
};
|
||||
|
||||
function FormSection({
|
||||
children,
|
||||
description,
|
||||
title,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
description?: string;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkflowFormBuilder({
|
||||
activationLabel,
|
||||
disabled,
|
||||
onChange,
|
||||
scopeField,
|
||||
yaml,
|
||||
}: WorkflowFormBuilderProps) {
|
||||
// Parse once on mount instead of calling yamlToFormState three times
|
||||
@@ -155,21 +200,29 @@ export function WorkflowFormBuilder({
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleToggleMode = React.useCallback(() => {
|
||||
if (mode === "form") {
|
||||
setMode("yaml");
|
||||
setParseError(null);
|
||||
} else {
|
||||
const result = yamlToFormState(yaml);
|
||||
if (result.ok) {
|
||||
setFormState(result.state);
|
||||
const handleModeChange = React.useCallback(
|
||||
(nextMode: string) => {
|
||||
if (nextMode === mode) return;
|
||||
|
||||
if (nextMode === "yaml") {
|
||||
setMode("yaml");
|
||||
setParseError(null);
|
||||
setMode("form");
|
||||
} else {
|
||||
setParseError(result.error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [mode, yaml]);
|
||||
|
||||
if (nextMode === "form") {
|
||||
const result = yamlToFormState(yaml);
|
||||
if (result.ok) {
|
||||
setFormState(result.state);
|
||||
setParseError(null);
|
||||
setMode("form");
|
||||
} else {
|
||||
setParseError(result.error);
|
||||
}
|
||||
}
|
||||
},
|
||||
[mode, yaml],
|
||||
);
|
||||
|
||||
const addStep = React.useCallback(() => {
|
||||
updateFormState({
|
||||
@@ -201,19 +254,27 @@ export function WorkflowFormBuilder({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-end">
|
||||
<Button
|
||||
className="h-7 gap-1.5 text-xs"
|
||||
disabled={disabled}
|
||||
onClick={handleToggleMode}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Code className="h-4 w-4" />
|
||||
{mode === "form" ? "Edit as YAML" : "Back to form"}
|
||||
</Button>
|
||||
<Tabs onValueChange={handleModeChange} value={mode}>
|
||||
<TabsList aria-label="Workflow editor mode" className="h-8 p-0.5">
|
||||
<TabsTrigger
|
||||
className="h-7 px-3 text-xs"
|
||||
disabled={disabled}
|
||||
value="form"
|
||||
>
|
||||
Form
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
className="h-7 gap-1.5 px-3 text-xs"
|
||||
disabled={disabled}
|
||||
value="yaml"
|
||||
>
|
||||
<Code className="h-3.5 w-3.5" />
|
||||
YAML
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{parseError ? (
|
||||
@@ -223,135 +284,178 @@ export function WorkflowFormBuilder({
|
||||
) : null}
|
||||
|
||||
{mode === "yaml" ? (
|
||||
<div className="space-y-1.5">
|
||||
<Textarea
|
||||
autoCapitalize="off"
|
||||
className="min-h-[240px] resize-y font-mono text-xs"
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
value={yaml}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Edit the raw YAML definition directly.
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
{scopeField}
|
||||
<div className="space-y-1.5">
|
||||
<Textarea
|
||||
aria-label="Workflow YAML"
|
||||
autoCapitalize="off"
|
||||
className="min-h-[240px] resize-y font-mono text-xs"
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
value={yaml}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Edit the raw YAML definition directly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel htmlFor="wf-name">Workflow name</FieldLabel>
|
||||
<Input
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
disabled={disabled}
|
||||
id="wf-name"
|
||||
onChange={(event) =>
|
||||
updateFormState({ ...formState, name: event.target.value })
|
||||
}
|
||||
placeholder="e.g. deploy_notifier"
|
||||
value={formState.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-5">
|
||||
<FormSection title="Basics">
|
||||
<div
|
||||
className={scopeField ? "grid gap-3 sm:grid-cols-2" : undefined}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel htmlFor="wf-name">Workflow name</FieldLabel>
|
||||
<Input
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
disabled={disabled}
|
||||
id="wf-name"
|
||||
onChange={(event) =>
|
||||
updateFormState({ ...formState, name: event.target.value })
|
||||
}
|
||||
placeholder="e.g. deploy_notifier"
|
||||
value={formState.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel htmlFor="wf-description">
|
||||
Description (optional)
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
autoCapitalize="off"
|
||||
className="min-h-[72px] 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>
|
||||
{scopeField}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 rounded-md border border-border/70 px-3 py-2">
|
||||
<Checkbox
|
||||
checked={formState.enabled}
|
||||
disabled={disabled}
|
||||
id="wf-enabled"
|
||||
onCheckedChange={(checked) =>
|
||||
updateFormState({
|
||||
...formState,
|
||||
enabled: checked === true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<label className="text-sm" htmlFor="wf-enabled">
|
||||
Workflow is enabled
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<FieldLabel htmlFor="wf-trigger-type">Trigger</FieldLabel>
|
||||
<FormSelect
|
||||
<FieldLabel htmlFor="wf-description">
|
||||
Description (optional)
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
autoCapitalize="off"
|
||||
className="min-h-[60px] resize-y text-sm"
|
||||
disabled={disabled}
|
||||
id="wf-trigger-type"
|
||||
onChange={(value) =>
|
||||
id="wf-description"
|
||||
onChange={(event) =>
|
||||
updateFormState({
|
||||
...formState,
|
||||
trigger: { on: value as TriggerType },
|
||||
description: event.target.value,
|
||||
})
|
||||
}
|
||||
value={formState.trigger.on}
|
||||
>
|
||||
{TRIGGER_TYPES.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{TRIGGER_LABELS[type]}
|
||||
</option>
|
||||
))}
|
||||
</FormSelect>
|
||||
placeholder="What does this workflow do?"
|
||||
value={formState.description}
|
||||
/>
|
||||
</div>
|
||||
<TriggerConfigFields
|
||||
onUpdate={(trigger) => updateFormState({ ...formState, trigger })}
|
||||
trigger={formState.trigger}
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<FieldLabel>Steps</FieldLabel>
|
||||
<Button
|
||||
className="h-7 gap-1.5 text-xs"
|
||||
disabled={disabled}
|
||||
onClick={addStep}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add step
|
||||
</Button>
|
||||
</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.length === 0 ? (
|
||||
<p className="py-4 text-center text-xs text-muted-foreground">
|
||||
No steps yet — add one to get started.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{formState.steps.map((step, index) => (
|
||||
<WorkflowStepCard
|
||||
disabled={disabled}
|
||||
index={index}
|
||||
key={step.id}
|
||||
onRemove={() => removeStep(index)}
|
||||
onUpdate={(updated) => updateStep(index, updated)}
|
||||
step={step}
|
||||
triggerType={formState.trigger.on}
|
||||
/>
|
||||
<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"
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={formState.enabled}
|
||||
disabled={disabled}
|
||||
id="wf-enabled"
|
||||
onCheckedChange={(checked) =>
|
||||
updateFormState({ ...formState, enabled: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</FormSection>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -28,7 +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 dialog.getByRole("button", { name: "Create" }).click();
|
||||
await dialog.getByRole("button", { name: "Create workflow" }).click();
|
||||
await expect(dialog).not.toBeVisible();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,10 +34,10 @@ async function createWorkflow(
|
||||
await dialog.getByLabel("Description (optional)").fill(options.description);
|
||||
}
|
||||
if (options?.enabled === false) {
|
||||
await dialog.getByLabel("Workflow is enabled").click();
|
||||
await dialog.getByLabel("Enable after creation").click();
|
||||
}
|
||||
if (options?.trigger) {
|
||||
await dialog.getByLabel("Trigger").selectOption(options.trigger);
|
||||
await dialog.getByLabel("Event").selectOption(options.trigger);
|
||||
}
|
||||
|
||||
await dialog.getByRole("button", { name: "Add step" }).click();
|
||||
@@ -55,10 +55,10 @@ async function createWorkflow(
|
||||
.fill(options.stepTimeoutSecs);
|
||||
}
|
||||
|
||||
await dialog.getByRole("button", { name: "Create" }).click();
|
||||
await dialog.getByRole("button", { name: "Create workflow" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Create Workflow" }),
|
||||
page.getByRole("heading", { name: "Create workflow" }),
|
||||
).not.toBeVisible();
|
||||
}
|
||||
|
||||
@@ -99,6 +99,23 @@ test("disables autocapitalization in the workflow form", async ({ page }) => {
|
||||
);
|
||||
});
|
||||
|
||||
test("switches between the form and YAML editors", async ({ page }) => {
|
||||
await navigateToWorkflows(page);
|
||||
|
||||
await page.getByRole("button", { name: "Create Workflow" }).click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
const nameInput = dialog.getByLabel("Workflow name");
|
||||
|
||||
await nameInput.fill("yaml_round_trip");
|
||||
await dialog.getByRole("tab", { name: "YAML" }).click();
|
||||
await expect(dialog.getByLabel("Workflow YAML")).toHaveValue(
|
||||
/name: yaml_round_trip/,
|
||||
);
|
||||
|
||||
await dialog.getByRole("tab", { name: "Form" }).click();
|
||||
await expect(nameInput).toHaveValue("yaml_round_trip");
|
||||
});
|
||||
|
||||
test("captures disabled diff workflows in the list UI", async ({ page }) => {
|
||||
const workflowName = `diff_workflow_${Date.now()}`;
|
||||
const description = "Watches diff events for src/ changes";
|
||||
@@ -157,7 +174,7 @@ test("edits an existing workflow", async ({ page }) => {
|
||||
|
||||
// Dialog should open in edit mode
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
await expect(page.getByText("Edit Workflow")).toBeVisible();
|
||||
await expect(page.getByText("Edit workflow")).toBeVisible();
|
||||
|
||||
// Change the name
|
||||
const nameInput = page.getByLabel("Workflow name");
|
||||
@@ -165,7 +182,7 @@ test("edits an existing workflow", async ({ page }) => {
|
||||
await nameInput.fill(updatedName);
|
||||
|
||||
// Save
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await page.getByRole("button", { name: "Save changes" }).click();
|
||||
await expect(page.getByRole("dialog")).not.toBeVisible();
|
||||
|
||||
// Verify the updated name appears
|
||||
@@ -184,10 +201,10 @@ test("duplicates a workflow", async ({ page }) => {
|
||||
|
||||
// Dialog should open in duplicate mode with "(copy)" suffix
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
await expect(page.getByText("Duplicate Workflow")).toBeVisible();
|
||||
await expect(page.getByText("Duplicate workflow")).toBeVisible();
|
||||
|
||||
// Submit the duplicate
|
||||
await page.getByRole("button", { name: "Create Copy" }).click();
|
||||
await page.getByRole("button", { name: "Create copy" }).click();
|
||||
await expect(page.getByRole("dialog")).not.toBeVisible();
|
||||
|
||||
// Both the original and copy should exist
|
||||
|
||||
Reference in New Issue
Block a user