Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions desktop/src/features/workflows/ui/WorkflowActionsMenu.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
205 changes: 124 additions & 81 deletions desktop/src/features/workflows/ui/WorkflowCard.tsx
Original file line number Diff line number Diff line change
@@ -1,138 +1,181 @@
import {
Clock,
Copy,
MoreHorizontal,
Pencil,
Play,
Trash2,
ArrowRight,
CalendarClock,
CircleCheckBig,
GitPullRequest,
Hash,
MessageCircle,
MessageSquare,
Send,
SmilePlus,
Timer,
Webhook,
Zap,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";

import type { Workflow } from "@/shared/api/types";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { cn } from "@/shared/lib/cn";
import { WorkflowActionsMenu } from "./WorkflowActionsMenu";
import {
getWorkflowDescription,
getWorkflowDisplayStatus,
getWorkflowEnabled,
getWorkflowPrimaryAction,
getWorkflowTriggerSummary,
getWorkflowTriggerType,
} from "./workflowDefinition";

type WorkflowCardProps = {
workflow: Workflow;
channelName?: string;
isActive?: boolean;
isTogglingEnabled?: boolean;
onSelect: (workflowId: string) => void;
onTrigger: (workflowId: string) => void;
onToggleEnabled: (workflow: Workflow) => void;
onEdit: (workflow: Workflow) => void;
onDuplicate: (workflow: Workflow) => void;
onDelete: (workflow: Workflow) => void;
};

function StatusBadge({ status }: { status: Workflow["status"] }) {
const variants: Record<
Workflow["status"],
"success" | "secondary" | "warning"
> = {
active: "success",
disabled: "secondary",
archived: "warning",
};
const TRIGGER_ICONS: Record<string, LucideIcon> = {
diff_posted: GitPullRequest,
message_posted: MessageSquare,
reaction_added: SmilePlus,
schedule: CalendarClock,
webhook: Webhook,
};

return <Badge variant={variants[status]}>{status}</Badge>;
const ACTION_ICONS: Record<string, LucideIcon> = {
add_reaction: SmilePlus,
call_webhook: Webhook,
delay: Timer,
request_approval: CircleCheckBig,
send_dm: MessageCircle,
send_message: Send,
set_channel_topic: Hash,
};

const TRIGGER_ACCENTS: Record<string, string> = {
diff_posted: "border-violet-400/30 bg-violet-600 text-white",
message_posted: "border-blue-400/30 bg-blue-600 text-white",
reaction_added: "border-pink-400/30 bg-pink-600 text-white",
schedule: "border-emerald-400/30 bg-emerald-600 text-white",
webhook: "border-orange-300/30 bg-orange-500 text-white",
};

function StatusBadge({ status }: { status: Workflow["status"] }) {
return (
<span
className={cn(
"rounded-full border border-border/65 bg-background/80 px-2 py-1 text-2xs font-semibold uppercase tracking-wider shadow-xs",
status === "active" ? "text-foreground" : "text-muted-foreground",
)}
>
{status}
</span>
);
}

export function WorkflowCard({
workflow,
channelName,
isActive = false,
isTogglingEnabled = false,
onSelect,
onTrigger,
onToggleEnabled,
onEdit,
onDuplicate,
onDelete,
}: WorkflowCardProps) {
const displayStatus = getWorkflowDisplayStatus(workflow);
const description = getWorkflowDescription(workflow.definition);
const triggerSummary = getWorkflowTriggerSummary(workflow.definition);
const description = getWorkflowDescription(workflow.definition);
const triggerType = getWorkflowTriggerType(workflow.definition);
const actionType = getWorkflowPrimaryAction(workflow.definition);
const TriggerIcon = triggerType ? TRIGGER_ICONS[triggerType] : undefined;
const ActionIcon = actionType ? ACTION_ICONS[actionType] : undefined;
const triggerAccent = triggerType ? TRIGGER_ACCENTS[triggerType] : undefined;

return (
<div
className={`relative w-full rounded-lg border bg-card p-3 text-left transition-colors hover:bg-muted/50 ${
isActive ? "border-primary/40 bg-primary/5 shadow-xs" : ""
}`}
className={cn(
"group relative min-h-60 w-full overflow-hidden rounded-2xl border border-border/70 bg-muted/50 p-5 text-left text-foreground shadow-xs transition-all hover:-translate-y-0.5 hover:border-border hover:bg-muted/65 hover:shadow-md",
isActive && "border-primary/50 bg-primary/5 ring-1 ring-primary/30",
)}
data-testid={`workflow-card-${workflow.id}`}
>
<button
className="absolute inset-0 rounded-lg"
className="absolute inset-0 z-0 rounded-2xl focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
onClick={() => onSelect(workflow.id)}
type="button"
>
<span className="sr-only">View {workflow.name}</span>
</button>

<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Zap className="h-4 w-4 shrink-0 text-amber-500" />
<span className="truncate text-sm font-medium">
{workflow.name}
<div className="pointer-events-none relative z-10 flex h-full min-h-48 flex-col">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2" aria-hidden="true">
<span
className={cn(
"flex h-9 w-9 items-center justify-center rounded-xl border shadow-xs",
triggerAccent ?? "border-slate-400/30 bg-slate-600 text-white",
)}
>
{TriggerIcon ? (
<TriggerIcon className="h-5 w-5" />
) : (
<Zap className="h-5 w-5" />
)}
</span>
<StatusBadge status={displayStatus} />
{ActionIcon ? (
<>
<ArrowRight className="h-4 w-4 text-muted-foreground/60" />
<span className="flex h-9 w-9 items-center justify-center rounded-xl border border-border/65 bg-background/80 text-muted-foreground shadow-xs">
<ActionIcon className="h-5 w-5" />
</span>
</>
) : null}
</div>
<div className="mt-1.5 flex items-center gap-3 pl-6 text-2xs text-muted-foreground">
{channelName ? <span>{channelName}</span> : null}
{triggerSummary ? <span>{triggerSummary}</span> : null}
<span className="flex items-center gap-1">
<Clock className="h-4 w-4" />
{new Date(workflow.updatedAt * 1000).toLocaleDateString()}
</span>

<div className="pointer-events-auto flex items-center gap-1.5">
<StatusBadge status={displayStatus} />
<WorkflowActionsMenu
isEnabled={getWorkflowEnabled(workflow.definition)}
isTogglingEnabled={isTogglingEnabled}
onDelete={() => onDelete(workflow)}
onDuplicate={() => onDuplicate(workflow)}
onEdit={() => onEdit(workflow)}
onToggleEnabled={() => onToggleEnabled(workflow)}
onTrigger={() => onTrigger(workflow.id)}
/>
</div>
{description ? (
<p className="mt-2 pl-6 text-xs text-muted-foreground">
{description}
</p>
) : null}
</div>

<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Workflow actions"
className="relative z-10 h-7 w-7 shrink-0"
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>
<DropdownMenuItem
className="text-destructive"
onClick={() => onDelete(workflow)}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{triggerSummary ? (
<p className="mt-4 line-clamp-1 text-xs font-semibold text-muted-foreground">
{triggerSummary}
</p>
) : null}
<h3 className="mt-1 line-clamp-2 text-xl font-bold leading-tight tracking-tight">
{workflow.name}
</h3>
{description ? (
<p className="mt-2 line-clamp-2 text-sm leading-relaxed text-muted-foreground">
{description}
</p>
) : null}

<div className="mt-auto flex min-w-0 items-end justify-between gap-3 pt-5 text-muted-foreground">
<p className="min-w-0 truncate text-2xs">
{channelName ? `#${channelName}` : "Channel workflow"}
</p>
<span className="shrink-0 text-2xs">
{new Date(workflow.updatedAt * 1000).toLocaleDateString()}
</span>
</div>
</div>
</div>
);
Expand Down
Loading
Loading