diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts new file mode 100644 index 000000000..800effd0c --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts @@ -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): 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 +): 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, + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts index d2845af28..88772d16f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts @@ -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' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 649271070..85e0447df 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -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) } }