mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-01 02:05:18 -05:00
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:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user