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>
This commit is contained in:
waleed
2026-01-31 19:35:01 -08:00
parent 901bffe44c
commit c987b6ff6d
3 changed files with 75 additions and 21 deletions

View File

@@ -0,0 +1,57 @@
import type { BlockState } from '@/stores/workflows/workflow/types'
/**
* Result of filtering protected blocks from a deletion operation
*/
export interface FilterProtectedBlocksResult {
/** Block IDs that can be deleted (not protected) */
deletableIds: string[]
/** Block IDs that are protected and cannot be deleted */
protectedIds: string[]
/** Whether all blocks are protected (deletion should be cancelled entirely) */
allProtected: boolean
}
/**
* Checks if a block is protected from deletion.
* A block is protected if it is locked or if its parent container is locked.
*
* @param blockId - The ID of the block to check
* @param blocks - Record of all blocks in the workflow
* @returns True if the block is protected from deletion
*/
export function isBlockProtected(blockId: string, blocks: Record<string, BlockState>): boolean {
const block = blocks[blockId]
if (!block) return false
// Block is locked directly
if (block.locked) return true
// Block is inside a locked container
const parentId = block.data?.parentId
if (parentId && blocks[parentId]?.locked) return true
return false
}
/**
* Filters out protected blocks from a list of block IDs for deletion.
* Protected blocks are those that are locked or inside a locked container.
*
* @param blockIds - Array of block IDs to filter
* @param blocks - Record of all blocks in the workflow
* @returns Result containing deletable IDs, protected IDs, and whether all are protected
*/
export function filterProtectedBlocks(
blockIds: string[],
blocks: Record<string, BlockState>
): FilterProtectedBlocksResult {
const protectedIds = blockIds.filter((id) => isBlockProtected(id, blocks))
const deletableIds = blockIds.filter((id) => !protectedIds.includes(id))
return {
deletableIds,
protectedIds,
allProtected: protectedIds.length === blockIds.length && blockIds.length > 0,
}
}

View File

@@ -1,4 +1,5 @@
export * from './auto-layout-utils'
export * from './block-protection-utils'
export * from './block-ring-utils'
export * from './node-position-utils'
export * from './workflow-canvas-helpers'

View File

@@ -55,6 +55,7 @@ import {
clearDragHighlights,
computeClampedPositionUpdates,
estimateBlockDimensions,
filterProtectedBlocks,
getClampedPositionForNode,
isInEditableElement,
resolveParentChildSelectionConflicts,
@@ -1069,14 +1070,11 @@ const WorkflowContent = React.memo(() => {
}, [contextMenuBlocks, copyBlocks, executePasteOperation])
const handleContextDelete = useCallback(() => {
let blockIds = contextMenuBlocks.map((b) => b.id)
// Filter out locked blocks and blocks inside locked containers
const protectedBlockIds = contextMenuBlocks
.filter((b) => b.locked || (b.parentId && blocks[b.parentId]?.locked))
.map((b) => b.id)
if (protectedBlockIds.length > 0) {
blockIds = blockIds.filter((id) => !protectedBlockIds.includes(id))
if (protectedBlockIds.length === contextMenuBlocks.length) {
const blockIds = contextMenuBlocks.map((b) => b.id)
const { deletableIds, protectedIds, allProtected } = filterProtectedBlocks(blockIds, blocks)
if (protectedIds.length > 0) {
if (allProtected) {
addNotification({
level: 'info',
message: 'Cannot delete locked blocks or blocks inside locked containers',
@@ -1086,12 +1084,12 @@ const WorkflowContent = React.memo(() => {
}
addNotification({
level: 'info',
message: `Skipped ${protectedBlockIds.length} protected block(s)`,
message: `Skipped ${protectedIds.length} protected block(s)`,
workflowId: activeWorkflowId || undefined,
})
}
if (blockIds.length > 0) {
collaborativeBatchRemoveBlocks(blockIds)
if (deletableIds.length > 0) {
collaborativeBatchRemoveBlocks(deletableIds)
}
}, [contextMenuBlocks, collaborativeBatchRemoveBlocks, addNotification, activeWorkflowId, blocks])
@@ -3499,16 +3497,14 @@ const WorkflowContent = React.memo(() => {
}
event.preventDefault()
let selectedIds = selectedNodes.map((node) => node.id)
// Filter out locked blocks and blocks inside locked containers
const protectedIds = selectedIds.filter(
(id) =>
blocks[id]?.locked ||
(blocks[id]?.data?.parentId && blocks[blocks[id]?.data?.parentId]?.locked)
const selectedIds = selectedNodes.map((node) => node.id)
const { deletableIds, protectedIds, allProtected } = filterProtectedBlocks(
selectedIds,
blocks
)
if (protectedIds.length > 0) {
selectedIds = selectedIds.filter((id) => !protectedIds.includes(id))
if (protectedIds.length === selectedNodes.length) {
if (allProtected) {
addNotification({
level: 'info',
message: 'Cannot delete locked blocks or blocks inside locked containers',
@@ -3522,8 +3518,8 @@ const WorkflowContent = React.memo(() => {
workflowId: activeWorkflowId || undefined,
})
}
if (selectedIds.length > 0) {
collaborativeBatchRemoveBlocks(selectedIds)
if (deletableIds.length > 0) {
collaborativeBatchRemoveBlocks(deletableIds)
}
}