Files
buzz/desktop/src/shared/api/tauriWorkflows.ts
Taylor Ho edc4a09aaa feat(workflows): add responsive library card actions (#6008)
**Category:** improvement
**User Impact:** Users can scan what each workflow does and trigger,
edit, duplicate, enable, disable, or delete it directly from the
library.
**Problem:** The workflow list buried common actions and did not expose
each automation's trigger-to-action shape at a glance.
**Solution:** Add a responsive workflow library with a persistent create
tile, compact trigger/action diagrams, prominent workflow titles with
supporting descriptions, and shared card actions while preserving
existing detail, editor, and run-history entry points. Card toggles
refresh both list and open-detail caches so status and definition stay
consistent.

<details>
<summary>File changes</summary>

**desktop/src/features/workflows/ui/WorkflowActionsMenu.tsx**
Adds a shared card menu for trigger, edit, duplicate, enable/disable,
and delete actions.

**desktop/src/features/workflows/ui/WorkflowCard.tsx**
Reworks cards around the prototype's visual hierarchy: color-coded
trigger, action flow, sentence-case eyebrow, prominent title, supporting
description, status, channel, and update date without a footer clock
icon.

**desktop/src/features/workflows/ui/WorkflowsView.tsx**
Adds the responsive grid, create tile, mutation wiring, and list/detail
cache invalidation. Container breakpoints keep cards two-across at
medium widths and three-across in the 1280px desktop layout.

**desktop/src/features/workflows/ui/workflowDefinition.ts**
Adds immutable enabled-state updates plus narrow trigger and
first-action readers used only to select card icons.

**desktop/src/features/workflows/ui/workflowDefinition.test.mjs**
Covers neutral icon selection, enabled-state immutability, and status
presentation.

**desktop/tests/e2e/workflows.spec.ts**
Covers the create tile, title/description hierarchy, selected-card
enable/disable consistency, and deterministic narrow/medium/wide
captures while retaining existing action coverage.

</details>

## Reproduction steps

1. Open **Workflows** and confirm the create tile stays first as cards
flow from one to three columns with available width.
2. Confirm each card shows a sentence-case trigger eyebrow, prominent
workflow title, supporting description when present, status, channel,
and update date without a clock icon.
3. Open a card's overflow menu and trigger, edit, duplicate,
enable/disable, or delete the workflow.
4. Leave the detail panel open while toggling and confirm its badge and
JSON definition update with the card.

## Screenshots

Real built E2E UI with representative workflow data at three viewport
sizes.

### Narrow — 800 × 720

![Workflow library at 800 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-narrow-482d1b4c8.png)

### Medium — 1024 × 720

![Workflow library at 1024 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-medium-482d1b4c8.png)

### Wide — 1280 × 720

![Workflow library at 1280 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-wide-482d1b4c8.png)

### Card actions

![Workflow library actions at 1280 by
720](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6008/workflow-library-wide-actions-482d1b4c8.png)

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
2026-08-17 16:56:34 +00:00

295 lines
7.3 KiB
TypeScript

import { invokeTauri } from "@/shared/api/tauri";
import type {
ApprovalActionResponse,
TriggerWorkflowResponse,
Workflow,
WorkflowApproval,
WorkflowRun,
WorkflowSaveResult,
TraceEntry,
} from "@/shared/api/types";
// ── Raw types (snake_case from backend) ───────────────────────────────────
type RawWorkflow = {
id: string;
revision: string;
name: string;
owner_pubkey: string;
channel_id: string | null;
definition: Record<string, unknown>;
status: Workflow["status"];
created_at: number;
updated_at: number;
};
type RawWorkflowSaveResponse = RawWorkflow & {
webhook_secret?: string | null;
};
type RawTraceEntry = {
step_id: string;
status: string;
output?: Record<string, unknown>;
started_at?: number | null;
completed_at?: number | null;
error?: string | null;
};
type RawWorkflowRun = {
id: string;
workflow_id: string;
status: WorkflowRun["status"];
current_step: number | null;
execution_trace: RawTraceEntry[];
started_at: number | null;
completed_at: number | null;
error_code?: string | null;
error_message: string | null;
created_at: number;
};
type RawWorkflowRunCursor = {
before: string;
before_id: string;
};
type RawWorkflowRunsResponse = {
runs: RawWorkflowRun[];
next: RawWorkflowRunCursor | null;
};
type RawWorkflowApproval = {
approval_ref: string;
workflow_id: string;
run_id: string;
step_id: string;
step_index: number;
approver_spec: string;
status: WorkflowApproval["status"];
approver_pubkey: string | null;
note: string | null;
expires_at: string;
created_at: number;
};
type RawWorkflowApprovalsResponse = {
approvals: RawWorkflowApproval[];
};
type RawTriggerWorkflowResponse = {
run_id: string;
workflow_id: string;
status: string;
};
type RawApprovalActionResponse = {
token: string;
status: string;
run_id: string;
workflow_id: string;
};
// ── Conversion functions ──────────────────────────────────────────────────
function fromRawWorkflow(raw: RawWorkflow): Workflow {
return {
id: raw.id,
revision: raw.revision,
name: raw.name,
ownerPubkey: raw.owner_pubkey,
channelId: raw.channel_id,
definition: raw.definition,
status: raw.status,
createdAt: raw.created_at,
updatedAt: raw.updated_at,
};
}
function fromRawWorkflowSave(raw: RawWorkflowSaveResponse): WorkflowSaveResult {
return {
workflow: fromRawWorkflow(raw),
webhookSecret: raw.webhook_secret ?? null,
};
}
function fromRawTraceEntry(raw: RawTraceEntry): TraceEntry {
return {
stepId: raw.step_id,
status: raw.status,
output: raw.output ?? {},
startedAt: raw.started_at ?? null,
completedAt: raw.completed_at ?? null,
error: raw.error ?? null,
};
}
function fromRawWorkflowRun(raw: RawWorkflowRun): WorkflowRun {
return {
id: raw.id,
workflowId: raw.workflow_id,
status: raw.status,
currentStep: raw.current_step,
executionTrace: raw.execution_trace.map(fromRawTraceEntry),
startedAt: raw.started_at,
completedAt: raw.completed_at,
errorCode: raw.error_code ?? null,
errorMessage: raw.error_message,
createdAt: raw.created_at,
};
}
export function fromRawApproval(raw: RawWorkflowApproval): WorkflowApproval {
return {
approvalRef: raw.approval_ref,
workflowId: raw.workflow_id,
runId: raw.run_id,
stepId: raw.step_id,
stepIndex: raw.step_index,
approverSpec: raw.approver_spec,
status: raw.status,
approverPubkey: raw.approver_pubkey,
note: raw.note,
expiresAt: raw.expires_at,
createdAt: raw.created_at,
};
}
function fromRawTriggerResponse(
raw: RawTriggerWorkflowResponse,
): TriggerWorkflowResponse {
return {
runId: raw.run_id,
workflowId: raw.workflow_id,
status: raw.status,
};
}
function fromRawApprovalResponse(
raw: RawApprovalActionResponse,
): ApprovalActionResponse {
return {
token: raw.token,
status: raw.status,
runId: raw.run_id,
workflowId: raw.workflow_id,
};
}
// ── Tauri invoke wrappers ─────────────────────────────────────────────────
export async function getChannelWorkflows(
channelId: string,
): Promise<Workflow[]> {
const raw = await invokeTauri<RawWorkflow[]>("get_channel_workflows", {
channelId,
});
return raw.map(fromRawWorkflow);
}
/**
* Fetch workflows across many channels in a single relay round-trip.
*
* Replaces the per-channel `Promise.all(getChannelWorkflows)` fanout on the
* Workflows overview: the backend `#h` filter matches any listed channel, and
* each returned workflow carries its own `channelId` so callers can group.
*/
export async function getChannelsWorkflows(
channelIds: string[],
): Promise<Workflow[]> {
const raw = await invokeTauri<RawWorkflow[]>("get_channels_workflows", {
channelIds,
});
return raw.map(fromRawWorkflow);
}
export async function getWorkflow(workflowId: string): Promise<Workflow> {
const raw = await invokeTauri<RawWorkflow>("get_workflow", { workflowId });
return fromRawWorkflow(raw);
}
export async function createWorkflow(
channelId: string,
yamlDefinition: string,
): Promise<WorkflowSaveResult> {
const raw = await invokeTauri<RawWorkflowSaveResponse>("create_workflow", {
channelId,
yamlDefinition,
});
return fromRawWorkflowSave(raw);
}
export async function updateWorkflow(
workflowId: string,
yamlDefinition: string,
expectedRevision: string,
): Promise<WorkflowSaveResult> {
const raw = await invokeTauri<RawWorkflowSaveResponse>("update_workflow", {
workflowId,
yamlDefinition,
expectedRevision,
});
return fromRawWorkflowSave(raw);
}
export async function deleteWorkflow(workflowId: string): Promise<void> {
await invokeTauri("delete_workflow", { workflowId });
}
export async function getWorkflowRuns(
workflowId: string,
limit?: number,
): Promise<WorkflowRun[]> {
const raw = await invokeTauri<RawWorkflowRunsResponse>("get_workflow_runs", {
workflowId,
limit: limit ?? null,
});
return raw.runs.map(fromRawWorkflowRun);
}
export async function getRunApprovals(
workflowId: string,
runId: string,
): Promise<WorkflowApproval[]> {
const raw = await invokeTauri<RawWorkflowApprovalsResponse>(
"get_run_approvals",
{
workflowId,
runId,
},
);
return raw.approvals.map(fromRawApproval);
}
export async function triggerWorkflow(
workflowId: string,
): Promise<TriggerWorkflowResponse> {
const raw = await invokeTauri<RawTriggerWorkflowResponse>(
"trigger_workflow",
{ workflowId },
);
return fromRawTriggerResponse(raw);
}
export async function grantApproval(
token: string,
note?: string,
): Promise<ApprovalActionResponse> {
const raw = await invokeTauri<RawApprovalActionResponse>("grant_approval", {
token,
note: note ?? null,
});
return fromRawApprovalResponse(raw);
}
export async function denyApproval(
token: string,
note?: string,
): Promise<ApprovalActionResponse> {
const raw = await invokeTauri<RawApprovalActionResponse>("deny_approval", {
token,
note: note ?? null,
});
return fromRawApprovalResponse(raw);
}