removed redundant logic, kept single source of truth for diff

This commit is contained in:
waleed
2026-01-28 12:28:07 -08:00
parent 8aaaefcd72
commit 1a1f3e87e7
4 changed files with 468 additions and 286 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import { useCallback, useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import {
Button,
Modal,
@@ -32,14 +32,14 @@ export function VersionDescriptionModal({
versionName,
currentDescription,
}: VersionDescriptionModalProps) {
const initialDescription = currentDescription || ''
const [description, setDescription] = useState(initialDescription)
const initialDescriptionRef = useRef(currentDescription || '')
const [description, setDescription] = useState(initialDescriptionRef.current)
const [showUnsavedChangesAlert, setShowUnsavedChangesAlert] = useState(false)
const updateMutation = useUpdateDeploymentVersion()
const generateMutation = useGenerateVersionDescription()
const hasChanges = description.trim() !== initialDescription.trim()
const hasChanges = description.trim() !== initialDescriptionRef.current.trim()
const isGenerating = generateMutation.isPending
const handleCloseAttempt = useCallback(() => {
@@ -55,9 +55,9 @@ export function VersionDescriptionModal({
const handleDiscardChanges = useCallback(() => {
setShowUnsavedChangesAlert(false)
setDescription(initialDescription)
setDescription(initialDescriptionRef.current)
onOpenChange(false)
}, [initialDescription, onOpenChange])
}, [onOpenChange])
const handleGenerateDescription = useCallback(() => {
generateMutation.mutate({

View File

@@ -73,6 +73,7 @@ export function Versions({
}
const handleSaveRename = (version: number) => {
if (renameMutation.isPending) return
if (!workflowId || !editValue.trim()) {
setEditingVersion(null)
return

View File

@@ -7,7 +7,11 @@ import {
} from '@sim/testing'
import { describe, expect, it } from 'vitest'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
import { hasWorkflowChanged } from './compare'
import {
formatDiffSummaryForDescription,
generateWorkflowDiffSummary,
hasWorkflowChanged,
} from './compare'
/**
* Type helper for converting test workflow state to app workflow state.
@@ -2735,3 +2739,299 @@ describe('hasWorkflowChanged', () => {
})
})
})
describe('generateWorkflowDiffSummary', () => {
describe('Basic Cases', () => {
it.concurrent('should return hasChanges=true when previousState is null', () => {
const currentState = createWorkflowState({
blocks: { block1: createBlock('block1') },
})
const result = generateWorkflowDiffSummary(currentState, null)
expect(result.hasChanges).toBe(true)
expect(result.addedBlocks).toHaveLength(1)
expect(result.addedBlocks[0].id).toBe('block1')
})
it.concurrent('should return hasChanges=false for identical states', () => {
const state = createWorkflowState({
blocks: { block1: createBlock('block1') },
})
const result = generateWorkflowDiffSummary(state, state)
expect(result.hasChanges).toBe(false)
expect(result.addedBlocks).toHaveLength(0)
expect(result.removedBlocks).toHaveLength(0)
expect(result.modifiedBlocks).toHaveLength(0)
})
})
describe('Block Changes', () => {
it.concurrent('should detect added blocks', () => {
const previousState = createWorkflowState({
blocks: { block1: createBlock('block1') },
})
const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
block2: createBlock('block2'),
},
})
const result = generateWorkflowDiffSummary(currentState, previousState)
expect(result.hasChanges).toBe(true)
expect(result.addedBlocks).toHaveLength(1)
expect(result.addedBlocks[0].id).toBe('block2')
})
it.concurrent('should detect removed blocks', () => {
const previousState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
block2: createBlock('block2'),
},
})
const currentState = createWorkflowState({
blocks: { block1: createBlock('block1') },
})
const result = generateWorkflowDiffSummary(currentState, previousState)
expect(result.hasChanges).toBe(true)
expect(result.removedBlocks).toHaveLength(1)
expect(result.removedBlocks[0].id).toBe('block2')
})
it.concurrent('should detect modified blocks with field changes', () => {
const previousState = createWorkflowState({
blocks: {
block1: createBlock('block1', {
subBlocks: { model: { id: 'model', type: 'dropdown', value: 'gpt-4o' } },
}),
},
})
const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1', {
subBlocks: { model: { id: 'model', type: 'dropdown', value: 'claude-sonnet' } },
}),
},
})
const result = generateWorkflowDiffSummary(currentState, previousState)
expect(result.hasChanges).toBe(true)
expect(result.modifiedBlocks).toHaveLength(1)
expect(result.modifiedBlocks[0].id).toBe('block1')
expect(result.modifiedBlocks[0].changes.length).toBeGreaterThan(0)
const modelChange = result.modifiedBlocks[0].changes.find((c) => c.field === 'model')
expect(modelChange).toBeDefined()
expect(modelChange?.oldValue).toBe('gpt-4o')
expect(modelChange?.newValue).toBe('claude-sonnet')
})
})
describe('Edge Changes', () => {
it.concurrent('should detect added edges', () => {
const previousState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
block2: createBlock('block2'),
},
edges: [],
})
const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
block2: createBlock('block2'),
},
edges: [{ id: 'e1', source: 'block1', target: 'block2' }],
})
const result = generateWorkflowDiffSummary(currentState, previousState)
expect(result.hasChanges).toBe(true)
expect(result.edgeChanges.added).toBe(1)
expect(result.edgeChanges.removed).toBe(0)
})
it.concurrent('should detect removed edges', () => {
const previousState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
block2: createBlock('block2'),
},
edges: [{ id: 'e1', source: 'block1', target: 'block2' }],
})
const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
block2: createBlock('block2'),
},
edges: [],
})
const result = generateWorkflowDiffSummary(currentState, previousState)
expect(result.hasChanges).toBe(true)
expect(result.edgeChanges.added).toBe(0)
expect(result.edgeChanges.removed).toBe(1)
})
})
describe('Variable Changes', () => {
it.concurrent('should detect added variables', () => {
const previousState = createWorkflowState({
blocks: { block1: createBlock('block1') },
variables: {},
})
const currentState = createWorkflowState({
blocks: { block1: createBlock('block1') },
variables: { var1: { id: 'var1', name: 'test', type: 'string', value: 'hello' } },
})
const result = generateWorkflowDiffSummary(currentState, previousState)
expect(result.hasChanges).toBe(true)
expect(result.variableChanges.added).toBe(1)
})
it.concurrent('should detect modified variables', () => {
const previousState = createWorkflowState({
blocks: { block1: createBlock('block1') },
variables: { var1: { id: 'var1', name: 'test', type: 'string', value: 'hello' } },
})
const currentState = createWorkflowState({
blocks: { block1: createBlock('block1') },
variables: { var1: { id: 'var1', name: 'test', type: 'string', value: 'world' } },
})
const result = generateWorkflowDiffSummary(currentState, previousState)
expect(result.hasChanges).toBe(true)
expect(result.variableChanges.modified).toBe(1)
})
})
describe('Consistency with hasWorkflowChanged', () => {
it.concurrent('hasChanges should match hasWorkflowChanged result', () => {
const state1 = createWorkflowState({
blocks: { block1: createBlock('block1') },
})
const state2 = createWorkflowState({
blocks: {
block1: createBlock('block1', {
subBlocks: { prompt: { id: 'prompt', type: 'long-input', value: 'new value' } },
}),
},
})
const diffResult = generateWorkflowDiffSummary(state2, state1)
const hasChangedResult = hasWorkflowChanged(state2, state1)
expect(diffResult.hasChanges).toBe(hasChangedResult)
})
it.concurrent('should return same result as hasWorkflowChanged for no changes', () => {
const state = createWorkflowState({
blocks: { block1: createBlock('block1') },
})
const diffResult = generateWorkflowDiffSummary(state, state)
const hasChangedResult = hasWorkflowChanged(state, state)
expect(diffResult.hasChanges).toBe(hasChangedResult)
expect(diffResult.hasChanges).toBe(false)
})
})
})
describe('formatDiffSummaryForDescription', () => {
it.concurrent('should return no changes message when hasChanges is false', () => {
const summary = {
addedBlocks: [],
removedBlocks: [],
modifiedBlocks: [],
edgeChanges: { added: 0, removed: 0 },
loopChanges: { added: 0, removed: 0 },
parallelChanges: { added: 0, removed: 0 },
variableChanges: { added: 0, removed: 0, modified: 0 },
hasChanges: false,
}
const result = formatDiffSummaryForDescription(summary)
expect(result).toContain('No structural changes')
})
it.concurrent('should format added blocks', () => {
const summary = {
addedBlocks: [{ id: 'block1', type: 'agent', name: 'My Agent' }],
removedBlocks: [],
modifiedBlocks: [],
edgeChanges: { added: 0, removed: 0 },
loopChanges: { added: 0, removed: 0 },
parallelChanges: { added: 0, removed: 0 },
variableChanges: { added: 0, removed: 0, modified: 0 },
hasChanges: true,
}
const result = formatDiffSummaryForDescription(summary)
expect(result).toContain('Added block: My Agent (agent)')
})
it.concurrent('should format removed blocks', () => {
const summary = {
addedBlocks: [],
removedBlocks: [{ id: 'block1', type: 'function', name: 'Old Function' }],
modifiedBlocks: [],
edgeChanges: { added: 0, removed: 0 },
loopChanges: { added: 0, removed: 0 },
parallelChanges: { added: 0, removed: 0 },
variableChanges: { added: 0, removed: 0, modified: 0 },
hasChanges: true,
}
const result = formatDiffSummaryForDescription(summary)
expect(result).toContain('Removed block: Old Function (function)')
})
it.concurrent('should format modified blocks with field changes', () => {
const summary = {
addedBlocks: [],
removedBlocks: [],
modifiedBlocks: [
{
id: 'block1',
type: 'agent',
name: 'Agent 1',
changes: [{ field: 'model', oldValue: 'gpt-4o', newValue: 'claude-sonnet' }],
},
],
edgeChanges: { added: 0, removed: 0 },
loopChanges: { added: 0, removed: 0 },
parallelChanges: { added: 0, removed: 0 },
variableChanges: { added: 0, removed: 0, modified: 0 },
hasChanges: true,
}
const result = formatDiffSummaryForDescription(summary)
expect(result).toContain('Modified Agent 1')
expect(result).toContain('model')
expect(result).toContain('gpt-4o')
expect(result).toContain('claude-sonnet')
})
it.concurrent('should format edge changes', () => {
const summary = {
addedBlocks: [],
removedBlocks: [],
modifiedBlocks: [],
edgeChanges: { added: 2, removed: 1 },
loopChanges: { added: 0, removed: 0 },
parallelChanges: { added: 0, removed: 0 },
variableChanges: { added: 0, removed: 0, modified: 0 },
hasChanges: true,
}
const result = formatDiffSummaryForDescription(summary)
expect(result).toContain('Added 2 connection(s)')
expect(result).toContain('Removed 1 connection(s)')
})
it.concurrent('should format variable changes', () => {
const summary = {
addedBlocks: [],
removedBlocks: [],
modifiedBlocks: [],
edgeChanges: { added: 0, removed: 0 },
loopChanges: { added: 0, removed: 0 },
parallelChanges: { added: 0, removed: 0 },
variableChanges: { added: 1, removed: 0, modified: 2 },
hasChanges: true,
}
const result = formatDiffSummaryForDescription(summary)
expect(result).toContain('Variables:')
expect(result).toContain('1 added')
expect(result).toContain('2 modified')
})
})

View File

@@ -10,7 +10,6 @@ import {
sanitizeInputFormat,
sanitizeTools,
sanitizeVariable,
sortEdges,
} from './normalize'
/** Block with optional diff markers added by copilot */
@@ -28,252 +27,14 @@ type SubBlockWithDiffMarker = {
}
/**
* Compare the current workflow state with the deployed state to detect meaningful changes
* @param currentState - The current workflow state
* @param deployedState - The deployed workflow state
* @returns True if there are meaningful changes, false if only position changes or no changes
* Compare the current workflow state with the deployed state to detect meaningful changes.
* Uses generateWorkflowDiffSummary internally to ensure consistent change detection.
*/
export function hasWorkflowChanged(
currentState: WorkflowState,
deployedState: WorkflowState | null
): boolean {
// If no deployed state exists, then the workflow has changed
if (!deployedState) return true
// 1. Compare edges (connections between blocks)
const currentEdges = currentState.edges || []
const deployedEdges = deployedState.edges || []
const normalizedCurrentEdges = sortEdges(currentEdges.map(normalizeEdge))
const normalizedDeployedEdges = sortEdges(deployedEdges.map(normalizeEdge))
if (
normalizedStringify(normalizedCurrentEdges) !== normalizedStringify(normalizedDeployedEdges)
) {
return true
}
// 2. Compare blocks and their configurations
const currentBlockIds = Object.keys(currentState.blocks || {}).sort()
const deployedBlockIds = Object.keys(deployedState.blocks || {}).sort()
if (
currentBlockIds.length !== deployedBlockIds.length ||
normalizedStringify(currentBlockIds) !== normalizedStringify(deployedBlockIds)
) {
return true
}
// 3. Build normalized representations of blocks for comparison
const normalizedCurrentBlocks: Record<string, unknown> = {}
const normalizedDeployedBlocks: Record<string, unknown> = {}
for (const blockId of currentBlockIds) {
const currentBlock = currentState.blocks[blockId]
const deployedBlock = deployedState.blocks[blockId]
// Destructure and exclude non-functional fields:
// - position: visual positioning only
// - subBlocks: handled separately below
// - layout: contains measuredWidth/measuredHeight from autolayout
// - height: block height measurement from autolayout
// - outputs: derived from subBlocks (e.g., inputFormat), already compared via subBlocks
// - is_diff, field_diffs: diff markers from copilot edits
const currentBlockWithDiff = currentBlock as BlockWithDiffMarkers
const deployedBlockWithDiff = deployedBlock as BlockWithDiffMarkers
const {
position: _currentPos,
subBlocks: currentSubBlocks = {},
layout: _currentLayout,
height: _currentHeight,
outputs: _currentOutputs,
is_diff: _currentIsDiff,
field_diffs: _currentFieldDiffs,
...currentRest
} = currentBlockWithDiff
const {
position: _deployedPos,
subBlocks: deployedSubBlocks = {},
layout: _deployedLayout,
height: _deployedHeight,
outputs: _deployedOutputs,
is_diff: _deployedIsDiff,
field_diffs: _deployedFieldDiffs,
...deployedRest
} = deployedBlockWithDiff
// Also exclude width/height from data object (container dimensions from autolayout)
const {
width: _currentDataWidth,
height: _currentDataHeight,
...currentDataRest
} = currentRest.data || {}
const {
width: _deployedDataWidth,
height: _deployedDataHeight,
...deployedDataRest
} = deployedRest.data || {}
normalizedCurrentBlocks[blockId] = {
...currentRest,
data: currentDataRest,
subBlocks: undefined,
}
normalizedDeployedBlocks[blockId] = {
...deployedRest,
data: deployedDataRest,
subBlocks: undefined,
}
// Get all subBlock IDs from both states, excluding runtime metadata and UI-only elements
const allSubBlockIds = [
...new Set([...Object.keys(currentSubBlocks), ...Object.keys(deployedSubBlocks)]),
]
.filter(
(id) => !TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(id) && !SYSTEM_SUBBLOCK_IDS.includes(id)
)
.sort()
// Normalize and compare each subBlock
for (const subBlockId of allSubBlockIds) {
// If the subBlock doesn't exist in either state, there's a difference
if (!currentSubBlocks[subBlockId] || !deployedSubBlocks[subBlockId]) {
return true
}
// Get values with special handling for null/undefined
// Using unknown type since sanitization functions return different types
let currentValue: unknown = currentSubBlocks[subBlockId].value ?? null
let deployedValue: unknown = deployedSubBlocks[subBlockId].value ?? null
if (subBlockId === 'tools' && Array.isArray(currentValue) && Array.isArray(deployedValue)) {
currentValue = sanitizeTools(currentValue)
deployedValue = sanitizeTools(deployedValue)
}
if (
subBlockId === 'inputFormat' &&
Array.isArray(currentValue) &&
Array.isArray(deployedValue)
) {
currentValue = sanitizeInputFormat(currentValue)
deployedValue = sanitizeInputFormat(deployedValue)
}
// For string values, compare directly to catch even small text changes
if (typeof currentValue === 'string' && typeof deployedValue === 'string') {
if (currentValue !== deployedValue) {
return true
}
} else {
// For other types, use normalized comparison
const normalizedCurrentValue = normalizeValue(currentValue)
const normalizedDeployedValue = normalizeValue(deployedValue)
if (
normalizedStringify(normalizedCurrentValue) !==
normalizedStringify(normalizedDeployedValue)
) {
return true
}
}
// Compare type and other properties (excluding diff markers and value)
const currentSubBlockWithDiff = currentSubBlocks[subBlockId] as SubBlockWithDiffMarker
const deployedSubBlockWithDiff = deployedSubBlocks[subBlockId] as SubBlockWithDiffMarker
const { value: _cv, is_diff: _cd, ...currentSubBlockRest } = currentSubBlockWithDiff
const { value: _dv, is_diff: _dd, ...deployedSubBlockRest } = deployedSubBlockWithDiff
if (normalizedStringify(currentSubBlockRest) !== normalizedStringify(deployedSubBlockRest)) {
return true
}
}
const blocksEqual =
normalizedStringify(normalizedCurrentBlocks[blockId]) ===
normalizedStringify(normalizedDeployedBlocks[blockId])
if (!blocksEqual) {
return true
}
}
// 4. Compare loops
const currentLoops = currentState.loops || {}
const deployedLoops = deployedState.loops || {}
const currentLoopIds = Object.keys(currentLoops).sort()
const deployedLoopIds = Object.keys(deployedLoops).sort()
if (
currentLoopIds.length !== deployedLoopIds.length ||
normalizedStringify(currentLoopIds) !== normalizedStringify(deployedLoopIds)
) {
return true
}
for (const loopId of currentLoopIds) {
const normalizedCurrentLoop = normalizeValue(normalizeLoop(currentLoops[loopId]))
const normalizedDeployedLoop = normalizeValue(normalizeLoop(deployedLoops[loopId]))
if (
normalizedStringify(normalizedCurrentLoop) !== normalizedStringify(normalizedDeployedLoop)
) {
return true
}
}
// 5. Compare parallels
const currentParallels = currentState.parallels || {}
const deployedParallels = deployedState.parallels || {}
const currentParallelIds = Object.keys(currentParallels).sort()
const deployedParallelIds = Object.keys(deployedParallels).sort()
if (
currentParallelIds.length !== deployedParallelIds.length ||
normalizedStringify(currentParallelIds) !== normalizedStringify(deployedParallelIds)
) {
return true
}
for (const parallelId of currentParallelIds) {
const normalizedCurrentParallel = normalizeValue(
normalizeParallel(currentParallels[parallelId])
)
const normalizedDeployedParallel = normalizeValue(
normalizeParallel(deployedParallels[parallelId])
)
if (
normalizedStringify(normalizedCurrentParallel) !==
normalizedStringify(normalizedDeployedParallel)
) {
return true
}
}
// 6. Compare variables
const currentVariables = normalizeVariables(currentState.variables)
const deployedVariables = normalizeVariables(deployedState.variables)
const normalizedCurrentVars = normalizeValue(
Object.fromEntries(Object.entries(currentVariables).map(([id, v]) => [id, sanitizeVariable(v)]))
)
const normalizedDeployedVars = normalizeValue(
Object.fromEntries(
Object.entries(deployedVariables).map(([id, v]) => [id, sanitizeVariable(v)])
)
)
if (normalizedStringify(normalizedCurrentVars) !== normalizedStringify(normalizedDeployedVars)) {
return true
}
return false
return generateWorkflowDiffSummary(currentState, deployedState).hasChanges
}
/**
@@ -368,29 +129,83 @@ export function generateWorkflowDiffSummary(
const previousBlock = previousBlocks[id] as BlockWithDiffMarkers
const changes: FieldChange[] = []
if (currentBlock.name !== previousBlock.name) {
changes.push({ field: 'name', oldValue: previousBlock.name, newValue: currentBlock.name })
}
if (currentBlock.enabled !== previousBlock.enabled) {
changes.push({
field: 'enabled',
oldValue: previousBlock.enabled,
newValue: currentBlock.enabled,
})
// Compare block-level properties (excluding visual-only fields)
const {
position: _currentPos,
subBlocks: currentSubBlocks = {},
layout: _currentLayout,
height: _currentHeight,
outputs: _currentOutputs,
is_diff: _currentIsDiff,
field_diffs: _currentFieldDiffs,
...currentRest
} = currentBlock
const {
position: _previousPos,
subBlocks: previousSubBlocks = {},
layout: _previousLayout,
height: _previousHeight,
outputs: _previousOutputs,
is_diff: _previousIsDiff,
field_diffs: _previousFieldDiffs,
...previousRest
} = previousBlock
// Exclude width/height from data object (container dimensions from autolayout)
const { width: _cw, height: _ch, ...currentDataRest } = currentRest.data || {}
const { width: _pw, height: _ph, ...previousDataRest } = previousRest.data || {}
const normalizedCurrentBlock = { ...currentRest, data: currentDataRest, subBlocks: undefined }
const normalizedPreviousBlock = {
...previousRest,
data: previousDataRest,
subBlocks: undefined,
}
const currentSubBlocks = currentBlock.subBlocks || {}
const previousSubBlocks = previousBlock.subBlocks || {}
const allSubBlockIds = new Set([
...Object.keys(currentSubBlocks),
...Object.keys(previousSubBlocks),
])
if (
normalizedStringify(normalizedCurrentBlock) !== normalizedStringify(normalizedPreviousBlock)
) {
if (currentBlock.type !== previousBlock.type) {
changes.push({ field: 'type', oldValue: previousBlock.type, newValue: currentBlock.type })
}
if (currentBlock.name !== previousBlock.name) {
changes.push({ field: 'name', oldValue: previousBlock.name, newValue: currentBlock.name })
}
if (currentBlock.enabled !== previousBlock.enabled) {
changes.push({
field: 'enabled',
oldValue: previousBlock.enabled,
newValue: currentBlock.enabled,
})
}
// Check other block properties
const blockFields = ['horizontalHandles', 'advancedMode', 'triggerMode'] as const
for (const field of blockFields) {
if (currentBlock[field] !== previousBlock[field]) {
changes.push({
field,
oldValue: previousBlock[field],
newValue: currentBlock[field],
})
}
}
if (normalizedStringify(currentDataRest) !== normalizedStringify(previousDataRest)) {
changes.push({ field: 'data', oldValue: previousDataRest, newValue: currentDataRest })
}
}
// Compare subBlocks
const allSubBlockIds = [
...new Set([...Object.keys(currentSubBlocks), ...Object.keys(previousSubBlocks)]),
]
.filter(
(subId) =>
!TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(subId) && !SYSTEM_SUBBLOCK_IDS.includes(subId)
)
.sort()
for (const subId of allSubBlockIds) {
if (TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(subId) || SYSTEM_SUBBLOCK_IDS.includes(subId)) {
continue
}
const currentSub = currentSubBlocks[subId]
const previousSub = previousSubBlocks[subId]
@@ -403,11 +218,45 @@ export function generateWorkflowDiffSummary(
continue
}
const currentValue = normalizeValue(currentSub.value ?? null)
const previousValue = normalizeValue(previousSub.value ?? null)
// Compare subBlock values with sanitization
let currentValue: unknown = currentSub.value ?? null
let previousValue: unknown = previousSub.value ?? null
if (normalizedStringify(currentValue) !== normalizedStringify(previousValue)) {
changes.push({ field: subId, oldValue: previousSub.value, newValue: currentSub.value })
if (subId === 'tools' && Array.isArray(currentValue) && Array.isArray(previousValue)) {
currentValue = sanitizeTools(currentValue)
previousValue = sanitizeTools(previousValue)
}
if (subId === 'inputFormat' && Array.isArray(currentValue) && Array.isArray(previousValue)) {
currentValue = sanitizeInputFormat(currentValue)
previousValue = sanitizeInputFormat(previousValue)
}
// For string values, compare directly to catch even small text changes
if (typeof currentValue === 'string' && typeof previousValue === 'string') {
if (currentValue !== previousValue) {
changes.push({ field: subId, oldValue: previousSub.value, newValue: currentSub.value })
}
} else {
const normalizedCurrent = normalizeValue(currentValue)
const normalizedPrevious = normalizeValue(previousValue)
if (normalizedStringify(normalizedCurrent) !== normalizedStringify(normalizedPrevious)) {
changes.push({ field: subId, oldValue: previousSub.value, newValue: currentSub.value })
}
}
// Compare subBlock REST properties (type, id, etc. - excluding value and is_diff)
const currentSubWithDiff = currentSub as SubBlockWithDiffMarker
const previousSubWithDiff = previousSub as SubBlockWithDiffMarker
const { value: _cv, is_diff: _cd, ...currentSubRest } = currentSubWithDiff
const { value: _pv, is_diff: _pd, ...previousSubRest } = previousSubWithDiff
if (normalizedStringify(currentSubRest) !== normalizedStringify(previousSubRest)) {
changes.push({
field: `${subId}.properties`,
oldValue: previousSubRest,
newValue: currentSubRest,
})
}
}
@@ -433,22 +282,54 @@ export function generateWorkflowDiffSummary(
if (!currentEdgeSet.has(edge)) result.edgeChanges.removed++
}
const currentLoopIds = Object.keys(currentState.loops || {})
const previousLoopIds = Object.keys(previousState.loops || {})
result.loopChanges.added = currentLoopIds.filter((id) => !previousLoopIds.includes(id)).length
result.loopChanges.removed = previousLoopIds.filter((id) => !currentLoopIds.includes(id)).length
const currentLoops = currentState.loops || {}
const previousLoops = previousState.loops || {}
const currentLoopIds = Object.keys(currentLoops)
const previousLoopIds = Object.keys(previousLoops)
const currentParallelIds = Object.keys(currentState.parallels || {})
const previousParallelIds = Object.keys(previousState.parallels || {})
result.parallelChanges.added = currentParallelIds.filter(
(id) => !previousParallelIds.includes(id)
).length
result.parallelChanges.removed = previousParallelIds.filter(
(id) => !currentParallelIds.includes(id)
).length
for (const id of currentLoopIds) {
if (!previousLoopIds.includes(id)) {
result.loopChanges.added++
} else {
const normalizedCurrent = normalizeValue(normalizeLoop(currentLoops[id]))
const normalizedPrevious = normalizeValue(normalizeLoop(previousLoops[id]))
if (normalizedStringify(normalizedCurrent) !== normalizedStringify(normalizedPrevious)) {
result.loopChanges.added++
result.loopChanges.removed++
}
}
}
for (const id of previousLoopIds) {
if (!currentLoopIds.includes(id)) {
result.loopChanges.removed++
}
}
const currentVars = currentState.variables || {}
const previousVars = previousState.variables || {}
const currentParallels = currentState.parallels || {}
const previousParallels = previousState.parallels || {}
const currentParallelIds = Object.keys(currentParallels)
const previousParallelIds = Object.keys(previousParallels)
for (const id of currentParallelIds) {
if (!previousParallelIds.includes(id)) {
result.parallelChanges.added++
} else {
const normalizedCurrent = normalizeValue(normalizeParallel(currentParallels[id]))
const normalizedPrevious = normalizeValue(normalizeParallel(previousParallels[id]))
if (normalizedStringify(normalizedCurrent) !== normalizedStringify(normalizedPrevious)) {
result.parallelChanges.added++
result.parallelChanges.removed++
}
}
}
for (const id of previousParallelIds) {
if (!currentParallelIds.includes(id)) {
result.parallelChanges.removed++
}
}
const currentVars = normalizeVariables(currentState.variables)
const previousVars = normalizeVariables(previousState.variables)
const currentVarIds = Object.keys(currentVars)
const previousVarIds = Object.keys(previousVars)