mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Polish workflow editor actions
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user