Add workflow enable toggle to card menu

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
Taylor Ho
2026-08-14 00:21:19 -07:00
parent 37f288942b
commit 0d92496f69
5 changed files with 110 additions and 0 deletions
@@ -11,6 +11,8 @@ import {
MoreHorizontal,
Pencil,
Play,
Power,
PowerOff,
Send,
SmilePlus,
Timer,
@@ -31,12 +33,14 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import {
getWorkflowCardLabel,
getWorkflowDescription,
getWorkflowDisplayStatus,
getWorkflowEnabled,
getWorkflowPrimaryAction,
getWorkflowPrimaryActionChannel,
getWorkflowPrimaryActionEmoji,
@@ -50,7 +54,9 @@ type WorkflowCardProps = {
workflow: Workflow;
channelName?: string;
isActive?: boolean;
isTogglingEnabled?: boolean;
onTrigger: (workflowId: string) => void;
onToggleEnabled: (workflow: Workflow) => void;
onEdit: (workflow: Workflow) => void;
onDuplicate: (workflow: Workflow) => void;
onDelete: (workflow: Workflow) => void;
@@ -182,12 +188,15 @@ export function WorkflowCard({
workflow,
channelName,
isActive = false,
isTogglingEnabled = false,
onTrigger,
onToggleEnabled,
onEdit,
onDuplicate,
onDelete,
}: WorkflowCardProps) {
const customEmoji = useCustomEmoji();
const isEnabled = getWorkflowEnabled(workflow.definition);
const displayStatus = getWorkflowDisplayStatus(workflow);
const description = getWorkflowDescription(workflow.definition);
const configuredTrigger = getWorkflowTriggerConfig(workflow.definition);
@@ -321,6 +330,18 @@ export function WorkflowCard({
<Copy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
disabled={isTogglingEnabled}
onClick={() => onToggleEnabled(workflow)}
>
{isEnabled ? (
<PowerOff className="mr-2 h-4 w-4" />
) : (
<Power className="mr-2 h-4 w-4" />
)}
{isEnabled ? "Disable" : "Enable"}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={() => onDelete(workflow)}
@@ -1,6 +1,7 @@
import { Plus, RefreshCw, Zap } from "lucide-react";
import * as React from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { stringify as yamlStringify } from "yaml";
import {
allWorkflowsQueryKey,
@@ -10,11 +11,16 @@ import { WorkflowCard } from "@/features/workflows/ui/WorkflowCard";
import { WorkflowDeleteDialog } from "@/features/workflows/ui/WorkflowDeleteDialog";
import { WorkflowDetailPanel } from "@/features/workflows/ui/WorkflowDetailPanel";
import { WorkflowDialog } from "@/features/workflows/ui/WorkflowDialog";
import {
getWorkflowEnabled,
withWorkflowEnabled,
} from "@/features/workflows/ui/workflowDefinition";
import type { Channel, Workflow } from "@/shared/api/types";
import {
deleteWorkflow,
getChannelsWorkflows,
triggerWorkflow,
updateWorkflow,
} from "@/shared/api/tauriWorkflows";
import { Button } from "@/shared/ui/button";
import { Skeleton } from "@/shared/ui/skeleton";
@@ -126,6 +132,26 @@ export function WorkflowsView({
},
});
const toggleEnabledMutation = useMutation({
mutationFn: (workflow: Workflow) =>
updateWorkflow(
workflow.id,
yamlStringify(
withWorkflowEnabled(
workflow.definition,
!getWorkflowEnabled(workflow.definition),
),
),
),
onSuccess: () => {
void queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === "workflows" ||
query.queryKey[0] === "workflows-all",
});
},
});
const triggerOne = triggerMutation.mutate;
const handleTrigger = React.useCallback(
(workflowId: string) => triggerOne(workflowId),
@@ -156,6 +182,12 @@ export function WorkflowsView({
[],
);
const toggleEnabled = toggleEnabledMutation.mutate;
const handleToggleEnabled = React.useCallback(
(workflow: Workflow) => toggleEnabled(workflow),
[toggleEnabled],
);
const handleDialogOpenChange = React.useCallback((open: boolean) => {
if (!open) {
setDialogState({ mode: "closed" });
@@ -235,10 +267,15 @@ export function WorkflowsView({
<WorkflowCard
channelName={channelName}
isActive={selectedWorkflowId === workflow.id}
isTogglingEnabled={
toggleEnabledMutation.isPending &&
toggleEnabledMutation.variables?.id === workflow.id
}
key={workflow.id}
onDelete={handleDelete}
onDuplicate={handleDuplicate}
onEdit={handleEdit}
onToggleEnabled={handleToggleEnabled}
onTrigger={handleTrigger}
workflow={workflow}
/>
@@ -4,6 +4,7 @@ import test from "node:test";
import {
getWorkflowCardLabel,
getWorkflowPrimaryActionEmoji,
withWorkflowEnabled,
} from "./workflowDefinition.ts";
test("builds a plain-language workflow card label", () => {
@@ -111,6 +112,23 @@ test("returns the primary add-reaction emoji for rich card rendering", () => {
);
});
test("toggles workflow enabled state without mutating its definition", () => {
const definition = {
name: "deploy",
enabled: false,
trigger: { on: "message_posted" },
future_field: { keep: true },
};
assert.deepEqual(withWorkflowEnabled(definition, true), {
name: "deploy",
trigger: { on: "message_posted" },
future_field: { keep: true },
});
assert.deepEqual(withWorkflowEnabled(definition, false), definition);
assert.equal(definition.enabled, false);
});
test("summarizes common and custom schedules", () => {
assert.equal(
getWorkflowCardLabel({
@@ -362,6 +362,19 @@ export function getWorkflowEnabled(
return definition.enabled !== false;
}
export function withWorkflowEnabled(
definition: Record<string, unknown>,
enabled: boolean,
): Record<string, unknown> {
const updated = { ...definition };
if (enabled) {
delete updated.enabled;
} else {
updated.enabled = false;
}
return updated;
}
export function getWorkflowDisplayStatus(
workflow: Workflow,
): Workflow["status"] | "disabled" {
+21
View File
@@ -922,6 +922,27 @@ test("captures disabled diff workflows in the list UI", async ({ page }) => {
await expect(card).toContainText("disabled");
});
test("enables and disables a workflow from its card menu", async ({ page }) => {
const workflowName = `toggle_workflow_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
const card = page
.locator('[data-testid^="workflow-card-"]')
.filter({ hasText: workflowName })
.first();
const actions = card.getByRole("button", { name: "Workflow actions" });
await actions.click();
await page.getByRole("menuitem", { name: "Disable" }).click();
await expect(card.getByText("disabled", { exact: true })).toBeVisible();
await actions.click();
await page.getByRole("menuitem", { name: "Enable" }).click();
await expect(card.getByText("active", { exact: true })).toBeVisible();
});
test("shows the webhook secret dialog after saving a webhook workflow", async ({
page,
}) => {