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,22 @@ 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 (this .hidePositionChanges && POSITION_PROPS .has (prop)) continue
187211 group .propChanges .push ({ prop, value1: isAdded ? undefined : val, value2: isAdded ? val : undefined })
188212 }
189213 } else if (change .diffType === ' changed' ) {
190214 group .propChanges .push ({ prop: change .prop , value1: change .value1 , value2: change .value2 })
191215 }
192216 }
217+ // When hiding position changes, drop nodes that only had position diffs
218+ if (this .hidePositionChanges ) {
219+ for (const [key , group ] of groups) {
220+ if (group .diffType === ' changed' && group .propChanges .length === 0 ) {
221+ groups .delete (key)
222+ }
223+ }
224+ }
193225 return [... groups .values ()]
194226 },
195227 currentGroup () {
@@ -198,6 +230,12 @@ export default {
198230 currentGroupChanges () {
199231 return this .transformChanges (this .currentGroup ? .propChanges || [])
200232 },
233+ currentGroupCategoryLabel () {
234+ const cat = this .currentGroup ? .category
235+ if (cat === ' global-config' ) return ' Global Config'
236+ if (cat === ' flow-config' ) return ' Flow Config'
237+ return null
238+ },
201239 currentGroupTabMove () {
202240 // Returns { from, to } if a changed node moved between tabs, null otherwise.
203241 // Not shown for added/deleted nodes — the tab is part of their identity,
@@ -214,6 +252,13 @@ export default {
214252 watch: {
215253 compareSnapshot (val ) {
216254 if (val) this .renderComparison ()
255+ },
256+ hidePositionChanges () {
257+ // Clamp index — the list may have shrunk
258+ if (this .currentGroupIndex >= this .groupedChanges .length ) {
259+ this .currentGroupIndex = Math .max (0 , this .groupedChanges .length - 1 )
260+ }
261+ this .highlightCurrent ()
217262 }
218263 },
219264 mounted () {
@@ -257,9 +302,12 @@ export default {
257302 // Explicit scope prevents the renderer from using Tailwind utility
258303 // classes (e.g. flex-1) as CSS selectors, which would leak
259304 // svg sizing rules to the rest of the page.
260- scope: ' ff-flow-compare-view'
305+ scope: ' ff-flow-compare-view' ,
306+ persistentHighlight: true ,
307+ allChanges: true
261308 })
262309 this .rendererChanges = result? .changes || []
310+ this .clearRendererHighlight = result? .clearHighlight || (() => {})
263311 this .changes = this .computeDiff (compareFlow, this .flow )
264312 this .currentGroupIndex = 0
265313 this .hasCompared = true
@@ -281,16 +329,13 @@ export default {
281329 highlightCurrent () {
282330 const group = this .currentGroup
283331 if (! group) return
332+ this .clearRendererHighlight ()
333+ if (CONFIG_CATEGORIES .has (group .category )) return
284334 // Always pass layerNo explicitly from our own diffType so the renderer
285335 // 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%).
288336 const layerNo = group .diffType === ' added' ? 1 : group .diffType === ' deleted' ? 0 : - 1
289337 // 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.
338+ // the renderer updates layer opacities immediately.
294339 if (layerNo !== - 1 ) {
295340 const slider = this .$refs .compareViewer ? .querySelector (' .flow-compare-slider' )
296341 const target = layerNo === 1 ? 90 : 10
@@ -300,18 +345,14 @@ export default {
300345 }
301346 }
302347 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.
307348 const proxy = this .rendererChanges .find (rc => rc .tab === group .nodeId && rc .highlight )
308349 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)
350+ } else {
351+ for ( const rc of this . rendererChanges ) {
352+ if ( rc . item === group . nodeId && rc . highlight ) {
353+ rc . highlight (layerNo )
354+ }
355+ }
315356 }
316357 },
317358 diffTypeBadgeClass (diffType ) {
@@ -355,12 +396,16 @@ export default {
355396 if (c .prop === ' x' ) { xChange = c; continue }
356397 if (c .prop === ' y' ) { yChange = c; continue }
357398 if (c .prop === ' z' ) {
358- result .push ({ prop: ' z' , label: ' tab' , value1: this .resolveTabName (c .value1 ), value2: this .resolveTabName (c .value2 ) })
399+ result .push ({ prop: ' z' , label: ' Tab' , value1: this .resolveTabName (c .value1 ), value2: this .resolveTabName (c .value2 ) })
400+ continue
401+ }
402+ if (c .prop === ' g' ) {
403+ result .push ({ prop: ' g' , label: ' Group' , value1: this .resolveNodeName (c .value1 ), value2: this .resolveNodeName (c .value2 ) })
359404 continue
360405 }
361406 if (c .prop === ' disabled' ) {
362407 // 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 ) })
408+ result .push ({ prop: ' disabled' , label: ' Status ' , value1: this .resolveDisabled (c .value1 ), value2: this .resolveDisabled (c .value2 ) })
364409 continue
365410 }
366411 if (c .prop === ' wires' ) {
@@ -399,10 +444,26 @@ export default {
399444 const tab = this .nodeMap [tabId]
400445 return tab ? (tab .label || tabId) : tabId
401446 },
447+ resolveNodeName (nodeId ) {
448+ if (! nodeId) return nodeId
449+ const node = this .nodeMap [nodeId]
450+ return node ? (node .name || node .label || nodeId) : nodeId
451+ },
402452 resolveDisabled (val ) {
403453 if (val === undefined || val === null ) return undefined
404454 return val ? ' disabled' : ' enabled'
405455 },
456+ nodeCategory (node ) {
457+ if (! node || ! node .type ) return ' node'
458+ if (node .type === ' tab' ) return ' tab'
459+ if (node .type === ' group' ) return ' group'
460+ if (node .type === ' subflow' ) return ' subflow'
461+ // Config nodes have no canvas position — works for all node packages
462+ if (node .x === undefined && node .y === undefined ) {
463+ return node .z ? ' flow-config' : ' global-config'
464+ }
465+ return ' node'
466+ },
406467 computeDiff (flow1 , flow2 ) {
407468 const map1 = {}
408469 const map2 = {}
@@ -422,7 +483,7 @@ export default {
422483 const n1 = map1[id]
423484 const n2 = map2[id]
424485 for (const prop of new Set ([... Object .keys (n1), ... Object .keys (n2)])) {
425- if (prop === ' id ' ) continue
486+ if (IGNORED_PROPS . has ( prop) ) continue
426487 const v1 = n1[prop]
427488 const v2 = n2[prop]
428489 if (JSON .stringify (v1) !== JSON .stringify (v2)) {
0 commit comments