diff --git a/desktop/src/features/workflows/ui/WorkflowDialog.tsx b/desktop/src/features/workflows/ui/WorkflowDialog.tsx
index 06d10051f7..99bf2a0b32 100644
--- a/desktop/src/features/workflows/ui/WorkflowDialog.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowDialog.tsx
@@ -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({
setDraft(event.target.value)}
@@ -217,6 +220,7 @@ export function WorkflowDialog({
pane,
workflow,
}: WorkflowDialogProps) {
+ const formBuilderRef = React.useRef
(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 ? (
@@ -563,19 +576,32 @@ export function WorkflowDialog({
>
Cancel
-
+ {isAddingFirstStep ? (
+
+ ) : (
+
+ )}
diff --git a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx
index b75c956a92..49ade0d93f 100644
--- a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx
+++ b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx
@@ -158,6 +158,10 @@ type WorkflowFormBuilderProps = {
workflowChannelId?: string | null;
};
+export type WorkflowFormBuilderHandle = {
+ addFirstStep: () => void;
+};
+
export type WorkflowEditorMode = "form" | "yaml";
function nodePosition(node: Exclude): 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(
@@ -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}
>
);
-}
+});
diff --git a/desktop/src/features/workflows/ui/workflowFormTypes.ts b/desktop/src/features/workflows/ui/workflowFormTypes.ts
index 131d862396..f01e96892b 100644
--- a/desktop/src/features/workflows/ui/workflowFormTypes.ts
+++ b/desktop/src/features/workflows/ui/workflowFormTypes.ts
@@ -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[];
diff --git a/desktop/tests/e2e/navigation.spec.ts b/desktop/tests/e2e/navigation.spec.ts
index 87ac01a7aa..f4ca16bb03 100644
--- a/desktop/tests/e2e/navigation.spec.ts
+++ b/desktop/tests/e2e/navigation.spec.ts
@@ -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();
}
diff --git a/desktop/tests/e2e/workflows.spec.ts b/desktop/tests/e2e/workflows.spec.ts
index 96cf5ae684..5d8bb43761 100644
--- a/desktop/tests/e2e/workflows.spec.ts
+++ b/desktop/tests/e2e/workflows.spec.ts
@@ -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();