Skip to content

Commit d533ea2

Browse files
committed
feat(canvas): added the ability to lock blocks
1 parent 6cb3977 commit d533ea2

File tree

39 files changed

+11681
-96
lines changed

39 files changed

+11681
-96
lines changed

apps/docs/content/docs/en/tools/pulse.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
1111
/>
1212

1313
{/* MANUAL-CONTENT-START:intro */}
14-
The [Pulse](https://www.pulseapi.com/) tool enables seamless extraction of text and structured content from a wide variety of documents—including PDFs, images, and Office files—using state-of-the-art OCR (Optical Character Recognition) powered by Pulse. Designed for automated agentic workflows, Pulse Parser makes it easy to unlock valuable information trapped in unstructured documents and integrate the extracted content directly into your workflow.
14+
The [Pulse](https://www.runpulse.com) tool enables seamless extraction of text and structured content from a wide variety of documents—including PDFs, images, and Office files—using state-of-the-art OCR (Optical Character Recognition) powered by Pulse. Designed for automated agentic workflows, Pulse Parser makes it easy to unlock valuable information trapped in unstructured documents and integrate the extracted content directly into your workflow.
1515

1616
With Pulse, you can:
1717

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

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { memo, useCallback } from 'react'
2-
import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, LogOut } from 'lucide-react'
2+
import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, Lock, LogOut, Unlock } from 'lucide-react'
33
import { Button, Copy, PlayOutline, Tooltip, Trash2 } from '@/components/emcn'
44
import { cn } from '@/lib/core/utils/cn'
55
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
@@ -49,6 +49,7 @@ export const ActionBar = memo(
4949
collaborativeBatchRemoveBlocks,
5050
collaborativeBatchToggleBlockEnabled,
5151
collaborativeBatchToggleBlockHandles,
52+
collaborativeBatchToggleLocked,
5253
} = useCollaborativeWorkflow()
5354
const { setPendingSelection } = useWorkflowRegistry()
5455
const { handleRunFromBlock } = useWorkflowExecution()
@@ -84,21 +85,25 @@ export const ActionBar = memo(
8485
)
8586
}, [blockId, addNotification, collaborativeBatchAddBlocks, setPendingSelection])
8687

