Skip to content

Commit 4db6e55

Browse files
waleedlatif1claude
andauthored
feat(canvas): added the ability to lock blocks (#3102)
* feat(canvas): added the ability to lock blocks * unlock duplicates of locked blocks * fix(duplicate): place duplicate outside locked container When duplicating a block that's inside a locked loop/parallel, the duplicate is now placed outside the container since nothing should be added to a locked container. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(duplicate): unlock all blocks when duplicating workflow - Server-side workflow duplication now sets locked: false for all blocks - regenerateWorkflowStateIds also unlocks blocks for templates - Client-side regenerateBlockIds already handled this (for paste/import) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix code block disabled state, allow unlock from editor * fix(lock): address code review feedback - Fix toggle enabled using first toggleable block, not first block - Delete button now checks isParentLocked - Lock button now has disabled state - Editor lock icon distinguishes block vs parent lock state Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lock): prevent unlocking blocks inside locked containers - Editor: can't unlock block if parent container is locked - Action bar: can't unlock block if parent container is locked - Shows "Parent container is locked" tooltip in both cases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lock): ensure consistent behavior across all UIs Block Menu, Editor, Action Bar now all have identical behavior: - Enable/Disable: disabled when locked OR parent locked - Flip Handles: disabled when locked OR parent locked - Delete: disabled when locked OR parent locked - Remove from Subflow: disabled when locked OR parent locked - Lock: always available for admins - Unlock: disabled when parent is locked (unlock parent first) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(enable): consistent behavior - can't enable if parent disabled Same pattern as lock: must enable parent container first before enabling children inside it. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs(quick-reference): add lock block action Added documentation for the lock/unlock block feature (admin only). Note: Image placeholder added, pending actual screenshot. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * remove prefix square brackets in error notif * add lock block image * fix(block-menu): paste should not be disabled for locked selection Paste creates new blocks, doesn't modify selected ones. Changed from disableEdit (includes lock state) to !userCanEdit (permission only), matching the Duplicate action behavior. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(workflow): extract block deletion protection into shared utility Extract duplicated block protection logic from workflow.tsx into a reusable filterProtectedBlocks helper in utils/block-protection-utils.ts. This ensures consistent behavior between context menu delete and keyboard delete operations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(workflow): extend block protection utilities for edge protection Add isEdgeProtected, filterUnprotectedEdges, and hasProtectedBlocks utilities. Refactor workflow.tsx to use these helpers for: - onEdgesChange edge removal filtering - onConnect connection prevention - onNodeDragStart drag prevention - Keyboard edge deletion - Block menu disableEdit calculation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lock): address review comments for lock feature 1. Store batchToggleEnabled now uses continue to skip locked blocks entirely, matching database operation behavior 2. Copilot add operation now checks if parent container is locked before adding nested nodes (defensive check for consistency) 3. Remove unused filterUnprotectedEdges function Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(copilot): add lock checks for insert and extract operations - insert_into_subflow: Check if existing block being moved is locked - extract_from_subflow: Check if block or parent subflow is locked These operations now match the UI behavior where locked blocks cannot be moved into/out of containers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lock): prevent duplicates inside locked containers via regenerateBlockIds 1. regenerateBlockIds now checks if existing parent is locked before keeping the block inside it. If parent is locked, the duplicate is placed outside (parentId cleared) instead of creating an inconsistent state. 2. Remove unnecessary effectivePermissions.canAdmin and potentialParentId from onNodeDragStart dependency array. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lock): fix toggle locked target state and draggable check 1. BATCH_TOGGLE_LOCKED now uses first block from blocksToToggle set instead of blockIds[0], matching BATCH_TOGGLE_ENABLED pattern. Also added early exit if blocksToToggle is empty. 2. Blocks inside locked containers are now properly non-draggable. Changed draggable check from !block.locked to use isBlockProtected() which checks both block lock and parent container lock. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(copilot): check parent lock in edit and delete operations Both edit and delete operations now check if the block's parent container is locked, not just if the block itself is locked. This ensures consistent behavior with the UI which uses isBlockProtected utility that checks both direct lock and parent lock. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(socket): add server-side lock validation and admin-only permissions 1. BATCH_TOGGLE_LOCKED now requires admin role - non-admin users with write role can no longer bypass UI restriction via direct socket messages 2. BATCH_REMOVE_BLOCKS now validates lock status server-side - filters out protected blocks (locked or inside locked parent) before deletion 3. Remove duplicate/outdated comment in regenerateBlockIds Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test(socket): update permission test for admin-only lock toggle batch-toggle-locked is now admin-only, so write role should be denied. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(undo-redo): use consistent target state for toggle redo The redo logic for BATCH_TOGGLE_ENABLED and BATCH_TOGGLE_LOCKED was incorrectly computing each block's new state as !previousStates[blockId]. However, the store's batchToggleEnabled/batchToggleLocked set ALL blocks to the SAME target state based on the first block's previous state. Now redo computes targetState = !previousStates[firstBlockId] and applies it to all blocks, matching the store's behavior. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(socket): add comprehensive lock validation across operations Based on audit findings, adds lock validation to multiple operations: 1. BATCH_TOGGLE_HANDLES - now skips locked/protected blocks at: - Store layer (batchToggleHandles) - Collaborative hook (collaborativeBatchToggleBlockHandles) - Server socket handler 2. BATCH_ADD_BLOCKS - server now filters blocks being added to locked parent containers 3. BATCH_UPDATE_PARENT - server now: - Skips protected blocks (locked or inside locked container) - Prevents moving blocks into locked containers All validations use consistent isProtected() helper that checks both direct lock and parent container lock. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor(workflow): use pre-computed lock state from contextMenuBlocks contextMenuBlocks already has locked and isParentLocked properties computed in use-canvas-context-menu.ts, so there's no need to look up blocks again via hasProtectedBlocks. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lock): add lock validation to block rename operations Defense-in-depth: although the UI disables rename for locked blocks, the collaborative layer and server now also validate locks. - collaborativeUpdateBlockName: checks if block is locked or inside locked container before attempting rename - UPDATE_NAME server handler: checks lock status and parent lock before performing database update Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * added defense in depth for renaming locked blocks * fix(socket): add server-side lock validation for edges and subblocks Defense-in-depth: adds lock checks to server-side handlers that were previously relying only on client-side validation. Edge operations (ADD, REMOVE, BATCH_ADD, BATCH_REMOVE): - Check if source or target blocks are protected before modifying edges Subblock updates: - Check if parent block is protected before updating subblock values Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lock): fetch parent blocks for edge protection checks and consistent tooltip - Fixed edge operations to fetch parent blocks before checking lock status - Previously, isBlockProtected checked if parent was locked, but the parent wasn't in blocksById because only source/target blocks were fetched - Now fetches parent blocks for all four edge operations: ADD, REMOVE, BATCH_ADD_EDGES, BATCH_REMOVE_EDGES - Fixed tooltip inconsistency: changed "Run previous blocks first" to "Run upstream blocks first" in action-bar to match workflow.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * updated tooltip text for run from block * fix(lock): add lock check to duplicate button and clean up drag handler - Added lock check to duplicate button in action bar to prevent duplicating locked blocks (consistent with other edit operations) - Removed ineffective early return in onNodeDragStart since the `draggable` property on nodes already prevents dragging protected blocks - the early return was misleading as it couldn't actually stop a drag operation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(lock): use disableEdit for duplicate in block menu Changed duplicate menu item to use disableEdit (which includes lock check) instead of !userCanEdit for consistency with action bar and other edit operations. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4ba2252 commit 4db6e55

File tree

48 files changed

+12553
-183
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+12553
-183
lines changed

apps/docs/content/docs/en/quick-reference/index.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ A quick lookup for everyday actions in the Sim workflow editor. For keyboard sho
180180
<td>Right-click → **Enable/Disable**</td>
181181
<td><ActionImage src="/static/quick-reference/disable-block.png" alt="Disable block" /></td>
182182
</tr>
183+
<tr>
184+
<td>Lock/Unlock a block</td>
185+
<td>Hover block → Click lock icon (Admin only)</td>
186+
<td><ActionImage src="/static/quick-reference/lock-block.png" alt="Lock block" /></td>
187+
</tr>
183188
<tr>
184189
<td>Toggle handle orientation</td>
185190
<td>Right-click → **Toggle Handles**</td>

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

33.6 KB
Loading

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

Lines changed: 94 additions & 31 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,16 +85,28 @@ export const ActionBar = memo(
8485
)
8586
}, [blockId, addNotification, collaborativeBatchAddBlocks, setPendingSelection])
8687

