From 75019b0906fde1adc351a473c83f3b2b4b1e656f Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Wed, 12 Aug 2026 01:04:34 -0700 Subject: [PATCH] Polish workflow creation form Signed-off-by: Taylor Ho --- .../features/workflows/ui/WorkflowDialog.tsx | 91 ++-- .../workflows/ui/WorkflowFormBuilder.tsx | 388 +++++++++++------- desktop/tests/e2e/navigation.spec.ts | 2 +- desktop/tests/e2e/workflows.spec.ts | 33 +- 4 files changed, 318 insertions(+), 196 deletions(-) diff --git a/desktop/src/features/workflows/ui/WorkflowDialog.tsx b/desktop/src/features/workflows/ui/WorkflowDialog.tsx index 588ec896c..dc6793792 100644 --- a/desktop/src/features/workflows/ui/WorkflowDialog.tsx +++ b/desktop/src/features/workflows/ui/WorkflowDialog.tsx @@ -43,21 +43,21 @@ function getInitialYaml( } const TITLES: Record = { - create: "Create Workflow", - edit: "Edit Workflow", - duplicate: "Duplicate Workflow", + create: "Create workflow", + edit: "Edit workflow", + duplicate: "Duplicate workflow", }; const SUBMIT_LABELS: Record = { - create: "Create", - edit: "Save", - duplicate: "Create Copy", + create: "Create workflow", + edit: "Save changes", + duplicate: "Create copy", }; const PENDING_LABELS: Record = { - create: "Creating...", - edit: "Saving...", - duplicate: "Creating...", + create: "Creating…", + edit: "Saving…", + duplicate: "Creating…", }; export function WorkflowDialog({ @@ -153,56 +153,57 @@ export function WorkflowDialog({ return ( <> - + {TITLES[mode]} {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."}
- {showChannelSelector ? ( -
- Channel - { - mutation.reset(); - setSelectedChannelId(value); - }} - value={selectedChannelId} - /> -

- {selectedChannel - ? `New workflows will belong to ${selectedChannel.name}.` - : "Join or create a channel before adding a workflow."} -

-
- ) : (showChannelInfo || mode === "edit") && selectedChannel ? ( -

- {mode === "edit" - ? "Editing workflow in" - : "This workflow will be created in"}{" "} - - {selectedChannel.name} - - . -

- ) : null} - { mutation.reset(); setYamlDefinition(yaml); }} + scopeField={ + showChannelSelector ? ( +
+ Channel + { + mutation.reset(); + setSelectedChannelId(value); + }} + value={selectedChannelId} + /> + {!selectedChannel ? ( +

+ Join or create a channel before adding a workflow. +

+ ) : null} +
+ ) : (showChannelInfo || mode === "edit") && selectedChannel ? ( +
+ Channel +

+ {selectedChannel.name} +

+
+ ) : null + } yaml={yamlDefinition} /> diff --git a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx index c2b7bfda1..f898088ae 100644 --- a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx +++ b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx @@ -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 (
- Filter expression (optional) + Condition (optional)

- 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.

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

{title}

+ {description ? ( +

{description}

+ ) : null} +
+ {children} +
+ ); +} + +function FlowNode({ + children, + marker, +}: { + children: React.ReactNode; + marker: React.ReactNode; +}) { + return ( +
  • +
    + {marker} +
    +
    {children}
    +
  • + ); +} + 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 ( -
    +
    - + + + + Form + + + + YAML + + +
    {parseError ? ( @@ -223,135 +284,178 @@ export function WorkflowFormBuilder({ ) : null} {mode === "yaml" ? ( -
    -