Skip to content

Commit bf5d0a5

Browse files
feat(copy-paste): allow cross workflow selection, paste, move for blocks (#2649)
* feat(copy-paste): allow cross workflow selection, paste, move for blocks * fix drag options * add keyboard and mouse controls into docs * refactor sockets and undo/redo for batch additions and removals * fix tests * cleanup more code * fix perms issue * fix subflow copy/paste * remove log file * fit paste in viewport bounds * fix deselection
1 parent fb148c6 commit bf5d0a5

File tree

30 files changed

+2089
-2022
lines changed

30 files changed

+2089
-2022
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
title: Keyboard Shortcuts
3+
description: Master the workflow canvas with keyboard shortcuts and mouse controls
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout'
7+
8+
Speed up your workflow building with these keyboard shortcuts and mouse controls. All shortcuts work when the canvas is focused (not when typing in an input field).
9+
10+
<Callout type="info">
11+
**Mod** refers to `Cmd` on macOS and `Ctrl` on Windows/Linux.
12+
</Callout>
13+
14+
## Canvas Controls
15+
16+
### Mouse Controls
17+
18+
| Action | Control |
19+
|--------|---------|
20+
| Pan/move canvas | Left-drag on empty space |
21+
| Pan/move canvas | Scroll or trackpad |
22+
| Select multiple blocks | Right-drag to draw selection box |
23+
| Drag block | Left-drag on block header |
24+
| Add to selection | `Mod` + click on blocks |
25+
26+
### Workflow Actions
27+
28+
| Shortcut | Action |
29+
|----------|--------|
30+
| `Mod` + `Enter` | Run workflow (or cancel if running) |
31+
| `Mod` + `Z` | Undo |
32+
| `Mod` + `Shift` + `Z` | Redo |
33+
| `Mod` + `C` | Copy selected blocks |
34+
| `Mod` + `V` | Paste blocks |
35+
| `Delete` or `Backspace` | Delete selected blocks or edges |
36+
| `Shift` + `L` | Auto-layout canvas |
37+
38+
## Panel Navigation
39+
40+
These shortcuts switch between panel tabs on the right side of the canvas.
41+
42+
| Shortcut | Action |
43+
|----------|--------|
44+
| `C` | Focus Copilot tab |
45+
| `T` | Focus Toolbar tab |
46+
| `E` | Focus Editor tab |
47+
| `Mod` + `F` | Focus Toolbar search |
48+
49+
## Global Navigation
50+
51+
| Shortcut | Action |
52+
|----------|--------|
53+
| `Mod` + `K` | Open search |
54+
| `Mod` + `Shift` + `A` | Add new agent workflow |
55+
| `Mod` + `Y` | Go to templates |
56+
| `Mod` + `L` | Go to logs |
57+
58+
## Utility
59+
60+
| Shortcut | Action |
61+
|----------|--------|
62+
| `Mod` + `D` | Clear terminal console |
63+
| `Mod` + `E` | Clear notifications |
64+

apps/docs/content/docs/en/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"execution",
1515
"permissions",
1616
"sdks",
17-
"self-hosting"
17+
"self-hosting",
18+
"./keyboard-shortcuts/index"
1819
],
1920
"defaultOpen": false
2021
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export interface SubflowNodeData {
6161
*/
6262
export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeData>) => {
6363
const { getNodes } = useReactFlow()
64-
const { collaborativeRemoveBlock } = useCollaborativeWorkflow()
64+
const { collaborativeBatchRemoveBlocks } = useCollaborativeWorkflow()
6565
const blockRef = useRef<HTMLDivElement>(null)
6666

6767
const currentWorkflow = useCurrentWorkflow()
@@ -184,7 +184,7 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeDat
184184
variant='ghost'
185185
onClick={(e) => {
186186
e.stopPropagation()
187-
collaborativeRemoveBlock(id)
187+
collaborativeBatchRemoveBlocks([id])
188188
}}
189189
className='h-[14px] w-[14px] p-0 opacity-0 transition-opacity duration-100 group-hover:opacity-100'
190190
>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ import { Button, Copy, Tooltip, Trash2 } from '@/components/emcn'
44
import { cn } from '@/lib/core/utils/cn'
55
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
66
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
7+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
8+
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
9+
import { getUniqueBlockName, prepareDuplicateBlockState } from '@/stores/workflows/utils'
710
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
811

12+
const DEFAULT_DUPLICATE_OFFSET = { x: 50, y: 50 }
13+
914
/**
1015
* Props for the ActionBar component
1116
*/
@@ -27,11 +32,39 @@ interface ActionBarProps {
2732
export const ActionBar = memo(
2833
function ActionBar({ blockId, blockType, disabled = false }: ActionBarProps) {
2934
const {
30-
collaborativeRemoveBlock,
35+
collaborativeBatchAddBlocks,
36+
collaborativeBatchRemoveBlocks,
3137
collaborativeToggleBlockEnabled,
32-
collaborativeDuplicateBlock,
3338
collaborativeToggleBlockHandles,
3439
} = useCollaborativeWorkflow()
40+
const { activeWorkflowId } = useWorkflowRegistry()
41+
const blocks = useWorkflowStore((state) => state.blocks)
42+
const subBlockStore = useSubBlockStore()
43+
44+
const handleDuplicateBlock = useCallback(() => {
45+
const sourceBlock = blocks[blockId]
46+
if (!sourceBlock) return
47+
48+
const newId = crypto.randomUUID()
49+
const newName = getUniqueBlockName(sourceBlock.name, blocks)
50+
const subBlockValues = subBlockStore.workflowValues[activeWorkflowId || '']?.[blockId] || {}
51+
52+
const { block, subBlockValues: filteredValues } = prepareDuplicateBlockState({
53+
sourceBlock,
54+
newId,
55+
newName,
56+
positionOffset: DEFAULT_DUPLICATE_OFFSET,
57+
subBlockValues,
58+
})
59+
60+
collaborativeBatchAddBlocks([block], [], {}, {}, { [newId]: filteredValues })
61+
}, [
62+
blockId,
63+
blocks,
64+
activeWorkflowId,
65+
subBlockStore.workflowValues,
66+
collaborativeBatchAddBlocks,
67+
])
3568

3669
/**
3770
* Optimized single store subscription for all block data
@@ -115,7 +148,7 @@ export const ActionBar = memo(
115148
onClick={(e) => {
116149
e.stopPropagation()
117150
if (!disabled) {
118-
collaborativeDuplicateBlock(blockId)
151+
handleDuplicateBlock()
119152
}
120153
}}
121154
className='hover:!text-[var(--text-inverse)] h-[23px] w-[23px] rounded-[8px] bg-[var(--surface-7)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]'
@@ -185,7 +218,7 @@ export const ActionBar = memo(
185218
onClick={(e) => {
186219
e.stopPropagation()
187220
if (!disabled) {
188-
collaborativeRemoveBlock(blockId)
221+
collaborativeBatchRemoveBlocks([blockId])
189222
}
190223
}}
191224
className='hover:!text-[var(--text-inverse)] h-[23px] w-[23px] rounded-[8px] bg-[var(--surface-7)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]'

0 commit comments

Comments
 (0)