Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,7 @@ export function Chat() {
onClick={() => document.getElementById('floating-chat-file-input')?.click()}
title='Attach file'
className={cn(
'!bg-transparent cursor-pointer rounded-[6px] p-[0px]',
'!bg-transparent !border-0 cursor-pointer rounded-[6px] p-[0px]',
(!activeWorkflowId || isExecuting || chatFiles.length >= 15) &&
'cursor-not-allowed opacity-50'
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
'use client'

import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
import type { BlockContextMenuProps } from './types'

/**
* Context menu for workflow block(s).
* Displays block-specific actions in a popover at right-click position.
* Supports multi-selection - actions apply to all selected blocks.
*/
export function BlockContextMenu({
isOpen,
position,
menuRef,
onClose,
selectedBlocks,
onCopy,
onPaste,
onDuplicate,
onDelete,
onToggleEnabled,
onToggleHandles,
onRemoveFromSubflow,
onOpenEditor,
onRename,
hasClipboard = false,
showRemoveFromSubflow = false,
disableEdit = false,
}: BlockContextMenuProps) {
const isSingleBlock = selectedBlocks.length === 1

const allEnabled = selectedBlocks.every((b) => b.enabled)
const allDisabled = selectedBlocks.every((b) => !b.enabled)

const hasStarterBlock = selectedBlocks.some(
(b) => b.type === 'starter' || b.type === 'start_trigger'
)
const allNoteBlocks = selectedBlocks.every((b) => b.type === 'note')
const isSubflow =
isSingleBlock && (selectedBlocks[0]?.type === 'loop' || selectedBlocks[0]?.type === 'parallel')

const canRemoveFromSubflow = showRemoveFromSubflow && !hasStarterBlock

const getToggleEnabledLabel = () => {
if (allEnabled) return 'Disable'
if (allDisabled) return 'Enable'
return 'Toggle Enabled'
}

return (
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
<PopoverAnchor
style={{
position: 'fixed',
left: `${position.x}px`,
top: `${position.y}px`,
width: '1px',
height: '1px',
}}
/>
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
{/* Copy */}
<PopoverItem
className='group'
onClick={() => {
onCopy()
onClose()
}}
>
<span>Copy</span>
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘C</span>
</PopoverItem>

{/* Paste */}
<PopoverItem
className='group'
disabled={disableEdit || !hasClipboard}
onClick={() => {
onPaste()
onClose()
}}
>
<span>Paste</span>
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘V</span>
</PopoverItem>

{/* Duplicate - hide for starter blocks */}
{!hasStarterBlock && (
<PopoverItem
disabled={disableEdit}
onClick={() => {
onDuplicate()
onClose()
}}
>
Duplicate
</PopoverItem>
)}

{/* Delete */}
<PopoverItem
className='group'
disabled={disableEdit}
onClick={() => {
onDelete()
onClose()
}}
>
<span>Delete</span>
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌫</span>
</PopoverItem>

{/* Enable/Disable - hide if all blocks are notes */}
{!allNoteBlocks && (
<PopoverItem
disabled={disableEdit}
onClick={() => {
onToggleEnabled()
onClose()
}}
>
{getToggleEnabledLabel()}
</PopoverItem>
)}

{/* Flip Handles - hide if all blocks are notes */}
{!allNoteBlocks && (
<PopoverItem
disabled={disableEdit}
onClick={() => {
onToggleHandles()
onClose()
}}
>
Flip Handles
</PopoverItem>
)}

{/* Remove from Subflow - only show when applicable */}
{canRemoveFromSubflow && (
<PopoverItem
disabled={disableEdit}
onClick={() => {
onRemoveFromSubflow()
onClose()
}}
>
Remove from Subflow
</PopoverItem>
)}

{/* Rename - only for single block, not subflows */}
{isSingleBlock && !isSubflow && (
<PopoverItem
disabled={disableEdit}
onClick={() => {
onRename()
onClose()
}}
>
Rename
</PopoverItem>
)}

{/* Open Editor - only for single block */}
{isSingleBlock && (
<PopoverItem
onClick={() => {
onOpenEditor()
onClose()
}}
>
Open Editor
</PopoverItem>
)}
</PopoverContent>
</Popover>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export { BlockContextMenu } from './block-context-menu'
export { PaneContextMenu } from './pane-context-menu'
export type {
BlockContextMenuProps,
ContextMenuBlockInfo,
ContextMenuPosition,
PaneContextMenuProps,
} from './types'
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use client'

import { Popover, PopoverAnchor, PopoverContent, PopoverItem } from '@/components/emcn'
import type { PaneContextMenuProps } from './types'

/**
* Context menu for workflow canvas pane.
* Displays canvas-level actions when right-clicking empty space.
*/
export function PaneContextMenu({
isOpen,
position,
menuRef,
onClose,
onUndo,
onRedo,
onPaste,
onAddBlock,
onAutoLayout,
onOpenLogs,
onOpenVariables,
onOpenChat,
onInvite,
hasClipboard = false,
disableEdit = false,
disableAdmin = false,
}: PaneContextMenuProps) {
return (
<Popover open={isOpen} onOpenChange={onClose} variant='secondary' size='sm'>
<PopoverAnchor
style={{
position: 'fixed',
left: `${position.x}px`,
top: `${position.y}px`,
width: '1px',
height: '1px',
}}
/>
<PopoverContent ref={menuRef} align='start' side='bottom' sideOffset={4}>
{/* Undo */}
<PopoverItem
className='group'
disabled={disableEdit}
onClick={() => {
onUndo()
onClose()
}}
>
<span>Undo</span>
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘Z</span>
</PopoverItem>

{/* Redo */}
<PopoverItem
className='group'
disabled={disableEdit}
onClick={() => {
onRedo()
onClose()
}}
>
<span>Redo</span>
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘⇧Z</span>
</PopoverItem>

{/* Paste */}
<PopoverItem
className='group'
disabled={disableEdit || !hasClipboard}
onClick={() => {
onPaste()
onClose()
}}
>
<span>Paste</span>
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘V</span>
</PopoverItem>

{/* Add Block */}
<PopoverItem
className='group'
disabled={disableEdit}
onClick={() => {
onAddBlock()
onClose()
}}
>
<span>Add Block</span>
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘K</span>
</PopoverItem>

{/* Auto-layout */}
<PopoverItem
className='group'
disabled={disableEdit}
onClick={() => {
onAutoLayout()
onClose()
}}
>
<span>Auto-layout</span>
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⇧L</span>
</PopoverItem>

{/* Open Logs */}
<PopoverItem
className='group'
onClick={() => {
onOpenLogs()
onClose()
}}
>
<span>Open Logs</span>
<span className='ml-auto text-[var(--text-tertiary)] group-hover:text-inherit'>⌘L</span>
</PopoverItem>

{/* Open Variables */}
<PopoverItem
onClick={() => {
onOpenVariables()
onClose()
}}
>
Variables
</PopoverItem>

{/* Open Chat */}
<PopoverItem
onClick={() => {
onOpenChat()
onClose()
}}
>
Open Chat
</PopoverItem>

{/* Invite to Workspace - admin only */}
<PopoverItem
disabled={disableAdmin}
onClick={() => {
onInvite()
onClose()
}}
>
Invite to Workspace
</PopoverItem>
</PopoverContent>
</Popover>
)
}
Loading