1515 option-title-key =" description"
1616 class =" flex-grow"
1717 />
18+ <button
19+ v-if =" hasCompared"
20+ v-ff-tooltip:left =" 'Simple view hides changes to node positions'"
21+ class =" text-xs px-2 py-1 rounded border font-medium shrink-0"
22+ :class =" hidePositionChanges
23+ ? 'bg-blue-50 border-blue-300 text-blue-700'
24+ : 'border-gray-300 text-gray-600 hover:bg-gray-50'"
25+ @click =" hidePositionChanges = !hidePositionChanges"
26+ >
27+ Simple view
28+ </button >
1829 </div >
1930
2031 <!-- Loading state -->
2536 <!-- Navigation bar — shown after comparison -->
2637 <div v-if =" hasCompared && !loading" class =" flex items-center gap-2 px-3 py-1.5 border-b border-gray-200 bg-white shrink-0" >
2738 <div v-if =" currentGroup" class =" flex-1 flex items-center gap-2 min-w-0" >
28- <span class =" text-xs font-semibold px-1.5 py-0.5 rounded shrink-0" :class =" diffTypeBadgeClass(currentGroup.diffType)" >{{ currentGroup.diffType }}</span >
39+ <span class =" text-xs font-semibold px-1.5 py-0.5 rounded capitalize shrink-0" :class =" diffTypeBadgeClass(currentGroup.diffType)" >{{ currentGroup.diffType }}</span >
2940 <span class =" font-semibold text-sm text-gray-800 truncate" >{{ currentGroup.name }}</span >
30- <span class =" text-xs text-gray-400 shrink-0" >{{ currentGroup.type }}</span >
41+ <span class =" text-xs font-semibold text-gray-700 bg-gray-200 px-1.5 py-0.5 rounded shrink-0" >{{ currentGroup.type }}</span >
42+ <span v-if =" currentGroupCategoryLabel" class =" text-xs font-semibold text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded shrink-0" >{{ currentGroupCategoryLabel }}</span >
3143 <span v-if =" currentGroupTabMove" class =" text-xs text-amber-600 bg-amber-50 px-1.5 py-0.5 rounded shrink-0" >
3244 {{ currentGroupTabMove.from }} → {{ currentGroupTabMove.to }}
3345 </span >
@@ -103,7 +115,17 @@ import Alerts from '../../services/alerts.js'
103115
104116import SnapshotDiffChangePanel from ' ./SnapshotDiffChangePanel.vue'
105117
106- const COMPACT_PROPS = new Set ([' position' , ' z' , ' name' , ' label' , ' type' , ' wires' , ' disabled' ])
118+ // Props shown in compact (single-line) mode in the diff sidebar
119+ const COMPACT_PROPS = new Set ([' position' , ' z' , ' g' , ' name' , ' label' , ' type' , ' wires' , ' disabled' ])
120+ // Props to skip entirely in computeDiff — computed by Node-RED at render time, not user data
121+ const IGNORED_PROPS = new Set ([' id' , ' w' , ' h' ])
122+ // Props that represent node position — can be toggled off by the user
123+ const POSITION_PROPS = new Set ([' x' , ' y' ])
124+ // Props shown in the nav header — skip when expanding all props for added/deleted nodes
125+ const HEADER_PROPS = new Set ([' id' , ' type' , ' z' , ' name' , ' label' ])
126+ // Node categories that have no visual presence on the SVG canvas
127+ const CONFIG_CATEGORIES = new Set ([' global-config' , ' flow-config' ])
128+
107129const SIDEBAR_MIN_WIDTH = 200
108130const SIDEBAR_MAX_WIDTH = 800
109131
@@ -150,7 +172,8 @@ export default {
150172 sidebarWidth: 380 ,
151173 resizing: false ,
152174 resizeStartX: 0 ,
153- resizeStartWidth: 0
175+ resizeStartWidth: 0 ,
176+ hidePositionChanges: false
154177 }
155178 },
156179 computed: {
@@ -163,6 +186,7 @@ export default {
163186 groupedChanges () {
164187 const groups = new Map ()
165188 for (const change of this .changes ) {
189+ if (this .hidePositionChanges && change .prop && POSITION_PROPS .has (change .prop )) continue
166190 const key = change .item
167191 if (! groups .has (key)) {
168192 const node = this .nodeMap [key] || {}
@@ -173,6 +197,7 @@ export default {
173197 name: node .name || node .label || node .type || key,
174198 type: node .type || ' ' ,
175199 diffType: change .diffType ,
200+ category: this .nodeCategory (node),
176201 propChanges: []
177202 })
178203 }
@@ -181,15 +206,23 @@ export default {
181206 const node = this .nodeMap [key] || {}
182207 const isAdded = change .diffType === ' added'
183208 for (const [prop , val ] of Object .entries (node)) {
184- // Skip id (internal) and props already represented in the nav header:
185- // type (shown as badge), name/label (shown as node display name), z (tab)
186- if (prop === ' id ' || prop === ' type ' || prop === ' z ' || prop === ' name ' || prop === ' label ' ) continue
209+ if ( HEADER_PROPS . has (prop)) continue
210+ if ( IGNORED_PROPS . has (prop)) continue
211+ if (this . hidePositionChanges && POSITION_PROPS . has ( prop) ) continue
187212 group .propChanges .push ({ prop, value1: isAdded ? undefined : val, value2: isAdded ? val : undefined })
188213 }
189214 } else if (change .diffType === ' changed' ) {
190215 group .propChanges .push ({ prop: change .prop , value1: change .value1 , value2: change .value2 })
191216 }
192217 }
218+ // When hiding position changes, drop nodes that only had position diffs
219+ if (this .hidePositionChanges ) {
220+ for (const [key , group ] of groups) {
221+ if (group .diffType === ' changed' && group .propChanges .length === 0 ) {
222+ groups .delete (key)
223+ }
224+ }
225+ }
193226 return [... groups .values ()]
194227 },
195228 currentGroup () {
@@ -198,6 +231,12 @@ export default {
198231 currentGroupChanges () {
199232 return this .transformChanges (this .currentGroup ? .propChanges || [])
200233 },
234+ currentGroupCategoryLabel () {
235+ const cat = this .currentGroup ? .category
236+ if (cat === ' global-config' ) return ' Global Config'
237+ if (cat === ' flow-config' ) return ' Flow Config'
238+ return null
239+ },
201240 currentGroupTabMove () {
202241 // Returns { from, to } if a changed node moved between tabs, null otherwise.
203242 // Not shown for added/deleted nodes — the tab is part of their identity,
@@ -214,6 +253,13 @@ export default {
214253 watch: {
215254 compareSnapshot (val ) {
216255 if (val) this .renderComparison ()
256+ },
257+ hidePositionChanges () {
258+ // Clamp index — the list may have shrunk
259+ if (this .currentGroupIndex >= this .groupedChanges .length ) {
260+ this .currentGroupIndex = Math .max (0 , this .groupedChanges .length - 1 )
261+ }
262+ this .highlightCurrent ()
217263 }
218264 },
219265 mounted () {
@@ -257,9 +303,12 @@ export default {
257303 // Explicit scope prevents the renderer from using Tailwind utility
258304 // classes (e.g. flex-1) as CSS selectors, which would leak
259305 // svg sizing rules to the rest of the page.
260- scope: ' ff-flow-compare-view'
306+ scope: ' ff-flow-compare-view' ,
307+ persistentHighlight: true ,
308+ allChanges: true
261309 })
262310 this .rendererChanges = result? .changes || []
311+ this .clearRendererHighlight = result? .clearHighlight || (() => {})
263312 this .changes = this .computeDiff (compareFlow, this .flow )
264313 this .currentGroupIndex = 0
265314 this .hasCompared = true
@@ -281,16 +330,13 @@ export default {
281330 highlightCurrent () {
282331 const group = this .currentGroup
283332 if (! group) return
333+ this .clearRendererHighlight ()
334+ if (CONFIG_CATEGORIES .has (group .category )) return
284335 // Always pass layerNo explicitly from our own diffType so the renderer
285336 // shows the correct layer regardless of its internal change classification.
286- // The renderer's fallback (no layerNo) infers from rc.diffType, which can
287- // be unreliable for tab entries ('tab' diffType → defaults to layer 0 → 10%).
288337 const layerNo = group .diffType === ' added' ? 1 : group .diffType === ' deleted' ? 0 : - 1
289338 // Jump the slider directly to the target and dispatch one input event so
290- // the renderer updates layer opacities immediately. This bypasses the
291- // renderer's slow JS stepping loop (1 unit / 10 ms → up to 900 ms).
292- // The visual smoothness comes from a CSS transition on the SVG layers
293- // defined in the <style> block below.
339+ // the renderer updates layer opacities immediately.
294340 if (layerNo !== - 1 ) {
295341 const slider = this .$refs .compareViewer ? .querySelector (' .flow-compare-slider' )
296342 const target = layerNo === 1 ? 90 : 10
@@ -300,18 +346,14 @@ export default {
300346 }
301347 }
302348 if (group .type === ' tab' ) {
303- // The renderer's highlight() for a tab entry looks up the item as an
304- // SVG node, which fails (tabs are DOM elements, not SVG nodes). Instead,
305- // find any node that lives on this tab and highlight that — the renderer
306- // will click the tab and navigate there as a side effect.
307349 const proxy = this .rendererChanges .find (rc => rc .tab === group .nodeId && rc .highlight )
308350 if (proxy) proxy .highlight (layerNo)
309- return
310- }
311- // Highlight all renderer changes for this node — handles nodes that
312- // appear in multiple tabs (e.g. moved from one tab to another )
313- for ( const rc of this . rendererChanges ) {
314- if ( rc . item === group . nodeId && rc . highlight ) rc . highlight (layerNo)
351+ } else {
352+ for ( const rc of this . rendererChanges ) {
353+ if ( rc . item === group . nodeId && rc . highlight ) {
354+ rc . highlight (layerNo )
355+ }
356+ }
315357 }
316358 },
317359 diffTypeBadgeClass (diffType ) {
@@ -355,12 +397,16 @@ export default {
355397 if (c .prop === ' x' ) { xChange = c; continue }
356398 if (c .prop === ' y' ) { yChange = c; continue }
357399 if (c .prop === ' z' ) {
358- result .push ({ prop: ' z' , label: ' tab' , value1: this .resolveTabName (c .value1 ), value2: this .resolveTabName (c .value2 ) })
400+ result .push ({ prop: ' z' , label: ' Tab' , value1: this .resolveTabName (c .value1 ), value2: this .resolveTabName (c .value2 ) })
401+ continue
402+ }
403+ if (c .prop === ' g' ) {
404+ result .push ({ prop: ' g' , label: ' Group' , value1: this .resolveNodeName (c .value1 ), value2: this .resolveNodeName (c .value2 ) })
359405 continue
360406 }
361407 if (c .prop === ' disabled' ) {
362408 // Show as "enabled" / "disabled" rather than raw true/false
363- result .push ({ prop: ' disabled' , label: ' status ' , value1: this .resolveDisabled (c .value1 ), value2: this .resolveDisabled (c .value2 ) })
409+ result .push ({ prop: ' disabled' , label: ' Status ' , value1: this .resolveDisabled (c .value1 ), value2: this .resolveDisabled (c .value2 ) })
364410 continue
365411 }
366412 if (c .prop === ' wires' ) {
@@ -399,10 +445,26 @@ export default {
399445 const tab = this .nodeMap [tabId]
400446 return tab ? (tab .label || tabId) : tabId
401447 },
448+ resolveNodeName (nodeId ) {
449+ if (! nodeId) return nodeId
450+ const node = this .nodeMap [nodeId]
451+ return node ? (node .name || node .label || nodeId) : nodeId
452+ },
402453 resolveDisabled (val ) {
403454 if (val === undefined || val === null ) return undefined
404455 return val ? ' disabled' : ' enabled'
405456 },
457+ nodeCategory (node ) {
458+ if (! node || ! node .type ) return ' node'
459+ if (node .type === ' tab' ) return ' tab'
460+ if (node .type === ' group' ) return ' group'
461+ if (node .type === ' subflow' ) return ' subflow'
462+ // Config nodes have no canvas position — works for all node packages
463+ if (node .x === undefined && node .y === undefined ) {
464+ return node .z ? ' flow-config' : ' global-config'
465+ }
466+ return ' node'
467+ },
406468 computeDiff (flow1 , flow2 ) {
407469 const map1 = {}
408470 const map2 = {}
@@ -422,7 +484,7 @@ export default {
422484 const n1 = map1[id]
423485 const n2 = map2[id]
424486 for (const prop of new Set ([... Object .keys (n1), ... Object .keys (n2)])) {
425- if (prop === ' id ' ) continue
487+ if (IGNORED_PROPS . has ( prop) ) continue
426488 const v1 = n1[prop]
427489 const v2 = n2[prop]
428490 if (JSON .stringify (v1) !== JSON .stringify (v2)) {
0 commit comments