87-
const { isEnabled, horizontalHandles, parentId, parentType } = useWorkflowStore(
88+
const {
89+
isEnabled,
90+
horizontalHandles,
91+
parentId,
92+
parentType,
93+
isLocked,
94+
isParentLocked,
95+
isParentDisabled,
96+
} = useWorkflowStore(
8897
useCallback(
8998
(state) => {
9099
const block = state.blocks[blockId]
91100
const parentId = block?.data?.parentId
101+
const parentBlock = parentId ? state.blocks[parentId] : undefined
92102
return {
93103
isEnabled: block?.enabled ?? true,
94104
horizontalHandles: block?.horizontalHandles ?? false,
95105
parentId,
96-
parentType: parentId ? state.blocks[parentId]?.type : undefined,
106+
parentType: parentBlock?.type,
107+
isLocked: block?.locked ?? false,
108+
isParentLocked: parentBlock?.locked ?? false,
109+
isParentDisabled: parentBlock ? !parentBlock.enabled : false,
97110
}
98111
},
99112
[blockId]
@@ -159,52 +172,90 @@ export const ActionBar = memo(
159172
)}
160173
>
161174
{!isNoteBlock && !isInsideSubflow && (
175+
<Tooltip.Root>
176+
<Tooltip.Trigger asChild>
177+
<span className='inline-flex'>
178+
<Button
179+
variant='ghost'
180+
onClick={(e) => {
181+
e.stopPropagation()
182+
if (canRunFromBlock && !disabled) {
183+
handleRunFromBlockClick()
184+
}
185+
}}
186+
className={ACTION_BUTTON_STYLES}
187+
disabled={disabled || !canRunFromBlock}
188+
>
189+
<PlayOutline className={ICON_SIZE} />
190+
</Button>
191+
</span>
192+
</Tooltip.Trigger>
193+
<Tooltip.Content side='top'>
194+
{(() => {
195+
if (disabled) return getTooltipMessage('Run from block')
196+
if (isExecuting) return 'Execution in progress'
197+
if (!dependenciesSatisfied) return 'Run previous blocks first'
198+
return 'Run from block'
199+
})()}
200+
</Tooltip.Content>
201+
</Tooltip.Root>
202+
)}
203+
204+
{!isNoteBlock && (
162205
<Tooltip.Root>
163206
<Tooltip.Trigger asChild>
164207
<Button
165208
variant='ghost'
166209
onClick={(e) => {
167210
e.stopPropagation()
168-
if (canRunFromBlock && !disabled) {
169-
handleRunFromBlockClick()
211+
// Can't enable if parent is disabled (must enable parent first)
212+
const cantEnable = !isEnabled && isParentDisabled
213+
if (!disabled && !isLocked && !isParentLocked && !cantEnable) {
214+
collaborativeBatchToggleBlockEnabled([blockId])
170215
}
171216
}}
172217
className={ACTION_BUTTON_STYLES}
173-
disabled={disabled || !canRunFromBlock}
218+
disabled={
219+
disabled || isLocked || isParentLocked || (!isEnabled && isParentDisabled)
220+
}
174221
>
175-
<PlayOutline className={ICON_SIZE} />
222+
{isEnabled ? <Circle className={ICON_SIZE} /> : <CircleOff className={ICON_SIZE} />}
176223
</Button>
177224
</Tooltip.Trigger>
178225
<Tooltip.Content side='top'>
179-
{(() => {
180-
if (disabled) return getTooltipMessage('Run from block')
181-
if (isExecuting) return 'Execution in progress'
182-
if (!dependenciesSatisfied) return 'Run upstream blocks first'
183-
return 'Run from block'
184-
})()}
226+
{isLocked || isParentLocked
227+
? 'Block is locked'
228+
: !isEnabled && isParentDisabled
229+
? 'Parent container is disabled'
230+
: getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')}
185231
</Tooltip.Content>
186232
</Tooltip.Root>
187233
)}
188234

189-
{!isNoteBlock && (
235+
{userPermissions.canAdmin && (
190236
<Tooltip.Root>
191237
<Tooltip.Trigger asChild>
192238
<Button
193239
variant='ghost'
194240
onClick={(e) => {
195241
e.stopPropagation()
196-
if (!disabled) {
197-
collaborativeBatchToggleBlockEnabled([blockId])
242+
// Can't unlock a block if its parent container is locked
243+
if (!disabled && !(isLocked && isParentLocked)) {
244+
collaborativeBatchToggleLocked([blockId])
198245
}
199246
}}
200247
className={ACTION_BUTTON_STYLES}
201-
disabled={disabled}
248+
disabled={disabled || (isLocked && isParentLocked)}
202249
>
203-
{isEnabled ? <Circle className={ICON_SIZE} /> : <CircleOff className={ICON_SIZE} />}
250+
{isLocked ? <Unlock className={ICON_SIZE} /> : <Lock className={ICON_SIZE} />}
204251
</Button>
205252
</Tooltip.Trigger>
206253
<Tooltip.Content side='top'>
207-
{getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')}
254+
{isLocked && isParentLocked
255+
? 'Parent container is locked'
256+
: isLocked
257+
? 'Unlock Block'
258+
: 'Lock Block'}
208259
</Tooltip.Content>
209260
</Tooltip.Root>
210261
)}
@@ -216,17 +267,21 @@ export const ActionBar = memo(
216267
variant='ghost'
217268
onClick={(e) => {
218269
e.stopPropagation()
219-
if (!disabled) {
270+
if (!disabled && !isLocked && !isParentLocked) {
220271
handleDuplicateBlock()
221272
}
222273
}}
223274
className={ACTION_BUTTON_STYLES}
224-
disabled={disabled}
275+
disabled={disabled || isLocked || isParentLocked}
225276
>
226277
<Copy className={ICON_SIZE} />
227278
</Button>
228279
</Tooltip.Trigger>
229-
<Tooltip.Content side='top'>{getTooltipMessage('Duplicate Block')}</Tooltip.Content>
280+
<Tooltip.Content side='top'>
281+
{isLocked || isParentLocked
282+
? 'Block is locked'
283+
: getTooltipMessage('Duplicate Block')}
284+
</Tooltip.Content>
230285
</Tooltip.Root>
231286
)}
232287

@@ -237,12 +292,12 @@ export const ActionBar = memo(
237292
variant='ghost'
238293
onClick={(e) => {
239294
e.stopPropagation()
240-
if (!disabled) {
295+
if (!disabled && !isLocked && !isParentLocked) {
241296
collaborativeBatchToggleBlockHandles([blockId])
242297
}
243298
}}
244299
className={ACTION_BUTTON_STYLES}
245-
disabled={disabled}
300+
disabled={disabled || isLocked || isParentLocked}
246301
>
247302
{horizontalHandles ? (
248303
<ArrowLeftRight className={ICON_SIZE} />
@@ -252,7 +307,9 @@ export const ActionBar = memo(
252307
</Button>
253308
</Tooltip.Trigger>
254309
<Tooltip.Content side='top'>
255-
{getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')}
310+
{isLocked || isParentLocked
311+
? 'Block is locked'
312+
: getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')}
256313
</Tooltip.Content>
257314
</Tooltip.Root>
258315
)}
@@ -264,19 +321,23 @@ export const ActionBar = memo(
264321
variant='ghost'
265322
onClick={(e) => {
266323
e.stopPropagation()
267-
if (!disabled && userPermissions.canEdit) {
324+
if (!disabled && userPermissions.canEdit && !isLocked && !isParentLocked) {
268325
window.dispatchEvent(
269326
new CustomEvent('remove-from-subflow', { detail: { blockIds: [blockId] } })
270327
)
271328
}
272329
}}
273330
className={ACTION_BUTTON_STYLES}
274-
disabled={disabled || !userPermissions.canEdit}
331+
disabled={disabled || !userPermissions.canEdit || isLocked || isParentLocked}
275332
>
276333
<LogOut className={ICON_SIZE} />
277334
</Button>
278335
</Tooltip.Trigger>
279-
<Tooltip.Content side='top'>{getTooltipMessage('Remove from Subflow')}</Tooltip.Content>
336+
<Tooltip.Content side='top'>
337+
{isLocked || isParentLocked
338+
? 'Block is locked'
339+
: getTooltipMessage('Remove from Subflow')}
340+
</Tooltip.Content>
280341
</Tooltip.Root>
281342
)}
282343

@@ -286,17 +347,19 @@ export const ActionBar = memo(
286347
variant='ghost'
287348
onClick={(e) => {
288349
e.stopPropagation()
289-
if (!disabled) {
350+
if (!disabled && !isLocked && !isParentLocked) {
290351
collaborativeBatchRemoveBlocks([blockId])
291352
}
292353
}}
293354
className={ACTION_BUTTON_STYLES}
294-
disabled={disabled}
355+
disabled={disabled || isLocked || isParentLocked}
295356
>
296357
<Trash2 className={ICON_SIZE} />
297358
</Button>
298359
</Tooltip.Trigger>
299-
<Tooltip.Content side='top'>{getTooltipMessage('Delete Block')}</Tooltip.Content>
360+
<Tooltip.Content side='top'>
361+
{isLocked || isParentLocked ? 'Block is locked' : getTooltipMessage('Delete Block')}
362+
</Tooltip.Content>
300363
</Tooltip.Root>
301364
</div>
302365
)

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

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export interface BlockInfo {
2020
horizontalHandles: boolean
2121
parentId?: string
2222
parentType?: string
23+
locked?: boolean
24+
isParentLocked?: boolean
25+
isParentDisabled?: boolean
2326
}
2427

2528
/**
@@ -46,10 +49,17 @@ export interface BlockMenuProps {
4649
showRemoveFromSubflow?: boolean
4750
/** Whether run from block is available (has snapshot, was executed, not inside subflow) */
4851
canRunFromBlock?: boolean
52+
/** Whether to disable edit actions (user can't edit OR blocks are locked) */
4953
disableEdit?: boolean
54+
/** Whether the user has edit permission (ignoring locked state) */
55+
userCanEdit?: boolean
5056
isExecuting?: boolean
5157
/** Whether the selected block is a trigger (has no incoming edges) */
5258
isPositionalTrigger?: boolean
59+
/** Callback to toggle locked state of selected blocks */
60+
onToggleLocked?: () => void
61+
/** Whether the user has admin permissions */
62+
canAdmin?: boolean
5363
}
5464

5565
/**
@@ -78,13 +88,22 @@ export function BlockMenu({
7888
showRemoveFromSubflow = false,
7989
canRunFromBlock = false,
8090
disableEdit = false,
91+
userCanEdit = true,
8192
isExecuting = false,
8293
isPositionalTrigger = false,
94+
onToggleLocked,
95+
canAdmin = false,
8396
}: BlockMenuProps) {
8497
const isSingleBlock = selectedBlocks.length === 1
8598

8699
const allEnabled = selectedBlocks.every((b) => b.enabled)
87100
const allDisabled = selectedBlocks.every((b) => !b.enabled)
101+
const allLocked = selectedBlocks.every((b) => b.locked)
102+
const allUnlocked = selectedBlocks.every((b) => !b.locked)
103+
// Can't unlock blocks that have locked parents
104+
const hasBlockWithLockedParent = selectedBlocks.some((b) => b.locked && b.isParentLocked)
105+
// Can't enable blocks that have disabled parents
106+
const hasBlockWithDisabledParent = selectedBlocks.some((b) => !b.enabled && b.isParentDisabled)
88107

89108
const hasSingletonBlock = selectedBlocks.some(
90109
(b) =>
@@ -108,6 +127,12 @@ export function BlockMenu({
108127
return 'Toggle Enabled'
109128
}
110129

130+
const getToggleLockedLabel = () => {
131+
if (allLocked) return 'Unlock'
132+
if (allUnlocked) return 'Lock'
133+
return 'Toggle Lock'
134+
}
135+
111136
return (
112137
<Popover
113138
open={isOpen}
@@ -139,7 +164,7 @@ export function BlockMenu({
139164
</PopoverItem>
140165
<PopoverItem
141166
className='group'
142-
disabled={disableEdit || !hasClipboard}
167+
disabled={!userCanEdit || !hasClipboard}
143168
onClick={() => {
144169
onPaste()
145170
onClose()
@@ -164,13 +189,15 @@ export function BlockMenu({
164189
{!allNoteBlocks && <PopoverDivider />}
165190
{!allNoteBlocks && (
166191
<PopoverItem
167-
disabled={disableEdit}
192+
disabled={disableEdit || hasBlockWithDisabledParent}
168193
onClick={() => {
169-
onToggleEnabled()
170-
onClose()
194+
if (!disableEdit && !hasBlockWithDisabledParent) {
195+
onToggleEnabled()
196+
onClose()
197+
}
171198
}}
172199
>
173-
{getToggleEnabledLabel()}
200+
{hasBlockWithDisabledParent ? 'Parent is disabled' : getToggleEnabledLabel()}
174201
</PopoverItem>
175202
)}
176203
{!allNoteBlocks && !isSubflow && (
@@ -195,6 +222,19 @@ export function BlockMenu({
195222
Remove from Subflow
196223
</PopoverItem>
197224
)}
225+
{canAdmin && onToggleLocked && (
226+
<PopoverItem
227+
disabled={hasBlockWithLockedParent}
228+
onClick={() => {
229+
if (!hasBlockWithLockedParent) {
230+
onToggleLocked()
231+
onClose()
232+
}
233+
}}
234+
>
235+
{hasBlockWithLockedParent ? 'Parent is locked' : getToggleLockedLabel()}
236+
</PopoverItem>
237+
)}
198238

199239
{/* Single block actions */}
200240
{isSingleBlock && <PopoverDivider />}

0 commit comments

Comments
 (0)