diff --git a/desktop/src/features/workflows/ui/WorkflowCard.tsx b/desktop/src/features/workflows/ui/WorkflowCard.tsx index fccc10355..b42f07797 100644 --- a/desktop/src/features/workflows/ui/WorkflowCard.tsx +++ b/desktop/src/features/workflows/ui/WorkflowCard.tsx @@ -37,7 +37,6 @@ import { } from "@/shared/ui/dropdown-menu"; import { getWorkflowCardLabel, - getWorkflowDescription, getWorkflowDisplayStatus, getWorkflowEnabled, getWorkflowPrimaryAction, @@ -217,7 +216,6 @@ export function WorkflowCard({ const customEmoji = useCustomEmoji(); const isEnabled = getWorkflowEnabled(workflow.definition); const displayStatus = getWorkflowDisplayStatus(workflow); - const description = getWorkflowDescription(workflow.definition); const configuredTrigger = getWorkflowTriggerConfig(workflow.definition); const trigger = configuredTrigger ?? { on: "message_posted" as const }; const triggerPresentation = useWorkflowTriggerPresentation({ @@ -383,12 +381,6 @@ export function WorkflowCard({ text={cardLabel} /> - {description ? ( -

- {description} -

- ) : null} -

diff --git a/desktop/src/features/workflows/ui/WorkflowConditionBuilder.tsx b/desktop/src/features/workflows/ui/WorkflowConditionBuilder.tsx index 927379734..66990df49 100644 --- a/desktop/src/features/workflows/ui/WorkflowConditionBuilder.tsx +++ b/desktop/src/features/workflows/ui/WorkflowConditionBuilder.tsx @@ -752,7 +752,7 @@ export function WorkflowConditionBuilder({ {isExpanded && activeEditor ? (

= { duplicate: "Creating…", }; +function visibleWorkflowName( + yaml: string, + fallbackName: string | undefined, +): string { + const parsed = yamlToFormState(yaml); + if (parsed.ok && parsed.state.name.trim()) return parsed.state.name.trim(); + return fallbackName?.trim() ?? ""; +} + +function yamlWithWorkflowName(yaml: string, name: string): string | null { + if (!yaml.trim()) { + return formStateToYaml({ ...DEFAULT_FORM_STATE, name }); + } + + const document = parseDocument(yaml); + if (document.errors.length > 0 || !isMap(document.contents)) return null; + document.set("name", name); + return document.toString(); +} + +function WorkflowNameEditor({ + disabled, + generating, + name, + onCommit, +}: { + disabled: boolean; + generating: boolean; + name: string; + onCommit: (name: string) => boolean; +}) { + const [editing, setEditing] = React.useState(false); + const [draft, setDraft] = React.useState(name); + const inputRef = React.useRef(null); + + React.useEffect(() => { + if (!editing) setDraft(name); + }, [editing, name]); + + React.useEffect(() => { + if (editing) inputRef.current?.select(); + }, [editing]); + + const commit = React.useCallback(() => { + const nextName = draft.trim(); + if (!nextName || !onCommit(nextName)) return; + setEditing(false); + }, [draft, onCommit]); + + if (editing) { + return ( +
+ setDraft(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + commit(); + } else if (event.key === "Escape") { + event.preventDefault(); + setDraft(name); + setEditing(false); + } + }} + ref={inputRef} + value={draft} + /> + +
+ ); + } + + return ( +
+ + {generating ? "Generating name…" : name || "Untitled workflow"} + + +
+ ); +} + export function WorkflowDialog({ channels, mode, @@ -115,10 +228,12 @@ export function WorkflowDialog({ } | null>(null); const [discardConfirmationOpen, setDiscardConfirmationOpen] = React.useState(false); + const [generatingName, setGeneratingName] = React.useState(false); const initialValuesRef = React.useRef({ channelId, yaml: getInitialYaml(mode, workflow), }); + const yamlDefinitionRef = React.useRef(yamlDefinition); const allowNavigationRef = React.useRef(false); const proceedingNavigationRef = React.useRef(false); @@ -135,6 +250,7 @@ export function WorkflowDialog({ // Re-initialize when dialog opens or workflow/mode changes React.useEffect(() => { + let active = true; if (open) { const newChannelId = mode === "edit" && workflowChannelId ? workflowChannelId : ""; @@ -144,6 +260,7 @@ export function WorkflowDialog({ channelId: newChannelId, yaml: initialYaml, }; + yamlDefinitionRef.current = initialYaml; setYamlDefinition(initialYaml); setEditorMode(getInitialEditorMode(initialYaml)); setEditorParseError(null); @@ -151,7 +268,37 @@ export function WorkflowDialog({ setDiscardConfirmationOpen(false); resetCreate(); resetUpdate(); + + if (mode === "create" && !workflow) { + setGeneratingName(true); + void generateBackupPassphrase({ words: 3, separator: "-" }) + .then((name) => { + if (!active || yamlDefinitionRef.current.trim()) return; + const generatedYaml = formStateToYaml({ + ...DEFAULT_FORM_STATE, + name, + }); + yamlDefinitionRef.current = generatedYaml; + initialValuesRef.current = { + ...initialValuesRef.current, + yaml: generatedYaml, + }; + setYamlDefinition(generatedYaml); + }) + .catch(() => { + // Leave the editable "Untitled workflow" fallback in place. + }) + .finally(() => { + if (active) setGeneratingName(false); + }); + } else { + setGeneratingName(false); + } } + + return () => { + active = false; + }; }, [open, mode, workflow, workflowChannelId, resetCreate, resetUpdate]); const closeDialog = React.useCallback(() => { @@ -244,6 +391,21 @@ export function WorkflowDialog({ [editorMode, yamlDefinition], ); + const workflowName = visibleWorkflowName(yamlDefinition, workflow?.name); + const canEditWorkflowName = + !yamlDefinition.trim() || yamlToFormState(yamlDefinition).ok; + const handleWorkflowNameCommit = React.useCallback( + (name: string) => { + const nextYaml = yamlWithWorkflowName(yamlDefinitionRef.current, name); + if (nextYaml === null) return false; + mutation.reset(); + yamlDefinitionRef.current = nextYaml; + setYamlDefinition(nextYaml); + return true; + }, + [mutation.reset], + ); + const showChannelSelector = mode !== "edit"; return ( @@ -254,15 +416,21 @@ export function WorkflowDialog({ > -
+
{TITLES[mode]} - + {mode === "edit" ? "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."} +
@@ -275,6 +443,7 @@ export function WorkflowDialog({ mode={editorMode} onChange={(yaml) => { mutation.reset(); + yamlDefinitionRef.current = yaml; setYamlDefinition(yaml); }} onSelectedNodeChange={onEditorPaneChange} diff --git a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx index 03f15cf50..e3c26c07b 100644 --- a/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx +++ b/desktop/src/features/workflows/ui/WorkflowFormBuilder.tsx @@ -28,7 +28,6 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/shared/ui/dropdown-menu"; -import { Input } from "@/shared/ui/input"; import { Switch } from "@/shared/ui/switch"; import { Textarea } from "@/shared/ui/textarea"; import { WorkflowConditionBuilder } from "./WorkflowConditionBuilder"; @@ -39,7 +38,6 @@ import { import { WorkflowScheduleFields } from "./WorkflowScheduleFields"; import { WorkflowStepCard } from "./WorkflowStepCard"; import { buildConditionExpression } from "./workflowConditionExpression"; -import { FieldLabel } from "./workflowFormPrimitives"; import { DEFAULT_FORM_STATE, ACTION_LABELS, @@ -424,6 +422,17 @@ export function WorkflowFormBuilder({ if (result.ok) setFormState(result.state); }, [mode, yaml]); + React.useEffect(() => { + if (!yaml.trim()) return; + const result = yamlToFormState(yaml); + if (!result.ok) return; + setFormState((current) => + current.name === result.state.name + ? current + : { ...current, name: result.state.name }, + ); + }, [yaml]); + const selectNode = React.useCallback( (nextNode: Exclude) => { if (selectedNode) { @@ -530,41 +539,6 @@ export function WorkflowFormBuilder({
) : (
-
-
- Workflow name - - updateFormState({ ...formState, name: event.target.value }) - } - placeholder="e.g. deploy_notifier" - value={formState.name} - /> -
-
- - Description (optional) - - - updateFormState({ - ...formState, - description: event.target.value, - }) - } - placeholder="What does this workflow do?" - value={formState.description} - /> -
-
-
diff --git a/desktop/src/features/workflows/ui/workflowDefinition.ts b/desktop/src/features/workflows/ui/workflowDefinition.ts index db3dfe120..317cc3a7d 100644 --- a/desktop/src/features/workflows/ui/workflowDefinition.ts +++ b/desktop/src/features/workflows/ui/workflowDefinition.ts @@ -385,15 +385,6 @@ export function getWorkflowDisplayStatus( return getWorkflowEnabled(workflow.definition) ? workflow.status : "disabled"; } -export function getWorkflowDescription( - definition: Record, -): string | null { - const description = definition.description; - return typeof description === "string" && description.trim().length > 0 - ? description.trim() - : null; -} - export function getWorkflowTriggerSummary( definition: Record, ): string | null {