Improve initial workflow step creation

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-14 23:40:02 -07:00
parent 7b41cf656b
commit 2166464b5c
5 changed files with 104 additions and 46 deletions

View File

@@ -35,6 +35,7 @@ import { ChannelCombobox } from "./ChannelCombobox";
import {
WorkflowFormBuilder,
type WorkflowEditorMode,
type WorkflowFormBuilderHandle,
} from "./WorkflowFormBuilder";
import { WorkflowWebhookSecretDialog } from "./WorkflowWebhookSecretDialog";
import type { WorkflowEditorPane } from "./workflowEditorPane";
@@ -155,6 +156,8 @@ function WorkflowNameEditor({
<div className="flex h-6 items-center gap-1.5">
<Input
aria-label="Workflow name"
autoCapitalize="off"
autoCorrect="off"
className="h-6 w-72 px-2 font-mono text-sm"
disabled={disabled}
onChange={(event) => setDraft(event.target.value)}
@@ -217,6 +220,7 @@ export function WorkflowDialog({
pane,
workflow,
}: WorkflowDialogProps) {
const formBuilderRef = React.useRef<WorkflowFormBuilderHandle>(null);
const channelId =
mode === "edit" && workflow?.channelId ? workflow.channelId : "";
@@ -255,6 +259,14 @@ export function WorkflowDialog({
const selectedChannel =
channels.find((c) => c.id === selectedChannelId) ?? null;
const parsedDefinition = yamlDefinition.trim()
? yamlToFormState(yamlDefinition)
: null;
const isAddingFirstStep =
mode === "create" &&
editorMode === "form" &&
(parsedDefinition === null ||
(parsedDefinition.ok && parsedDefinition.state.steps.length === 0));
const workflowChannelId = workflow?.channelId ?? null;
const resetCreate = createMutation.reset;
@@ -489,6 +501,7 @@ export function WorkflowDialog({
}}
onSelectedNodeChange={onEditorPaneChange}
parseError={editorParseError}
ref={formBuilderRef}
scopeField={
showChannelSelector ? (
<div className="space-y-1">
@@ -563,19 +576,32 @@ export function WorkflowDialog({
>
Cancel
</Button>
<Button
disabled={
!selectedChannelId ||
!yamlDefinition.trim() ||
mutation.isPending
}
onClick={handleSubmit}
type="button"
>
{mutation.isPending
? PENDING_LABELS[mode]
: SUBMIT_LABELS[mode]}
</Button>
{isAddingFirstStep ? (
<Button
aria-label="Add first step"
data-testid="workflow-dialog-primary-action"
disabled={!selectedChannelId || mutation.isPending}
onClick={() => formBuilderRef.current?.addFirstStep()}
type="button"
>
Add step
</Button>
) : (
<Button
data-testid="workflow-dialog-primary-action"
disabled={
!selectedChannelId ||
!yamlDefinition.trim() ||
mutation.isPending
}
onClick={handleSubmit}
type="button"
>
{mutation.isPending
? PENDING_LABELS[mode]
: SUBMIT_LABELS[mode]}
</Button>
)}
</div>
</div>
</DialogContent>

View File

@@ -158,6 +158,10 @@ type WorkflowFormBuilderProps = {
workflowChannelId?: string | null;
};
export type WorkflowFormBuilderHandle = {
addFirstStep: () => void;
};
export type WorkflowEditorMode = "form" | "yaml";
function nodePosition(node: Exclude<WorkflowEditorPane, null>): number {
@@ -362,19 +366,25 @@ function WorkflowNode({
);
}
export function WorkflowFormBuilder({
channels,
disabled,
mode,
nameLeadingContainer,
onChange,
onSelectedNodeChange,
parseError,
scopeField,
selectedNode: selectedRouteNode,
yaml,
workflowChannelId,
}: WorkflowFormBuilderProps) {
export const WorkflowFormBuilder = React.forwardRef<
WorkflowFormBuilderHandle,
WorkflowFormBuilderProps
>(function WorkflowFormBuilder(
{
channels,
disabled,
mode,
nameLeadingContainer,
onChange,
onSelectedNodeChange,
parseError,
scopeField,
selectedNode: selectedRouteNode,
yaml,
workflowChannelId,
},
ref,
) {
// Parse once on mount instead of calling yamlToFormState three times
const initialParseRef = React.useRef(yaml ? yamlToFormState(yaml) : null);
const [formState, setFormState] = React.useState<WorkflowFormState>(
@@ -470,6 +480,14 @@ export function WorkflowFormBuilder({
[formState, selectNode, updateFormState],
);
React.useImperativeHandle(
ref,
() => ({
addFirstStep: () => insertStep(0, "send_message"),
}),
[insertStep],
);
const removeStep = React.useCallback(
(index: number) => {
updateFormState({
@@ -838,4 +856,4 @@ export function WorkflowFormBuilder({
: null}
</>
);
}
});

View File

@@ -28,8 +28,8 @@ export type ActionType = (typeof ACTION_TYPES)[number];
// Keep every recognized schema action above so existing YAML remains editable,
// but only offer actions that can complete successfully in form selectors.
export const SELECTABLE_ACTION_TYPES = [
"delay",
"send_message",
"delay",
"call_webhook",
] as const satisfies readonly ActionType[];

View File

@@ -24,7 +24,7 @@ async function createWorkflow(
name: string,
) {
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog");
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await expect(dialog).toBeVisible();
await dialog.getByRole("combobox", { name: "Channel" }).click();
await dialog
@@ -32,10 +32,15 @@ async function createWorkflow(
.getByRole("button")
.first()
.click();
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 dialog.getByRole("button", { name: "Edit workflow name" }).click();
await dialog.getByRole("textbox", { name: "Workflow name" }).fill(name);
await dialog.getByRole("button", { name: "Save workflow name" }).click();
const primaryAction = dialog.getByTestId("workflow-dialog-primary-action");
await expect(primaryAction).toHaveText("Add step");
await primaryAction.click();
await expect(primaryAction).toHaveText("Create workflow");
await dialog.getByLabel("Message text").fill("Test workflow message");
await primaryAction.click();
await expect(dialog).not.toBeVisible();
}

View File

@@ -56,8 +56,8 @@ async function expectUnsupportedActionWarnings(
menu: import("@playwright/test").Locator,
) {
await expect(menu.getByRole("menuitem")).toHaveText([
"Delay",
"Send message",
"Delay",
"Call webhook",
"Send DM",
"Request approval",
@@ -100,15 +100,19 @@ async function createWorkflow(
},
) {
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog");
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await expect(dialog).toBeVisible();
await expect(dialog.getByText("Choose a channel")).toBeVisible();
await expect(
dialog.getByRole("button", { name: "Create workflow" }),
).toBeDisabled();
const primaryAction = dialog.getByTestId("workflow-dialog-primary-action");
await expect(primaryAction).toHaveText("Add step");
await expect(primaryAction).toBeDisabled();
await selectFirstChannel(dialog);
await expect(primaryAction).toHaveText("Add step");
await expect(primaryAction).toBeEnabled();
await dialog.getByLabel("Workflow name").fill(name);
await dialog.getByRole("button", { name: "Edit workflow name" }).click();
await dialog.getByRole("textbox", { name: "Workflow name" }).fill(name);
await dialog.getByRole("button", { name: "Save workflow name" }).click();
if (options?.description) {
await dialog.getByLabel("Description (optional)").fill(options.description);
}
@@ -131,8 +135,13 @@ async function createWorkflow(
await dialog.getByLabel("Custom expression").fill(options.triggerCondition);
}
await dialog.getByRole("button", { name: "Add step" }).click();
await page.getByRole("menuitem", { name: "Delay" }).click();
await primaryAction.click();
await expect(primaryAction).toHaveText("Create workflow");
await expect(dialog.getByLabel("Action")).toHaveAttribute(
"data-value",
"send_message",
);
await dialog.getByLabel("Message text").fill("Test workflow message");
if (options?.stepName) {
await dialog.getByLabel("Name (optional)").fill(options.stepName);
}
@@ -140,7 +149,7 @@ async function createWorkflow(
await dialog.getByLabel("Timeout (seconds)").fill(options.stepTimeoutSecs);
}
await dialog.getByRole("button", { name: "Create workflow" }).click();
await primaryAction.click();
await expect(
page.getByRole("heading", { name: "Create workflow" }),
@@ -172,10 +181,10 @@ test("disables autocapitalization in the workflow form", async ({ page }) => {
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog");
await expect(dialog.getByLabel("Workflow name")).toHaveAttribute(
"autocapitalize",
"off",
);
await dialog.getByRole("button", { name: "Edit workflow name" }).click();
await expect(
dialog.getByRole("textbox", { name: "Workflow name" }),
).toHaveAttribute("autocapitalize", "off");
await dialog.getByRole("button", { name: "Add step" }).click();
await page.getByRole("menuitem", { name: "Delay" }).click();