Polish workflow editor actions

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-15 00:09:47 -07:00
parent 2166464b5c
commit 8f44532084
5 changed files with 182 additions and 80 deletions
@@ -0,0 +1,97 @@
import {
Copy,
MoreHorizontal,
Pencil,
Play,
Power,
PowerOff,
Trash2,
} from "lucide-react";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { Switch } from "@/shared/ui/switch";
type WorkflowActionsMenuProps = {
isEnabled: boolean;
isTogglingEnabled?: boolean;
onDelete: () => void;
onDuplicate: () => void;
onEdit: () => void;
onToggleEnabled: () => void;
onTrigger: () => void;
};
export function WorkflowActionsMenu({
isEnabled,
isTogglingEnabled = false,
onDelete,
onDuplicate,
onEdit,
onToggleEnabled,
onTrigger,
}: WorkflowActionsMenuProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Workflow actions"
className="h-8 w-8 text-muted-foreground hover:bg-background/80 hover:text-foreground data-[state=open]:bg-background/80 data-[state=open]:text-foreground"
size="icon"
type="button"
variant="ghost"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={onTrigger}>
<Play className="mr-2 h-4 w-4" />
Trigger
</DropdownMenuItem>
<DropdownMenuItem onClick={onEdit}>
<Pencil className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={onDuplicate}>
<Copy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuCheckboxItem
checked={isEnabled}
className="gap-2 pl-2 [&>span:first-child]:hidden"
disabled={isTogglingEnabled}
onCheckedChange={(checked) => {
if (checked !== isEnabled) onToggleEnabled();
}}
onSelect={(event) => event.preventDefault()}
>
{isEnabled ? (
<Power className="mr-2 h-4 w-4 shrink-0" />
) : (
<PowerOff className="mr-2 h-4 w-4 shrink-0" />
)}
<span>Enable</span>
<Switch
aria-hidden="true"
checked={isEnabled}
className="pointer-events-none ml-auto"
tabIndex={-1}
/>
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive" onClick={onDelete}>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
@@ -2,20 +2,13 @@ import {
ArrowRight,
CalendarClock,
CircleCheckBig,
Copy,
GitPullRequest,
Hash,
MessageCircle,
MessageSquare,
MoreHorizontal,
Pencil,
Play,
Power,
PowerOff,
Send,
SmilePlus,
Timer,
Trash2,
Webhook,
Zap,
} from "lucide-react";
@@ -27,16 +20,7 @@ import type { Workflow } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { emojiDisplayName } from "@/shared/lib/emojiName";
import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { Switch } from "@/shared/ui/switch";
import { WorkflowActionsMenu } from "./WorkflowActionsMenu";
import {
getWorkflowCardLabel,
getWorkflowDisplayStatus,
@@ -355,62 +339,15 @@ export function WorkflowCard({
<div className="pointer-events-auto flex items-center gap-1.5">
<StatusBadge status={displayStatus} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Workflow actions"
className="h-8 w-8 text-muted-foreground hover:bg-background/80 hover:text-foreground data-[state=open]:bg-background/80 data-[state=open]:text-foreground"
size="icon"
variant="ghost"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onTrigger(workflow.id)}>
<Play className="mr-2 h-4 w-4" />
Trigger
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onEdit(workflow)}>
<Pencil className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onDuplicate(workflow)}>
<Copy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuCheckboxItem
checked={isEnabled}
className="gap-2 pl-2 [&>span:first-child]:hidden"
disabled={isTogglingEnabled}
onCheckedChange={(checked) => {
if (checked !== isEnabled) onToggleEnabled(workflow);
}}
onSelect={(event) => event.preventDefault()}
>
{isEnabled ? (
<Power className="mr-2 h-4 w-4 shrink-0" />
) : (
<PowerOff className="mr-2 h-4 w-4 shrink-0" />
)}
<span>Enable</span>
<Switch
aria-hidden="true"
checked={isEnabled}
className="pointer-events-none ml-auto"
tabIndex={-1}
/>
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={() => onDelete(workflow)}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<WorkflowActionsMenu
isEnabled={isEnabled}
isTogglingEnabled={isTogglingEnabled}
onDelete={() => onDelete(workflow)}
onDuplicate={() => onDuplicate(workflow)}
onEdit={() => onEdit(workflow)}
onToggleEnabled={() => onToggleEnabled(workflow)}
onTrigger={() => onTrigger(workflow.id)}
/>
</div>
</div>
@@ -32,12 +32,14 @@ import {
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import { ChannelCombobox } from "./ChannelCombobox";
import { WorkflowActionsMenu } from "./WorkflowActionsMenu";
import {
WorkflowFormBuilder,
type WorkflowEditorMode,
type WorkflowFormBuilderHandle,
} from "./WorkflowFormBuilder";
import { WorkflowWebhookSecretDialog } from "./WorkflowWebhookSecretDialog";
import { getWorkflowEnabled } from "./workflowDefinition";
import type { WorkflowEditorPane } from "./workflowEditorPane";
import {
DEFAULT_FORM_STATE,
@@ -50,8 +52,12 @@ type DialogMode = "create" | "edit" | "duplicate";
type WorkflowDialogProps = {
channels: Channel[];
mode: DialogMode;
onDeleteWorkflow: (workflow: Workflow) => void;
onDuplicateWorkflow: (workflowId: string) => void;
onEditWorkflow: (workflowId: string) => void;
onEditorPaneChange: (pane: WorkflowEditorPane) => void;
onOpenChange: (open: boolean) => void;
onTriggerWorkflow: (workflowId: string) => void;
open: boolean;
pane: WorkflowEditorPane;
workflow?: Workflow | null;
@@ -112,6 +118,24 @@ function yamlWithWorkflowName(yaml: string, name: string): string | null {
return document.toString();
}
function yamlWithWorkflowEnabled(
yaml: string,
enabled: boolean,
): string | null {
if (!yaml.trim()) {
return formStateToYaml({ ...DEFAULT_FORM_STATE, enabled });
}
const document = parseDocument(yaml);
if (document.errors.length > 0 || !isMap(document.contents)) return null;
if (enabled) {
document.delete("enabled");
} else {
document.set("enabled", false);
}
return document.toString();
}
function WorkflowNameEditor({
disabled,
generating,
@@ -214,8 +238,12 @@ function WorkflowNameEditor({
export function WorkflowDialog({
channels,
mode,
onDeleteWorkflow,
onDuplicateWorkflow,
onEditWorkflow,
onEditorPaneChange,
onOpenChange,
onTriggerWorkflow,
open,
pane,
workflow,
@@ -419,6 +447,11 @@ export function WorkflowDialog({
const workflowName = visibleWorkflowName(yamlDefinition, workflow?.name);
const canEditWorkflowName =
!yamlDefinition.trim() || yamlToFormState(yamlDefinition).ok;
const workflowEnabled = parsedDefinition?.ok
? parsedDefinition.state.enabled
: workflow
? getWorkflowEnabled(workflow.definition)
: true;
const handleWorkflowNameCommit = React.useCallback(
(name: string) => {
const nextYaml = yamlWithWorkflowName(yamlDefinitionRef.current, name);
@@ -430,6 +463,16 @@ export function WorkflowDialog({
},
[mutation.reset],
);
const handleToggleWorkflowEnabled = React.useCallback(() => {
const nextYaml = yamlWithWorkflowEnabled(
yamlDefinitionRef.current,
!workflowEnabled,
);
if (nextYaml === null) return;
mutation.reset();
yamlDefinitionRef.current = nextYaml;
setYamlDefinition(nextYaml);
}, [mutation.reset, workflowEnabled]);
const showChannelSelector = mode !== "edit";
@@ -465,7 +508,9 @@ export function WorkflowDialog({
/>
<div
className={
editorMode === "form" && !workflowNameEditing
mode !== "edit" &&
editorMode === "form" &&
!workflowNameEditing
? "flex items-center"
: "hidden"
}
@@ -474,6 +519,17 @@ export function WorkflowDialog({
</div>
</div>
<div className="flex items-center gap-2">
{mode === "edit" && workflow ? (
<WorkflowActionsMenu
isEnabled={workflowEnabled}
isTogglingEnabled={mutation.isPending || !canEditWorkflowName}
onDelete={() => onDeleteWorkflow(workflow)}
onDuplicate={() => onDuplicateWorkflow(workflow.id)}
onEdit={() => onEditWorkflow(workflow.id)}
onToggleEnabled={handleToggleWorkflowEnabled}
onTrigger={() => onTriggerWorkflow(workflow.id)}
/>
) : null}
<DialogClose asChild>
<Button
aria-label="Close"
@@ -493,7 +549,7 @@ export function WorkflowDialog({
channels={channels}
disabled={mutation.isPending}
mode={editorMode}
nameLeadingContainer={nameLeadingElement}
nameLeadingContainer={mode === "edit" ? null : nameLeadingElement}
onChange={(yaml) => {
mutation.reset();
yamlDefinitionRef.current = yaml;
@@ -436,11 +436,19 @@ export const WorkflowFormBuilder = React.forwardRef<
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 },
);
setFormState((current) => {
if (
current.name === result.state.name &&
current.enabled === result.state.enabled
) {
return current;
}
return {
...current,
enabled: result.state.enabled,
name: result.state.name,
};
});
}, [yaml]);
const selectNode = React.useCallback(
@@ -288,11 +288,15 @@ export function WorkflowsView({
: `${editor.mode}:${editor.workflowId}`
}
mode={editor.mode}
onDeleteWorkflow={handleDelete}
onDuplicateWorkflow={onDuplicateWorkflow}
onEditWorkflow={onEditWorkflow}
onEditorPaneChange={onEditorPaneChange}
onOpenChange={(open) => {
if (!open) onCloseEditor();
}}
open
onTriggerWorkflow={handleTrigger}
pane={editor.pane}
workflow={editorWorkflow}
/>