Skip to content

Commit 304c50c

Browse files
committed
add cut
1 parent aa864d5 commit 304c50c

4 files changed

Lines changed: 106 additions & 39 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface BlockMenuProps {
3535
onClose: () => void
3636
selectedBlocks: BlockInfo[]
3737
onCopy: () => void
38+
onCut: () => void
3839
onPaste: () => void
3940
onDuplicate: () => void
4041
onDelete: () => void
@@ -74,6 +75,7 @@ export function BlockMenu({
7475
onClose,
7576
selectedBlocks,
7677
onCopy,
78+
onCut,
7779
onPaste,
7880
onDuplicate,
7981
onDelete,
@@ -162,6 +164,17 @@ export function BlockMenu({
162164
<span>Copy</span>
163165
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘C</span>
164166
</PopoverItem>
167+
<PopoverItem
168+
className='group'
169+
disabled={disableEdit}
170+
onClick={() => {
171+
onCut()
172+
onClose()
173+
}}
174+
>
175+
<span>Cut</span>
176+
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘X</span>
177+
</PopoverItem>
165178
<PopoverItem
166179
className='group'
167180
disabled={!userCanEdit || !hasClipboard}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface CanvasMenuProps {
2424
onAutoLayout: () => void
2525
onFitToView: () => void
2626
onOpenLogs: () => void
27+
onOpenSearchReplace: () => void
2728
onToggleVariables: () => void
2829
onToggleChat: () => void
2930
onToggleWorkflowLock?: () => void
@@ -59,6 +60,7 @@ export function CanvasMenu({
5960
onAutoLayout,
6061
onFitToView,
6162
onOpenLogs,
63+
onOpenSearchReplace,
6264
onToggleVariables,
6365
onToggleChat,
6466
onToggleWorkflowLock,
@@ -114,9 +116,6 @@ export function CanvasMenu({
114116
<span>Redo</span>
115117
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘⇧Z</span>
116118
</PopoverItem>
117-
118-
{/* Edit and creation actions */}
119-
<PopoverDivider />
120119
<PopoverItem
121120
className='group'
122121
disabled={disableEdit || !hasClipboard}
@@ -128,6 +127,9 @@ export function CanvasMenu({
128127
<span>Paste</span>
129128
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘V</span>
130129
</PopoverItem>
130+
131+
{/* Edit and creation actions */}
132+
<PopoverDivider />
131133
<PopoverItem
132134
className='group'
133135
disabled={disableEdit}
@@ -173,6 +175,16 @@ export function CanvasMenu({
173175

174176
{/* Navigation actions */}
175177
<PopoverDivider />
178+
<PopoverItem
179+
className='group'
180+
onClick={() => {
181+
onOpenSearchReplace()
182+
onClose()
183+
}}
184+
>
185+
<span>Search and replace</span>
186+
<span className='ml-auto opacity-70 group-hover:opacity-100'>⌘F</span>
187+
</PopoverItem>
176188
<PopoverItem
177189
className='group'
178190
onClick={() => {

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { toError } from '@sim/utils/errors'
66
import { useQueryClient } from '@tanstack/react-query'
7-
import { History, Plus, Search } from 'lucide-react'
7+
import { History, Plus } from 'lucide-react'
88
import { useParams, useRouter } from 'next/navigation'
99
import { usePostHog } from 'posthog-js/react'
1010
import { useShallow } from 'zustand/react/shallow'
@@ -86,7 +86,6 @@ import { useVariablesModalStore } from '@/stores/variables/modal'
8686
import { useVariablesStore } from '@/stores/variables/store'
8787
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
8888
import { captureBaselineSnapshot } from '@/stores/workflow-diff/utils'
89-
import { useWorkflowSearchReplaceStore } from '@/stores/workflow-search-replace/store'
9089
import { getWorkflowWithValues } from '@/stores/workflows'
9190
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
9291
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -609,8 +608,6 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
609608
const hasValidationErrors = false // TODO: Add validation logic if needed
610609
const isWorkflowBlocked = isExecuting || hasValidationErrors
611610
const isButtonDisabled = !isExecuting && (isWorkflowBlocked || (!canRun && !isLoadingPermissions))
612-
const openWorkflowSearchReplace = useWorkflowSearchReplaceStore((state) => state.open)
613-
614611
/**
615612
* Register global keyboard shortcuts using the central commands registry.
616613
*
@@ -678,10 +675,6 @@ export const Panel = memo(function Panel({ workspaceId: propWorkspaceId }: Panel
678675
<VariableIcon />
679676
Variables
680677
</DropdownMenuItem>
681-
<DropdownMenuItem onSelect={openWorkflowSearchReplace}>
682-
<Search />
683-
Search and replace
684-
</DropdownMenuItem>
685678
{userPermissions.canAdmin && !isSnapshotView && (
686679
<DropdownMenuItem
687680
onSelect={handleToggleWorkflowLock}

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

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import { usePanelEditorStore } from '@/stores/panel'
9898
import { useUndoRedoStore } from '@/stores/undo-redo'
9999
import { useVariablesModalStore } from '@/stores/variables/modal'
100100
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
101+
import { useWorkflowSearchReplaceStore } from '@/stores/workflow-search-replace/store'
101102
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
102103
import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils'
103104
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -965,6 +966,56 @@ const WorkflowContent = React.memo(
965966
copyBlocks(blockIds)
966967
}, [contextMenuBlocks, copyBlocks])
967968

969+
const notifyProtectedBlockRemoval = useCallback(
970+
(protectedIds: string[], allProtected: boolean) => {
971+
if (protectedIds.length === 0) return false
972+
973+
if (allProtected) {
974+
addNotification({
975+
level: 'info',
976+
message: 'Cannot delete locked blocks or blocks inside locked containers',
977+
workflowId: activeWorkflowId || undefined,
978+
})
979+
return true
980+
}
981+
982+
addNotification({
983+
level: 'info',
984+
message: `Skipped ${protectedIds.length} protected block(s)`,
985+
workflowId: activeWorkflowId || undefined,
986+
})
987+
return false
988+
},
989+
[activeWorkflowId, addNotification]
990+
)
991+
992+
const removeBlocksWithProtection = useCallback(
993+
(blockIds: string[]) => {
994+
const { deletableIds, protectedIds, allProtected } = filterProtectedBlocks(blockIds, blocks)
995+
if (notifyProtectedBlockRemoval(protectedIds, allProtected)) return []
996+
997+
if (deletableIds.length > 0) {
998+
collaborativeBatchRemoveBlocks(deletableIds)
999+
}
1000+
1001+
return deletableIds
1002+
},
1003+
[blocks, collaborativeBatchRemoveBlocks, notifyProtectedBlockRemoval]
1004+
)
1005+
1006+
const cutBlocksWithProtection = useCallback(
1007+
(blockIds: string[]) => {
1008+
const { deletableIds, protectedIds, allProtected } = filterProtectedBlocks(blockIds, blocks)
1009+
if (notifyProtectedBlockRemoval(protectedIds, allProtected)) return
1010+
1011+
if (deletableIds.length > 0) {
1012+
copyBlocks(deletableIds)
1013+
collaborativeBatchRemoveBlocks(deletableIds)
1014+
}
1015+
},
1016+
[blocks, collaborativeBatchRemoveBlocks, copyBlocks, notifyProtectedBlockRemoval]
1017+
)
1018+
9681019
/**
9691020
* Executes a paste operation with validation and selection handling.
9701021
* Consolidates shared logic for context paste, duplicate, and keyboard paste.
@@ -1165,35 +1216,13 @@ const WorkflowContent = React.memo(
11651216
executePasteOperation('duplicate', DEFAULT_PASTE_OFFSET)
11661217
}, [contextMenuBlocks, copyBlocks, executePasteOperation])
11671218

1168-
const handleContextDelete = useCallback(() => {
1169-
const blockIds = contextMenuBlocks.map((b) => b.id)
1170-
const { deletableIds, protectedIds, allProtected } = filterProtectedBlocks(blockIds, blocks)
1219+
const handleContextCut = useCallback(() => {
1220+
cutBlocksWithProtection(contextMenuBlocks.map((b) => b.id))
1221+
}, [contextMenuBlocks, cutBlocksWithProtection])
11711222

1172-
if (protectedIds.length > 0) {
1173-
if (allProtected) {
1174-
addNotification({
1175-
level: 'info',
1176-
message: 'Cannot delete locked blocks or blocks inside locked containers',
1177-
workflowId: activeWorkflowId || undefined,
1178-
})
1179-
return
1180-
}
1181-
addNotification({
1182-
level: 'info',
1183-
message: `Skipped ${protectedIds.length} protected block(s)`,
1184-
workflowId: activeWorkflowId || undefined,
1185-
})
1186-
}
1187-
if (deletableIds.length > 0) {
1188-
collaborativeBatchRemoveBlocks(deletableIds)
1189-
}
1190-
}, [
1191-
contextMenuBlocks,
1192-
collaborativeBatchRemoveBlocks,
1193-
addNotification,
1194-
activeWorkflowId,
1195-
blocks,
1196-
])
1223+
const handleContextDelete = useCallback(() => {
1224+
removeBlocksWithProtection(contextMenuBlocks.map((b) => b.id))
1225+
}, [contextMenuBlocks, removeBlocksWithProtection])
11971226

11981227
const handleContextToggleEnabled = useCallback(() => {
11991228
const blockIds = contextMenuBlocks.map((block) => block.id)
@@ -1416,6 +1445,10 @@ const WorkflowContent = React.memo(
14161445
router.push(`/workspace/${workspaceId}/logs?workflowIds=${workflowIdParam}`)
14171446
}, [router, workspaceId, workflowIdParam])
14181447

1448+
const handleContextOpenSearchReplace = useCallback(() => {
1449+
useWorkflowSearchReplaceStore.getState().open()
1450+
}, [])
1451+
14191452
const handleContextToggleVariables = useCallback(() => {
14201453
const { isOpen, setIsOpen } = useVariablesModalStore.getState()
14211454
setIsOpen(!isOpen)
@@ -1467,6 +1500,19 @@ const WorkflowContent = React.memo(
14671500
copyBlocks([currentBlockId])
14681501
}
14691502
}
1503+
} else if ((event.ctrlKey || event.metaKey) && event.key === 'x') {
1504+
const selection = window.getSelection()
1505+
const hasTextSelection = selection && selection.toString().length > 0
1506+
1507+
if (hasTextSelection || !effectivePermissions.canEdit) {
1508+
return
1509+
}
1510+
1511+
const selectedNodes = getNodes().filter((node) => node.selected)
1512+
if (selectedNodes.length > 0) {
1513+
event.preventDefault()
1514+
cutBlocksWithProtection(selectedNodes.map((node) => node.id))
1515+
}
14701516
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
14711517
if (effectivePermissions.canEdit && hasClipboard()) {
14721518
event.preventDefault()
@@ -1487,6 +1533,7 @@ const WorkflowContent = React.memo(
14871533
redo,
14881534
getNodes,
14891535
copyBlocks,
1536+
cutBlocksWithProtection,
14901537
hasClipboard,
14911538
effectivePermissions.canEdit,
14921539
clipboard,
@@ -4172,6 +4219,7 @@ const WorkflowContent = React.memo(
41724219
onClose={closeContextMenu}
41734220
selectedBlocks={contextMenuBlocks}
41744221
onCopy={handleContextCopy}
4222+
onCut={handleContextCut}
41754223
onPaste={handleContextPaste}
41764224
onDuplicate={handleContextDuplicate}
41774225
onDelete={handleContextDelete}
@@ -4214,6 +4262,7 @@ const WorkflowContent = React.memo(
42144262
onAutoLayout={handleAutoLayout}
42154263
onFitToView={() => fitViewToBounds({ padding: 0.1, duration: 300 })}
42164264
onOpenLogs={handleContextOpenLogs}
4265+
onOpenSearchReplace={handleContextOpenSearchReplace}
42174266
onToggleVariables={handleContextToggleVariables}
42184267
onToggleChat={handleContextToggleChat}
42194268
isVariablesOpen={isVariablesOpen}

0 commit comments

Comments
 (0)