Skip to content

Commit 707739f

Browse files
committed
feat(web): enhance WorkflowCanvas with keyboard shortcuts and tooltip improvements for action buttons
1 parent 55bf77f commit 707739f

1 file changed

Lines changed: 118 additions & 53 deletions

File tree

apps/web/src/components/workflow/workflow-canvas.tsx

Lines changed: 118 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
Square,
2828
X,
2929
} from "lucide-react";
30+
import { useEffect, useRef } from "react";
3031

3132
import { Button } from "@/components/ui/button";
3233
import {
@@ -76,7 +77,7 @@ function CanvasButton({
7677
: `absolute top-4 ${position} z-50`;
7778

7879
return (
79-
<Tooltip>
80+
<Tooltip delayDuration={0}>
8081
<TooltipTrigger asChild>
8182
<Button
8283
onClick={onClick}
@@ -102,7 +103,7 @@ interface ActionBarButtonProps {
102103
onClick: (e: React.MouseEvent) => void;
103104
disabled?: boolean;
104105
className?: string;
105-
tooltip: string;
106+
tooltip: React.ReactNode;
106107
children: React.ReactNode;
107108
position?: "first" | "middle" | "last" | "only";
108109
}
@@ -117,30 +118,27 @@ function ActionBarButton({
117118
}: ActionBarButtonProps) {
118119
const roundingClass = {
119120
first: "rounded-l-lg rounded-r-none",
120-
middle: "rounded-none border-l-0",
121-
last: "rounded-r-lg rounded-l-none border-l-0",
121+
middle: "rounded-none",
122+
last: "rounded-r-lg rounded-l-none",
122123
only: "rounded-lg",
123124
}[position];
124125

125126
return (
126-
<Tooltip>
127-
<TooltipTrigger asChild>
128-
<Button
129-
onClick={onClick}
130-
disabled={disabled}
131-
className={cn(
132-
"h-10 px-3 border-neutral-300 shadow-sm",
133-
roundingClass,
134-
className,
135-
{ "opacity-50 cursor-not-allowed": disabled }
136-
)}
137-
>
138-
{children}
139-
</Button>
140-
</TooltipTrigger>
141-
<TooltipContent>
142-
<p>{tooltip}</p>
143-
</TooltipContent>
127+
<Tooltip delayDuration={0}>
128+
<div className={cn("bg-background", roundingClass)}>
129+
<TooltipTrigger asChild>
130+
<Button
131+
onClick={onClick}
132+
disabled={disabled}
133+
className={cn("h-10 px-3", className, roundingClass, {
134+
"opacity-50 cursor-not-allowed": disabled,
135+
})}
136+
>
137+
{children}
138+
</Button>
139+
</TooltipTrigger>
140+
<TooltipContent>{tooltip}</TooltipContent>
141+
</div>
144142
</Tooltip>
145143
);
146144
}
@@ -197,38 +195,45 @@ function ActionButton({
197195
idle: {
198196
icon: <Play className="!size-4" />,
199197
title: "Execute Workflow",
198+
shortcut: "⌘⏎",
200199
className: "bg-green-600 hover:bg-green-700 text-white border-green-600",
201200
},
202201
submitted: {
203202
icon: <Square className="!size-4" />,
204203
title: "Stop Execution",
204+
shortcut: "⌘⏎",
205205
className: "bg-red-500 hover:bg-red-600 text-white border-red-500",
206206
},
207207
executing: {
208208
icon: <Square className="!size-4" />,
209209
title: "Stop Execution",
210+
shortcut: "⌘⏎",
210211
className: "bg-red-500 hover:bg-red-600 text-white border-red-500",
211212
},
212213
completed: {
213214
icon: <X className="!size-4" />,
214215
title: "Clear Outputs & Reset",
216+
shortcut: "⌘⏎",
215217
className:
216218
"bg-emerald-600 hover:bg-emerald-700 text-white border-emerald-600",
217219
},
218220
error: {
219221
icon: <X className="!size-4" />,
220222
title: "Clear Errors & Reset",
223+
shortcut: "⌘⏎",
221224
className: "bg-rose-600 hover:bg-rose-700 text-white border-rose-600",
222225
},
223226
cancelled: {
224227
icon: <Play className="!size-4" />,
225228
title: "Restart Workflow",
229+
shortcut: "⌘⏎",
226230
className:
227231
"bg-neutral-600 hover:bg-neutral-700 text-white border-neutral-600",
228232
},
229233
paused: {
230234
icon: <Play className="!size-4" />,
231235
title: "Resume Workflow",
236+
shortcut: "⌘⏎",
232237
className: "bg-blue-500 hover:bg-blue-600 text-white border-blue-500",
233238
},
234239
};
@@ -241,7 +246,18 @@ function ActionButton({
241246
onClick={onClick}
242247
disabled={disabled}
243248
className={config.className}
244-
tooltip={config.title}
249+
tooltip={
250+
<div className="flex items-center gap-2">
251+
<span>{config.title}</span>
252+
<div className="flex items-center gap-1">
253+
{config.shortcut.split("").map((key) => (
254+
<kbd className="px-1 py-0.25 text-xs rounded border font-mono">
255+
{key}
256+
</kbd>
257+
))}
258+
</div>
259+
</div>
260+
}
245261
position="first"
246262
>
247263
{config.icon}
@@ -261,8 +277,8 @@ function DeployButton({
261277
onClick={onClick}
262278
disabled={disabled}
263279
className="bg-blue-600 hover:bg-blue-700 text-white border-blue-600"
264-
tooltip="Deploy Workflow"
265-
position="middle"
280+
tooltip={<p>Deploy Workflow</p>}
281+
position="last"
266282
>
267283
<ArrowUpToLine className="!size-4" />
268284
</ActionBarButton>
@@ -278,9 +294,8 @@ function SidebarToggle({ onClick, isSidebarVisible }: SidebarToggleProps) {
278294
return (
279295
<ActionBarButton
280296
onClick={onClick}
281-
tooltip={isSidebarVisible ? "Hide Sidebar" : "Show Sidebar"}
282-
className="bg-neutral-100 hover:bg-neutral-200 text-neutral-700 border-neutral-300"
283-
position="last"
297+
tooltip={<p>{isSidebarVisible ? "Hide Sidebar" : "Show Sidebar"}</p>}
298+
position="only"
284299
>
285300
{isSidebarVisible ? (
286301
<PanelLeftClose className="!size-4 rotate-180" />
@@ -300,18 +315,18 @@ function OutputsToggle({
300315
expandedOutputs: boolean;
301316
disabled?: boolean;
302317
}) {
318+
const tooltipText = disabled
319+
? "No outputs to show"
320+
: expandedOutputs
321+
? "Hide All Outputs"
322+
: "Show All Outputs";
323+
303324
return (
304325
<ActionBarButton
305326
onClick={onClick}
306327
disabled={disabled}
307328
className="bg-neutral-600 hover:bg-neutral-700 text-white border-neutral-600"
308-
tooltip={
309-
disabled
310-
? "No outputs to show"
311-
: expandedOutputs
312-
? "Hide All Outputs"
313-
: "Show All Outputs"
314-
}
329+
tooltip={<p>{tooltipText}</p>}
315330
position="middle"
316331
>
317332
{expandedOutputs ? (
@@ -352,6 +367,51 @@ export function WorkflowCanvas({
352367
node.data.outputs.some((output) => output.value !== undefined)
353368
);
354369

370+
const reactFlowRef = useRef<ReactFlowInstance<
371+
ReactFlowNode<WorkflowNodeType>,
372+
ReactFlowEdge<WorkflowEdgeType>
373+
> | null>(null);
374+
375+
// Keyboard shortcut handling
376+
useEffect(() => {
377+
const handleKeyDown = (event: KeyboardEvent) => {
378+
// Cmd+Enter (Mac) or Ctrl+Enter (Windows/Linux) - Execute workflow
379+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
380+
event.preventDefault();
381+
if (onAction && !readonly && nodes.length > 0) {
382+
const syntheticEvent = new MouseEvent("click", {
383+
bubbles: true,
384+
cancelable: true,
385+
}) as unknown as React.MouseEvent;
386+
onAction(syntheticEvent);
387+
}
388+
return;
389+
}
390+
391+
// Esc - Center the chart
392+
if (event.key === "Escape") {
393+
event.preventDefault();
394+
if (reactFlowRef.current) {
395+
reactFlowRef.current.fitView({ padding: 0.1, duration: 300 });
396+
}
397+
return;
398+
}
399+
};
400+
401+
document.addEventListener("keydown", handleKeyDown);
402+
return () => document.removeEventListener("keydown", handleKeyDown);
403+
}, [onAction, readonly, nodes.length]);
404+
405+
const handleInit = (
406+
instance: ReactFlowInstance<
407+
ReactFlowNode<WorkflowNodeType>,
408+
ReactFlowEdge<WorkflowEdgeType>
409+
>
410+
) => {
411+
reactFlowRef.current = instance;
412+
onInit(instance);
413+
};
414+
355415
return (
356416
<TooltipProvider>
357417
<ReactFlow
@@ -370,7 +430,7 @@ export function WorkflowCanvas({
370430
connectionMode={ConnectionMode.Strict}
371431
connectionLineComponent={WorkflowConnectionLine}
372432
connectionRadius={8}
373-
onInit={onInit}
433+
onInit={handleInit}
374434
isValidConnection={isValidConnection}
375435
fitView
376436
minZoom={0.05}
@@ -404,45 +464,50 @@ export function WorkflowCanvas({
404464
)}
405465

406466
{/* Main Action Bar */}
407-
{((!readonly && (onAction || onDeploy || onToggleExpandedOutputs)) ||
408-
onToggleSidebar) && (
409-
<div className="absolute top-4 right-4 flex items-center rounded-lg shadow-lg bg-background z-50 overflow-hidden">
410-
{!readonly && onAction && (
467+
{!readonly && (onAction || onDeploy || onToggleExpandedOutputs) && (
468+
<div
469+
className={cn(
470+
"absolute top-4 flex items-center gap-0.5 z-50",
471+
onToggleSidebar && isSidebarVisible !== undefined
472+
? "right-16" // Position to the left of sidebar with spacing
473+
: "right-4" // Position at right edge if no sidebar
474+
)}
475+
>
476+
{onAction && (
411477
<ActionButton
412478
onClick={onAction}
413479
workflowStatus={workflowStatus}
414480
disabled={nodes.length === 0}
415481
/>
416482
)}
417483

418-
{!readonly && onToggleExpandedOutputs && (
484+
{onToggleExpandedOutputs && (
419485
<OutputsToggle
420486
onClick={onToggleExpandedOutputs}
421487
expandedOutputs={expandedOutputs}
422488
disabled={!hasAnyOutputs}
423489
/>
424490
)}
425491

426-
{!readonly && onDeploy && (
492+
{onDeploy && (
427493
<DeployButton onClick={onDeploy} disabled={nodes.length === 0} />
428494
)}
495+
</div>
496+
)}
429497

430-
{onToggleSidebar && isSidebarVisible !== undefined && (
431-
<SidebarToggle
432-
onClick={onToggleSidebar}
433-
isSidebarVisible={isSidebarVisible}
434-
/>
435-
)}
498+
{/* Sidebar Toggle - Rightmost position */}
499+
{onToggleSidebar && isSidebarVisible !== undefined && (
500+
<div className="absolute top-4 right-4 z-50">
501+
<SidebarToggle
502+
onClick={onToggleSidebar}
503+
isSidebarVisible={isSidebarVisible}
504+
/>
436505
</div>
437506
)}
438507

439508
{onAddNode && !readonly && (
440509
<CanvasButton
441-
onClick={(e) => {
442-
e.preventDefault();
443-
e.stopPropagation();
444-
onAddNode();
445-
}}
510+
onClick={onAddNode}
446511
position="bottom-4 right-4"
447512
tooltip="Add Node"
448513
>

0 commit comments

Comments
 (0)