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>
This commit is contained in:
waleed
2026-01-31 19:02:03 -08:00
parent bd36283d94
commit ecb13a58d6
3 changed files with 18 additions and 10 deletions

View File

@@ -200,18 +200,18 @@ export const ActionBar = memo(
variant='ghost'
onClick={(e) => {
e.stopPropagation()
if (!disabled && !isLocked) {
if (!disabled && !isLocked && !isParentLocked) {
collaborativeBatchToggleBlockEnabled([blockId])
}
}}
className={ACTION_BUTTON_STYLES}
disabled={disabled || isLocked}
disabled={disabled || isLocked || isParentLocked}
>
{isEnabled ? <Circle className={ICON_SIZE} /> : <CircleOff className={ICON_SIZE} />}
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{isLocked
{isLocked || isParentLocked
? 'Block is locked'
: getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')}
</Tooltip.Content>
@@ -274,12 +274,12 @@ export const ActionBar = memo(
variant='ghost'
onClick={(e) => {
e.stopPropagation()
if (!disabled && !isLocked) {
if (!disabled && !isLocked && !isParentLocked) {
collaborativeBatchToggleBlockHandles([blockId])
}
}}
className={ACTION_BUTTON_STYLES}
disabled={disabled || isLocked}
disabled={disabled || isLocked || isParentLocked}
>
{horizontalHandles ? (
<ArrowLeftRight className={ICON_SIZE} />
@@ -289,7 +289,7 @@ export const ActionBar = memo(
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
{isLocked
{isLocked || isParentLocked
? 'Block is locked'
: getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')}
</Tooltip.Content>

View File

@@ -21,6 +21,7 @@ export interface BlockInfo {
parentId?: string
parentType?: string
locked?: boolean
isParentLocked?: boolean
}
/**
@@ -98,6 +99,8 @@ export function BlockMenu({
const allDisabled = selectedBlocks.every((b) => !b.enabled)
const allLocked = selectedBlocks.every((b) => b.locked)
const allUnlocked = selectedBlocks.every((b) => !b.locked)
// Can't unlock blocks that have locked parents
const hasBlockWithLockedParent = selectedBlocks.some((b) => b.locked && b.isParentLocked)
const hasSingletonBlock = selectedBlocks.some(
(b) =>
@@ -216,12 +219,15 @@ export function BlockMenu({
)}
{canAdmin && onToggleLocked && (
<PopoverItem
disabled={hasBlockWithLockedParent}
onClick={() => {
onToggleLocked()
onClose()
if (!hasBlockWithLockedParent) {
onToggleLocked()
onClose()
}
}}
>
{getToggleLockedLabel()}
{hasBlockWithLockedParent ? 'Parent is locked' : getToggleLockedLabel()}
</PopoverItem>
)}

View File

@@ -31,7 +31,8 @@ export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasCo
nodes.map((n) => {
const block = blocks[n.id]
const parentId = block?.data?.parentId
const parentType = parentId ? blocks[parentId]?.type : undefined
const parentBlock = parentId ? blocks[parentId] : undefined
const parentType = parentBlock?.type
return {
id: n.id,
type: block?.type || '',
@@ -40,6 +41,7 @@ export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasCo
parentId,
parentType,
locked: block?.locked ?? false,
isParentLocked: parentBlock?.locked ?? false,
}
}),
[blocks]