Skip to content

Commit 0c8d05f

Browse files
authored
feat(workflow): added context menu for block, pane, and multi-block selection on canvas (#2656)
* feat(workflow): added context menu for block, pane, and multi-block selection on canvas * added more * ack PR comments
1 parent 4301342 commit 0c8d05f

File tree

11 files changed

+872
-23
lines changed

11 files changed

+872
-23
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1049,7 +1049,7 @@ export function Chat() {
10491049
onClick={() => document.getElementById('floating-chat-file-input')?.click()}
10501050
title='Attach file'
10511051
className={cn(
1052-
'!bg-transparent cursor-pointer rounded-[6px] p-[0px]',
1052+
'!bg-transparent !border-0 cursor-pointer rounded-[6px] p-[0px]',
10531053
(!activeWorkflowId || isExecuting || chatFiles.length >= 15) &&
10541054
'cursor-not-allowed opacity-50'
10551055
)}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
'use client'
2+
3+
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
4+
import type { BlockContextMenuProps } from './types'
5+
6+
/**
7+
* Context menu for workflow block(s).
8+
* Displays block-specific actions in a popover at right-click position.
9+
* Supports multi-selection - actions apply to all selected blocks.
10+
*/
11+
export function BlockContextMenu({
12+
isOpen,
13+
position,
14+
menuRef,
15+
onClose,
16+
selectedBlocks,
17+
onCopy,
18+
onPaste,
19+
onDuplicate,
20+
onDelete,
21+
onToggleEnabled,
22+
onToggleHandles,
23+
onRemoveFromSubflow,
24+
onOpenEditor,
25+
onRename,
26+
hasClipboard = false,
27+
showRemoveFromSubflow = false,
28+
disableEdit = false,
29+
}: BlockContextMenuProps) {
30+
const isSingleBlock = selectedBlocks.length === 1
31+
32+
const allEnabled = selectedBlocks.every((b) => b.enabled)
33+
const allDisabled = selectedBlocks.every((b) => !b.enabled)
34+
35+
const hasStarterBlock = selectedBlocks.some(
36+
(b) => b.type === 'starter' || b.type === 'start_trigger'
37+
)
38+
const allNoteBlocks = selectedBlocks.every((b) => b.type === 'note')
39+
const isSubflow =
40+
isSingleBlock && (selectedBlocks[0]?.type === 'loop' || selectedBlocks[0]?.type === 'parallel')
41+
42+
const canRemoveFromSubflow = showRemoveFromSubflow && !hasStarterBlock
43+
44+
const getToggleEnabledLabel = () => {
45+
if (allEnabled) return 'Disable'
46+
if (allDisabled) return 'Enable'
47+
return 'Toggle Enabled'
48+
}
49+
50+
return (
51+
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
52+
<PopoverAnchor
53+
style={{
54+
position: 'fixed',
55+
left: `${position.x}px`,
56+
top: `${position.y}px`,
57+
width: '1px',
58+
height: '1px',
59+
}}
60+
/>
61+
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
62+
{/* Copy */}
63+
<PopoverItem
64+
className='group'
65+
onClick={() => {
66+
onCopy()
67+
onClose()
68+
}}
69+
>
70+
<span>Copy</span>
71+
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘C</span>
72+
</PopoverItem>
73+
74+
{/* Paste */}
75+
<PopoverItem
76+
className='group'
77+
disabled={disableEdit || !hasClipboard}
78+
onClick={() => {
79+
onPaste()
80+
onClose()
81+
}}
82+
>
83+
<span>Paste</span>
84+
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘V</span>
85+
</PopoverItem>
86+
87+
{/* Duplicate - hide for starter blocks */}
88+
{!hasStarterBlock && (
89+
<PopoverItem
90+
disabled={disableEdit}
91+
onClick={() => {
92+
onDuplicate()
93+
onClose()
94+
}}
95+
>
96+
Duplicate
97+
</PopoverItem>
98+
)}
99+
100+
{/* Delete */}
101+
<PopoverItem
102+
className='group'
103+
disabled={disableEdit}
104+
onClick={() => {
105+
onDelete()
106+
onClose()
107+
}}
108+
>
109+
<span>Delete</span>
110+
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'></span>
111+
</PopoverItem>
112+
113+
{/* Enable/Disable - hide if all blocks are notes */}
114+
{!allNoteBlocks && (
115+
<PopoverItem
116+
disabled={disableEdit}
117+
onClick={() => {
118+
onToggleEnabled()
119+
onClose()
120+
}}
121+
>
122+
{getToggleEnabledLabel()}
123+
</PopoverItem>
124+
)}
125+
126+
{/* Flip Handles - hide if all blocks are notes */}
127+
{!allNoteBlocks && (
128+
<PopoverItem
129+
disabled={disableEdit}
130+
onClick={() => {
131+
onToggleHandles()
132+
onClose()
133+
}}
134+
>
135+
Flip Handles
136+
</PopoverItem>
137+
)}
138+
139+
{/* Remove from Subflow - only show when applicable */}
140+
{canRemoveFromSubflow && (
141+
<PopoverItem
142+
disabled={disableEdit}
143+
onClick={() => {
144+
onRemoveFromSubflow()
145+
onClose()
146+
}}
147+
>
148+
Remove from Subflow
149+
</PopoverItem>
150+
)}
151+
152+
{/* Rename - only for single block, not subflows */}
153+
{isSingleBlock && !isSubflow && (
154+
<PopoverItem
155+
disabled={disableEdit}
156+
onClick={() => {
157+
onRename()
158+
onClose()
159+
}}
160+
>
161+
Rename
162+
</PopoverItem>
163+
)}
164+
165+
{/* Open Editor - only for single block */}
166+
{isSingleBlock && (
167+
<PopoverItem
168+
onClick={() => {
169+
onOpenEditor()
170+
onClose()
171+
}}
172+
>
173+
Open Editor
174+
</PopoverItem>
175+
)}
176+
</PopoverContent>
177+
</Popover>
178+
)
179+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export { BlockContextMenu } from './block-context-menu'
2+
export { PaneContextMenu } from './pane-context-menu'
3+
export type {
4+
BlockContextMenuProps,
5+
ContextMenuBlockInfo,
6+
ContextMenuPosition,
7+
PaneContextMenuProps,
8+
} from './types'
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use client'
2+
3+
import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
4+
import type { PaneContextMenuProps } from './types'
5+
6+
/**
7+
* Context menu for workflow canvas pane.
8+
* Displays canvas-level actions when right-clicking empty space.
9+
*/
10+
export function PaneContextMenu({
11+
isOpen,
12+
position,
13+
menuRef,
14+
onClose,
15+
onUndo,
16+
onRedo,
17+
onPaste,
18+
onAddBlock,
19+
onAutoLayout,
20+
onOpenLogs,
21+
onOpenVariables,
22+
onOpenChat,
23+
onInvite,
24+
hasClipboard = false,
25+
disableEdit = false,
26+
disableAdmin = false,
27+
}: PaneContextMenuProps) {
28+
return (
29+
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
30+
<PopoverAnchor
31+
style={{
32+
position: 'fixed',
33+
left: `${position.x}px`,
34+
top: `${position.y}px`,
35+
width: '1px',
36+
height: '1px',
37+
}}
38+
/>
39+
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
40+
{/* Undo */}
41+
<PopoverItem
42+
className='group'
43+
disabled={disableEdit}
44+
onClick={() => {
45+
onUndo()
46+
onClose()
47+
}}
48+
>
49+
<span>Undo</span>
50+
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘Z</span>
51+
</PopoverItem>
52+
53+
{/* Redo */}
54+
<PopoverItem
55+
className='group'
56+
disabled={disableEdit}
57+
onClick={() => {
58+
onRedo()
59+
onClose()
60+
}}
61+
>
62+
<span>Redo</span>
63+
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘⇧Z</span>
64+
</PopoverItem>
65+
66+
{/* Paste */}
67+
<PopoverItem
68+
className='group'
69+
disabled={disableEdit || !hasClipboard}
70+
onClick={() => {
71+
onPaste()
72+
onClose()
73+
}}
74+
>
75+
<span>Paste</span>
76+
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘V</span>
77+
</PopoverItem>
78+
79+
{/* Add Block */}
80+
<PopoverItem
81+
className='group'
82+
disabled={disableEdit}
83+
onClick={() => {
84+
onAddBlock()
85+
onClose()
86+
}}
87+
>
88+
<span>Add Block</span>
89+
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘K</span>
90+
</PopoverItem>
91+
92+
{/* Auto-layout */}
93+
<PopoverItem
94+
className='group'
95+
disabled={disableEdit}
96+
onClick={() => {
97+
onAutoLayout()
98+
onClose()
99+
}}
100+
>
101+
<span>Auto-layout</span>
102+
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⇧L</span>
103+
</PopoverItem>
104+
105+
{/* Open Logs */}
106+
<PopoverItem
107+
className='group'
108+
onClick={() => {
109+
onOpenLogs()
110+
onClose()
111+
}}
112+
>
113+
<span>Open Logs</span>
114+
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘L</span>
115+
</PopoverItem>
116+
117+
{/* Open Variables */}
118+
<PopoverItem
119+
onClick={() => {
120+
onOpenVariables()
121+
onClose()
122+
}}
123+
>
124+
Variables
125+
</PopoverItem>
126+
127+
{/* Open Chat */}
128+
<PopoverItem
129+
onClick={() => {
130+
onOpenChat()
131+
onClose()
132+
}}
133+
>
134+
Open Chat
135+
</PopoverItem>
136+
137+
{/* Invite to Workspace - admin only */}
138+
<PopoverItem
139+
disabled={disableAdmin}
140+
onClick={() => {
141+
onInvite()
142+
onClose()
143+
}}
144+
>
145+
Invite to Workspace
146+
</PopoverItem>
147+
</PopoverContent>
148+
</Popover>
149+
)
150+
}

0 commit comments

Comments
 (0)