diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts index e9adbebc26..1bf4dacd04 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts @@ -5,6 +5,7 @@ import { useOperationQueueStore } from '@/stores/operation-queue/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import type { WorkflowState } from '@/stores/workflows/workflow/types' +import { createDeploymentSignature } from '@/lib/workflows/deployment-signature' const logger = createLogger('useChangeDetection') @@ -17,6 +18,7 @@ interface UseChangeDetectionProps { /** * Hook to detect changes between current workflow state and deployed state * Uses API-based change detection for accuracy + * Only triggers checks when deployment-relevant changes occur (ignores UI-only changes like position, layout, etc.) */ export function useChangeDetection({ workflowId, @@ -24,9 +26,6 @@ export function useChangeDetection({ isLoadingDeployedState, }: UseChangeDetectionProps) { const [changeDetected, setChangeDetected] = useState(false) - const [blockStructureVersion, setBlockStructureVersion] = useState(0) - const [edgeStructureVersion, setEdgeStructureVersion] = useState(0) - const [subBlockStructureVersion, setSubBlockStructureVersion] = useState(0) // Get current store state for change detection const currentBlocks = useWorkflowStore((state) => state.blocks) @@ -36,35 +35,19 @@ export function useChangeDetection({ workflowId ? state.workflowValues[workflowId] : null ) - // Track structure changes - useEffect(() => { - setBlockStructureVersion((version) => version + 1) - }, [currentBlocks]) - - useEffect(() => { - setEdgeStructureVersion((version) => version + 1) - }, [currentEdges]) - - useEffect(() => { - setSubBlockStructureVersion((version) => version + 1) - }, [subBlockValues]) - - // Reset version counters when workflow changes - useEffect(() => { - setBlockStructureVersion(0) - setEdgeStructureVersion(0) - setSubBlockStructureVersion(0) - }, [workflowId]) + // Create a deployment signature that only includes deployment-relevant properties + // This excludes UI-only changes like position, layout, expanded states, etc. + const deploymentSignature = useMemo(() => { + return createDeploymentSignature(currentBlocks, currentEdges, subBlockValues) + }, [currentBlocks, currentEdges, subBlockValues]) - // Create trigger for status check + // Include lastSaved to trigger check after save operations const statusCheckTrigger = useMemo(() => { return JSON.stringify({ lastSaved: lastSaved ?? 0, - blockVersion: blockStructureVersion, - edgeVersion: edgeStructureVersion, - subBlockVersion: subBlockStructureVersion, + signature: deploymentSignature, }) - }, [lastSaved, blockStructureVersion, edgeStructureVersion, subBlockStructureVersion]) + }, [lastSaved, deploymentSignature]) const debouncedStatusCheckTrigger = useDebounce(statusCheckTrigger, 500) diff --git a/apps/sim/lib/workflows/deployment-signature.ts b/apps/sim/lib/workflows/deployment-signature.ts new file mode 100644 index 0000000000..853ab85daa --- /dev/null +++ b/apps/sim/lib/workflows/deployment-signature.ts @@ -0,0 +1,123 @@ +import type { Edge } from 'reactflow' +import type { BlockState } from '@/stores/workflows/workflow/types' + +/** + * Deployment Signature Module + * + * This module provides utilities to create a "deployment signature" from workflow state. + * The signature only includes properties that are relevant for deployment decisions, + * excluding UI-only changes such as: + * - Block positions + * - Layout measurements (width, height) + * - UI state (expanded/collapsed states) + * - Test values + * + * This ensures that the workflow redeployment check (via /api/workflows/[id]/status) + * is only triggered by meaningful changes that would actually require redeployment, + * not by UI interactions like moving blocks, opening/closing tools, etc. + * + * The normalization logic mirrors the hasWorkflowChanged function in @/lib/workflows/utils + * to ensure consistency between change detection and actual deployment checks. + */ + +/** + * Extracts deployment-relevant properties from a block, excluding UI-only changes + * This mirrors the logic in hasWorkflowChanged to ensure consistency + */ +function normalizeBlockForSignature(block: BlockState): Record { + const { + position: _pos, + layout: _layout, + height: _height, + subBlocks = {}, + ...rest + } = block + + // Exclude width/height from data object (container dimensions from autolayout) + const { width: _width, height: _dataHeight, ...dataRest } = rest.data || {} + + // For subBlocks, we need to extract just the values + const normalizedSubBlocks: Record = {} + for (const [subBlockId, subBlock] of Object.entries(subBlocks)) { + // Special handling for tools subBlock - exclude UI-only 'isExpanded' field + if (subBlockId === 'tools' && Array.isArray(subBlock.value)) { + normalizedSubBlocks[subBlockId] = subBlock.value.map((tool: any) => { + if (tool && typeof tool === 'object') { + const { isExpanded: _isExpanded, ...toolRest } = tool + return toolRest + } + return tool + }) + } else if (subBlockId === 'inputFormat' && Array.isArray(subBlock.value)) { + // Handle inputFormat - exclude collapsed state and test values + normalizedSubBlocks[subBlockId] = subBlock.value.map((field: any) => { + if (field && typeof field === 'object') { + const { value: _value, collapsed: _collapsed, ...fieldRest } = field + return fieldRest + } + return field + }) + } else { + normalizedSubBlocks[subBlockId] = subBlock.value + } + } + + return { + ...rest, + data: dataRest, + subBlocks: normalizedSubBlocks, + } +} + +/** + * Extracts deployment-relevant properties from an edge + */ +function normalizeEdgeForSignature(edge: Edge): Record { + return { + source: edge.source, + sourceHandle: edge.sourceHandle, + target: edge.target, + targetHandle: edge.targetHandle, + } +} + +/** + * Creates a deployment signature from workflow state that only includes + * properties that would trigger a redeployment. UI-only changes like + * position, layout, expanded states, etc. are excluded. + * + * @param blocks - Current blocks from workflow store + * @param edges - Current edges from workflow store + * @param subBlockValues - Current subblock values from subblock store + * @returns A stringified signature that changes only when deployment-relevant changes occur + */ +export function createDeploymentSignature( + blocks: Record, + edges: Edge[], + subBlockValues: Record | null +): string { + // Normalize blocks (excluding UI-only properties) + const normalizedBlocks: Record = {} + for (const [blockId, block] of Object.entries(blocks)) { + normalizedBlocks[blockId] = normalizeBlockForSignature(block) + } + + // Normalize edges (only connection information) + const normalizedEdges = edges + .map(normalizeEdgeForSignature) + .sort((a, b) => + `${a.source}-${a.sourceHandle}-${a.target}-${a.targetHandle}`.localeCompare( + `${b.source}-${b.sourceHandle}-${b.target}-${b.targetHandle}` + ) + ) + + // Create signature object + const signature = { + blockIds: Object.keys(blocks).sort(), + blocks: normalizedBlocks, + edges: normalizedEdges, + subBlockValues: subBlockValues || {}, + } + + return JSON.stringify(signature) +}