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>
This commit is contained in:
waleed
2026-01-31 19:41:22 -08:00
parent c987b6ff6d
commit 8dad4d43b2
2 changed files with 69 additions and 67 deletions

View File

@@ -13,12 +13,12 @@ export interface FilterProtectedBlocksResult {
}
/**
* Checks if a block is protected from deletion.
* Checks if a block is protected from editing/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
* @returns True if the block is protected
*/
export function isBlockProtected(blockId: string, blocks: Record<string, BlockState>): boolean {
const block = blocks[blockId]
@@ -34,6 +34,21 @@ export function isBlockProtected(blockId: string, blocks: Record<string, BlockSt
return false
}
/**
* Checks if an edge is protected from modification.
* An edge is protected if either its source or target block is protected.
*
* @param edge - The edge to check (must have source and target)
* @param blocks - Record of all blocks in the workflow
* @returns True if the edge is protected
*/
export function isEdgeProtected(
edge: { source: string; target: string },
blocks: Record<string, BlockState>
): boolean {
return isBlockProtected(edge.source, blocks) || isBlockProtected(edge.target, blocks)
}
/**
* Filters out protected blocks from a list of block IDs for deletion.
* Protected blocks are those that are locked or inside a locked container.
@@ -55,3 +70,32 @@ export function filterProtectedBlocks(
allProtected: protectedIds.length === blockIds.length && blockIds.length > 0,
}
}
/**
* Filters edges to only include those that are not protected.
*
* @param edges - Array of edges to filter
* @param blocks - Record of all blocks in the workflow
* @returns Array of edges that can be modified (not protected)
*/
export function filterUnprotectedEdges<T extends { source: string; target: string }>(
edges: T[],
blocks: Record<string, BlockState>
): T[] {
return edges.filter((edge) => !isEdgeProtected(edge, blocks))
}
/**
* Checks if any blocks in the selection are protected.
* Useful for determining if edit actions should be disabled.
*
* @param blockIds - Array of block IDs to check
* @param blocks - Record of all blocks in the workflow
* @returns True if any block is protected
*/
export function hasProtectedBlocks(
blockIds: string[],
blocks: Record<string, BlockState>
): boolean {
return blockIds.some((id) => isBlockProtected(id, blocks))
}

View File

@@ -57,6 +57,9 @@ import {
estimateBlockDimensions,
filterProtectedBlocks,
getClampedPositionForNode,
hasProtectedBlocks,
isBlockProtected,
isEdgeProtected,
isInEditableElement,
resolveParentChildSelectionConflicts,
validateTriggerPaste,
@@ -2519,21 +2522,10 @@ const WorkflowContent = React.memo(() => {
.filter((change: any) => change.type === 'remove')
.map((change: any) => change.id)
.filter((edgeId: string) => {
// Prevent removing edges connected to locked blocks or blocks inside locked containers
// Prevent removing edges connected to protected blocks
const edge = edges.find((e) => e.id === edgeId)
if (!edge) return true
const sourceBlock = blocks[edge.source]
const targetBlock = blocks[edge.target]
const sourceParentLocked =
sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked
const targetParentLocked =
targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked
return (
!sourceBlock?.locked &&
!targetBlock?.locked &&
!sourceParentLocked &&
!targetParentLocked
)
return !isEdgeProtected(edge, blocks)
})
if (edgeIdsToRemove.length > 0) {
@@ -2602,19 +2594,8 @@ const WorkflowContent = React.memo(() => {
if (!sourceNode || !targetNode) return
// Prevent connections to/from locked blocks or blocks inside locked containers
const sourceBlock = blocks[connection.source]
const targetBlock = blocks[connection.target]
const sourceParentLocked =
sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked
const targetParentLocked =
targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked
if (
sourceBlock?.locked ||
targetBlock?.locked ||
sourceParentLocked ||
targetParentLocked
) {
// Prevent connections to/from protected blocks
if (isEdgeProtected(connection, blocks)) {
addNotification({
level: 'info',
message: 'Cannot connect to locked blocks or blocks inside locked containers',
@@ -2875,9 +2856,8 @@ const WorkflowContent = React.memo(() => {
/** Captures initial parent ID and position when drag starts. */
const onNodeDragStart = useCallback(
(_event: React.MouseEvent, node: any) => {
// Prevent dragging locked blocks
const block = blocks[node.id]
if (block?.locked) {
// Prevent dragging protected blocks
if (isBlockProtected(node.id, blocks)) {
return
}
@@ -3386,28 +3366,15 @@ const WorkflowContent = React.memo(() => {
/** Stable delete handler to avoid creating new function references per edge. */
const handleEdgeDelete = useCallback(
(edgeId: string) => {
// Prevent removing edges connected to locked blocks or blocks inside locked containers
// Prevent removing edges connected to protected blocks
const edge = edges.find((e) => e.id === edgeId)
if (edge) {
const sourceBlock = blocks[edge.source]
const targetBlock = blocks[edge.target]
const sourceParentLocked =
sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked
const targetParentLocked =
targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked
if (
sourceBlock?.locked ||
targetBlock?.locked ||
sourceParentLocked ||
targetParentLocked
) {
addNotification({
level: 'info',
message: 'Cannot remove connections from locked blocks',
workflowId: activeWorkflowId || undefined,
})
return
}
if (edge && isEdgeProtected(edge, blocks)) {
addNotification({
level: 'info',
message: 'Cannot remove connections from locked blocks',
workflowId: activeWorkflowId || undefined,
})
return
}
removeEdge(edgeId)
// Remove this edge from selection (find by edge ID value)
@@ -3462,22 +3429,11 @@ const WorkflowContent = React.memo(() => {
// Handle edge deletion first (edges take priority if selected)
if (selectedEdges.size > 0) {
// Get all selected edge IDs and filter out edges connected to locked blocks or blocks inside locked containers
// Get all selected edge IDs and filter out edges connected to protected blocks
const edgeIds = Array.from(selectedEdges.values()).filter((edgeId) => {
const edge = edges.find((e) => e.id === edgeId)
if (!edge) return true
const sourceBlock = blocks[edge.source]
const targetBlock = blocks[edge.target]
const sourceParentLocked =
sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked
const targetParentLocked =
targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked
return (
!sourceBlock?.locked &&
!targetBlock?.locked &&
!sourceParentLocked &&
!targetParentLocked
)
return !isEdgeProtected(edge, blocks)
})
if (edgeIds.length > 0) {
collaborativeBatchRemoveEdges(edgeIds)
@@ -3657,8 +3613,10 @@ const WorkflowContent = React.memo(() => {
canRunFromBlock={runFromBlockState.canRun}
disableEdit={
!effectivePermissions.canEdit ||
contextMenuBlocks.some((b) => b.locked) ||
contextMenuBlocks.some((b) => b.parentId && blocks[b.parentId]?.locked)
hasProtectedBlocks(
contextMenuBlocks.map((b) => b.id),
blocks
)
}
userCanEdit={effectivePermissions.canEdit}
isExecuting={isExecuting}