mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Add workflow enable toggle to card menu
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -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" {
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user