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();