87-
const { isEnabled, horizontalHandles, parentId, parentType } = useWorkflowStore(
88-
useCallback(
89-
(state) => {
90-
const block = state.blocks[blockId]
91-
const parentId = block?.data?.parentId
92-
return {
93-
isEnabled: block?.enabled ?? true,
94-
horizontalHandles: block?.horizontalHandles ?? false,
95-
parentId,
96-
parentType: parentId ? state.blocks[parentId]?.type : undefined,
97-
}
98-
},
99-
[blockId]
88+
const { isEnabled, horizontalHandles, parentId, parentType, isLocked, isParentLocked } =
89+
useWorkflowStore(
90+
useCallback(
91+
(state) => {
92+
const block = state.blocks[blockId]
93+
const parentId = block?.data?.parentId
94+
const parentBlock = parentId ? state.blocks[parentId] : undefined
95+
return {
96+
isEnabled: block?.enabled ?? true,
97+
horizontalHandles: block?.horizontalHandles ?? false,
98+
parentId,
99+
parentType: parentBlock?.type,
100+
isLocked: block?.locked ?? false,
101+
isParentLocked: parentBlock?.locked ?? false,
102+
}
103+
},
104+
[blockId]
105+
)
100106
)
101-
)
102107

103108
const { activeWorkflowId } = useWorkflowRegistry()
104109
const { isExecuting, getLastExecutionSnapshot } = useExecutionStore()
@@ -161,25 +166,27 @@ export const ActionBar = memo(
161166
{!isNoteBlock && !isInsideSubflow && (
162167
<Tooltip.Root>
163168
<Tooltip.Trigger asChild>
164-
<Button
165-
variant='ghost'
166-
onClick={(e) => {
167-
e.stopPropagation()
168-
if (canRunFromBlock && !disabled) {
169-
handleRunFromBlockClick()
170-
}
171-
}}
172-
className={ACTION_BUTTON_STYLES}
173-
disabled={disabled || !canRunFromBlock}
174-
>
175-
<PlayOutline className={ICON_SIZE} />
176-
</Button>
169+
<span className='inline-flex'>
170+
<Button
171+
variant='ghost'
172+
onClick={(e) => {
173+
e.stopPropagation()
174+
if (canRunFromBlock && !disabled) {
175+
handleRunFromBlockClick()
176+
}
177+
}}
178+
className={ACTION_BUTTON_STYLES}
179+
disabled={disabled || !canRunFromBlock}
180+
>
181+
<PlayOutline className={ICON_SIZE} />
182+
</Button>
183+
</span>
177184
</Tooltip.Trigger>
178185
<Tooltip.Content side='top'>
179186
{(() => {
180187
if (disabled) return getTooltipMessage('Run from block')
181188
if (isExecuting) return 'Execution in progress'
182-
if (!dependenciesSatisfied) return 'Run upstream blocks first'
189+
if (!dependenciesSatisfied) return 'Run previous blocks first'
183190
return 'Run from block'
184191
})()}
185192
</Tooltip.Content>
@@ -193,22 +200,42 @@ export const ActionBar = memo(
193200
variant='ghost'
194201
onClick={(e) => {
195202
e.stopPropagation()
196-
if (!disabled) {
203+
if (!disabled && !isLocked) {
197204
collaborativeBatchToggleBlockEnabled([blockId])
198205
}
199206
}}
200207
className={ACTION_BUTTON_STYLES}
201-
disabled={disabled}
208+
disabled={disabled || isLocked}
202209
>
203210
{isEnabled ? <Circle className={ICON_SIZE} /> : <CircleOff className={ICON_SIZE} />}
204211
</Button>
205212
</Tooltip.Trigger>
206213
<Tooltip.Content side='top'>
207-
{getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')}
214+
{isLocked
215+
? 'Block is locked'
216+
: getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')}
208217
</Tooltip.Content>
209218
</Tooltip.Root>
210219
)}
211220

221+
{userPermissions.canAdmin && (
222+
<Tooltip.Root>
223+
<Tooltip.Trigger asChild>
224+
<Button
225+
variant='ghost'
226+
onClick={(e) => {
227+
e.stopPropagation()
228+
collaborativeBatchToggleLocked([blockId])
229+
}}
230+
className={ACTION_BUTTON_STYLES}
231+
>
232+
{isLocked ? <Unlock className={ICON_SIZE} /> : <Lock className={ICON_SIZE} />}
233+
</Button>
234+
</Tooltip.Trigger>
235+
<Tooltip.Content side='top'>{isLocked ? 'Unlock Block' : 'Lock Block'}</Tooltip.Content>
236+
</Tooltip.Root>
237+
)}
238+
212239
{!isStartBlock && !isResponseBlock && (
213240
<Tooltip.Root>
214241
<Tooltip.Trigger asChild>
@@ -237,12 +264,12 @@ export const ActionBar = memo(
237264
variant='ghost'
238265
onClick={(e) => {
239266
e.stopPropagation()
240-
if (!disabled) {
267+
if (!disabled && !isLocked) {
241268
collaborativeBatchToggleBlockHandles([blockId])
242269
}
243270
}}
244271
className={ACTION_BUTTON_STYLES}
245-
disabled={disabled}
272+
disabled={disabled || isLocked}
246273
>
247274
{horizontalHandles ? (
248275
<ArrowLeftRight className={ICON_SIZE} />
@@ -252,7 +279,9 @@ export const ActionBar = memo(
252279
</Button>
253280
</Tooltip.Trigger>
254281
<Tooltip.Content side='top'>
255-
{getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')}
282+
{isLocked
283+
? 'Block is locked'
284+
: getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')}
256285
</Tooltip.Content>
257286
</Tooltip.Root>
258287
)}
@@ -264,19 +293,23 @@ export const ActionBar = memo(
264293
variant='ghost'
265294
onClick={(e) => {
266295
e.stopPropagation()
267-
if (!disabled && userPermissions.canEdit) {
296+
if (!disabled && userPermissions.canEdit && !isLocked && !isParentLocked) {
268297
window.dispatchEvent(
269298
new CustomEvent('remove-from-subflow', { detail: { blockIds: [blockId] } })
270299
)
271300
}
272301
}}
273302
className={ACTION_BUTTON_STYLES}
274-
disabled={disabled || !userPermissions.canEdit}
303+
disabled={disabled || !userPermissions.canEdit || isLocked || isParentLocked}
275304
>
276305
<LogOut className={ICON_SIZE} />
277306
</Button>
278307
</Tooltip.Trigger>
279-
<Tooltip.Content side='top'>{getTooltipMessage('Remove from Subflow')}</Tooltip.Content>
308+
<Tooltip.Content side='top'>
309+
{isLocked || isParentLocked
310+
? 'Block is locked'
311+
: getTooltipMessage('Remove from Subflow')}
312+
</Tooltip.Content>
280313
</Tooltip.Root>
281314
)}
282315

@@ -286,17 +319,19 @@ export const ActionBar = memo(
286319
variant='ghost'
287320
onClick={(e) => {
288321
e.stopPropagation()
289-
if (!disabled) {
322+
if (!disabled && !isLocked) {
290323
collaborativeBatchRemoveBlocks([blockId])
291324
}
292325
}}
293326
className={ACTION_BUTTON_STYLES}
294-
disabled={disabled}
327+
disabled={disabled || isLocked}
295328
>
296329
<Trash2 className={ICON_SIZE} />
297330
</Button>
298331
</Tooltip.Trigger>
299-
<Tooltip.Content side='top'>{getTooltipMessage('Delete Block')}</Tooltip.Content>
332+
<Tooltip.Content side='top'>
333+
{isLocked ? 'Block is locked' : getTooltipMessage('Delete Block')}
334+
</Tooltip.Content>
300335
</Tooltip.Root>
301336
</div>
302337
)

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface BlockInfo {
2020
horizontalHandles: boolean
2121
parentId?: string
2222
parentType?: string
23+
locked?: boolean
2324
}
2425

2526
/**
@@ -46,10 +47,17 @@ export interface BlockMenuProps {
4647
showRemoveFromSubflow?: boolean
4748
/** Whether run from block is available (has snapshot, was executed, not inside subflow) */
4849
canRunFromBlock?: boolean
50+
/** Whether to disable edit actions (user can't edit OR blocks are locked) */
4951
disableEdit?: boolean
52+
/** Whether the user has edit permission (ignoring locked state) */
53+
userCanEdit?: boolean
5054
isExecuting?: boolean
5155
/** Whether the selected block is a trigger (has no incoming edges) */
5256
isPositionalTrigger?: boolean
57+
/** Callback to toggle locked state of selected blocks */
58+
onToggleLocked?: () => void
59+
/** Whether the user has admin permissions */
60+
canAdmin?: boolean
5361
}
5462

5563
/**
@@ -78,13 +86,18 @@ export function BlockMenu({
7886
showRemoveFromSubflow = false,
7987
canRunFromBlock = false,
8088
disableEdit = false,
89+
userCanEdit = true,
8190
isExecuting = false,
8291
isPositionalTrigger = false,
92+
onToggleLocked,
93+
canAdmin = false,
8394
}: BlockMenuProps) {
8495
const isSingleBlock = selectedBlocks.length === 1
8596

8697
const allEnabled = selectedBlocks.every((b) => b.enabled)
8798
const allDisabled = selectedBlocks.every((b) => !b.enabled)
99+
const allLocked = selectedBlocks.every((b) => b.locked)
100+
const allUnlocked = selectedBlocks.every((b) => !b.locked)
88101

89102
const hasSingletonBlock = selectedBlocks.some(
90103
(b) =>
@@ -108,6 +121,12 @@ export function BlockMenu({
108121
return 'Toggle Enabled'
109122
}
110123

124+
const getToggleLockedLabel = () => {
125+
if (allLocked) return 'Unlock'
126+
if (allUnlocked) return 'Lock'
127+
return 'Toggle Lock'
128+
}
129+
111130
return (
112131
<Popover
113132
open={isOpen}
@@ -150,7 +169,7 @@ export function BlockMenu({
150169
</PopoverItem>
151170
{!hasSingletonBlock && (
152171
<PopoverItem
153-
disabled={disableEdit}
172+
disabled={!userCanEdit}
154173
onClick={() => {
155174
onDuplicate()
156175
onClose()
@@ -195,6 +214,16 @@ export function BlockMenu({
195214
Remove from Subflow
196215
</PopoverItem>
197216
)}
217+
{canAdmin && onToggleLocked && (
218+
<PopoverItem
219+
onClick={() => {
220+
onToggleLocked()
221+
onClose()
222+
}}
223+
>
224+
{getToggleLockedLabel()}
225+
</PopoverItem>
226+
)}
198227

199228
{/* Single block actions */}
200229
{isSingleBlock && <PopoverDivider />}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export interface CanvasMenuProps {
3434
canUndo?: boolean
3535
canRedo?: boolean
3636
isInvitationsDisabled?: boolean
37+
/** Whether the workflow has locked blocks (disables auto-layout) */
38+
hasLockedBlocks?: boolean
3739
}
3840

3941
/**
@@ -60,6 +62,7 @@ export function CanvasMenu({
6062
disableEdit = false,
6163
canUndo = false,
6264
canRedo = false,
65+
hasLockedBlocks = false,
6366
}: CanvasMenuProps) {
6467
return (
6568
<Popover
@@ -129,11 +132,12 @@ export function CanvasMenu({
129132
</PopoverItem>
130133
<PopoverItem
131134
className='group'
132-
disabled={disableEdit}
135+
disabled={disableEdit || hasLockedBlocks}
133136
onClick={() => {
134137
onAutoLayout()
135138
onClose()
136139
}}
140+
title={hasLockedBlocks ? 'Unlock blocks to use auto-layout' : undefined}
137141
>
138142
<span>Auto-layout</span>
139143
<span className='ml-auto opacity-70 group-hover:opacity-100'>⇧L</span>

0 commit comments

Comments
 (0)