@emeryld/rrroutes-export 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@emeryld/rrroutes-export",
3
3
  "description": "Finalized leaves export helpers and CLI for RRRoutes",
4
- "version": "1.0.20",
4
+ "version": "1.0.22",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "main": "dist/index.cjs",
@@ -378,6 +378,22 @@
378
378
  gap: 4px;
379
379
  }
380
380
 
381
+ .changelog-commit-separator {
382
+ margin-top: 12px;
383
+ padding-top: 10px;
384
+ border-top: 1px solid var(--accent);
385
+ }
386
+
387
+ .changelog-commit-separator:first-child {
388
+ margin-top: 0;
389
+ }
390
+
391
+ .changelog-commit-label {
392
+ color: var(--muted);
393
+ font-weight: 700;
394
+ font-size: 12px;
395
+ }
396
+
381
397
  .changelog-diff-line {
382
398
  display: flex;
383
399
  flex-wrap: wrap;
@@ -657,6 +673,14 @@
657
673
  >
658
674
  Changelog
659
675
  </button>
676
+ <button
677
+ id="tabChangelogGrouped"
678
+ class="field-chip"
679
+ type="button"
680
+ aria-pressed="false"
681
+ >
682
+ Grouped Changelog
683
+ </button>
660
684
  </div>
661
685
  </div>
662
686
  </div>
@@ -845,6 +869,7 @@
845
869
  const VIEW_IDS = {
846
870
  leaves: 'leaves',
847
871
  changelog: 'changelog',
872
+ changelogGrouped: 'changelogGrouped',
848
873
  }
849
874
 
850
875
  const VIEWER_MODE = {
@@ -879,6 +904,12 @@
879
904
  CHANGELOG_TIMELINE_FIELDS.map((field) => field.id),
880
905
  ),
881
906
  },
907
+ [VIEW_IDS.changelogGrouped]: {
908
+ fieldDefs: CHANGELOG_TIMELINE_FIELDS,
909
+ defaultSelected: new Set(
910
+ CHANGELOG_TIMELINE_FIELDS.map((field) => field.id),
911
+ ),
912
+ },
882
913
  }
883
914
 
884
915
  const state = {
@@ -896,6 +927,9 @@
896
927
  [VIEW_IDS.changelog]: new Set(
897
928
  TAB_CONFIG[VIEW_IDS.changelog].defaultSelected,
898
929
  ),
930
+ [VIEW_IDS.changelogGrouped]: new Set(
931
+ TAB_CONFIG[VIEW_IDS.changelogGrouped].defaultSelected,
932
+ ),
899
933
  },
900
934
  }
901
935
 
@@ -921,6 +955,9 @@
921
955
  const controlsEl = document.querySelector('.controls')
922
956
  const tabLeavesBtn = document.getElementById('tabLeaves')
923
957
  const tabChangelogBtn = document.getElementById('tabChangelog')
958
+ const tabChangelogGroupedBtn = document.getElementById(
959
+ 'tabChangelogGrouped',
960
+ )
924
961
 
925
962
  const URL_PARAM_KEY = 'filters'
926
963
  let isHydratingFromUrl = false
@@ -1758,21 +1795,96 @@
1758
1795
  return `${type}${schemaFlags(Boolean(entry.nullable), Boolean(entry.optional))}`
1759
1796
  }
1760
1797
 
1798
+ function splitTypeOptions(type) {
1799
+ if (typeof type !== 'string' || !type.includes('|')) return []
1800
+ return type
1801
+ .split('|')
1802
+ .map((item) => item.trim())
1803
+ .filter(Boolean)
1804
+ }
1805
+
1806
+ function hasDefinedLiteral(entry) {
1807
+ return (
1808
+ entry &&
1809
+ typeof entry === 'object' &&
1810
+ Object.prototype.hasOwnProperty.call(entry, 'literal') &&
1811
+ entry.literal !== undefined
1812
+ )
1813
+ }
1814
+
1815
+ function reduceEnumTypeDiff(beforeEntry, afterEntry) {
1816
+ if (!beforeEntry || !afterEntry) return null
1817
+ if (hasDefinedLiteral(beforeEntry) || hasDefinedLiteral(afterEntry)) {
1818
+ return null
1819
+ }
1820
+
1821
+ const beforeOptions = splitTypeOptions(beforeEntry.type)
1822
+ const afterOptions = splitTypeOptions(afterEntry.type)
1823
+ if (beforeOptions.length === 0 || afterOptions.length === 0) return null
1824
+
1825
+ const afterSet = new Set(afterOptions)
1826
+ const beforeSet = new Set(beforeOptions)
1827
+ const beforeOnly = beforeOptions.filter((option) => !afterSet.has(option))
1828
+ const afterOnly = afterOptions.filter((option) => !beforeSet.has(option))
1829
+ if (beforeOnly.length === 0 && afterOnly.length === 0) return null
1830
+
1831
+ const beforeType = `${beforeOnly.join('|') || 'none'}${schemaFlags(
1832
+ Boolean(beforeEntry.nullable),
1833
+ Boolean(beforeEntry.optional),
1834
+ )}`
1835
+ const afterType = `${afterOnly.join('|') || 'none'}${schemaFlags(
1836
+ Boolean(afterEntry.nullable),
1837
+ Boolean(afterEntry.optional),
1838
+ )}`
1839
+ return { beforeType, afterType }
1840
+ }
1841
+
1761
1842
  function formatPathLabel(path) {
1762
1843
  const basePath = typeof path === 'string' ? path : ''
1763
1844
  return basePath
1764
1845
  }
1765
1846
 
1766
- function parseSignatureTypeLabel(signature) {
1847
+ function parseSignatureEntry(signature) {
1767
1848
  if (typeof signature !== 'string') return null
1768
1849
  const typeMatch = signature.match(/(?:^|,\s*)type=([^,]+)/)
1769
- const nullableMatch = signature.match(/(?:^|,\s*)nullable=(true|false)/)
1770
- const optionalMatch = signature.match(/(?:^|,\s*)optional=(true|false)/)
1771
- if (!typeMatch) return null
1772
- const type = typeMatch[1]
1773
- const nullable = nullableMatch?.[1] === 'true'
1774
- const optional = optionalMatch?.[1] === 'true'
1775
- return `${type}${schemaFlags(nullable, optional)}`
1850
+ if (typeMatch) {
1851
+ const nullableMatch = signature.match(
1852
+ /(?:^|,\s*)nullable=(true|false)/,
1853
+ )
1854
+ const optionalMatch = signature.match(
1855
+ /(?:^|,\s*)optional=(true|false)/,
1856
+ )
1857
+ return {
1858
+ type: typeMatch[1],
1859
+ nullable: nullableMatch?.[1] === 'true',
1860
+ optional: optionalMatch?.[1] === 'true',
1861
+ literal: undefined,
1862
+ }
1863
+ }
1864
+ try {
1865
+ const parsed = JSON.parse(signature)
1866
+ if (!parsed || typeof parsed !== 'object') return null
1867
+ if (typeof parsed.type !== 'string') return null
1868
+ return {
1869
+ type: parsed.type,
1870
+ nullable: Boolean(parsed.nullable),
1871
+ optional: Boolean(parsed.optional),
1872
+ literal: Object.prototype.hasOwnProperty.call(parsed, 'literal')
1873
+ ? parsed.literal
1874
+ : undefined,
1875
+ }
1876
+ } catch {
1877
+ return null
1878
+ }
1879
+ }
1880
+
1881
+ function parseSignatureTypeLabel(signature) {
1882
+ const entry = parseSignatureEntry(signature)
1883
+ if (!entry) return null
1884
+ return `${entry.type}${schemaFlags(
1885
+ Boolean(entry.nullable),
1886
+ Boolean(entry.optional),
1887
+ )}`
1776
1888
  }
1777
1889
 
1778
1890
  function formatSourceTypeLabel(signatures) {
@@ -1784,6 +1896,18 @@
1784
1896
  return labels.join(' | ')
1785
1897
  }
1786
1898
 
1899
+ function formatSourceChangedTypes(beforeSignatures, afterSignatures) {
1900
+ if (!Array.isArray(beforeSignatures) || !Array.isArray(afterSignatures)) {
1901
+ return null
1902
+ }
1903
+ if (beforeSignatures.length !== 1 || afterSignatures.length !== 1) {
1904
+ return null
1905
+ }
1906
+ const beforeEntry = parseSignatureEntry(beforeSignatures[0])
1907
+ const afterEntry = parseSignatureEntry(afterSignatures[0])
1908
+ return reduceEnumTypeDiff(beforeEntry, afterEntry)
1909
+ }
1910
+
1787
1911
  function schemaDiffOperation(op) {
1788
1912
  if (op === 'added') {
1789
1913
  return { symbol: '+', word: 'added', className: 'added' }
@@ -1839,8 +1963,9 @@
1839
1963
  `removed ${location}: ${JSON.stringify(delta.before || [])}`,
1840
1964
  )
1841
1965
  }
1842
- const beforeType = formatSourceTypeLabel(delta.before)
1843
- const afterType = formatSourceTypeLabel(delta.after)
1966
+ const reducedTypes = formatSourceChangedTypes(delta.before, delta.after)
1967
+ const beforeType = reducedTypes?.beforeType || formatSourceTypeLabel(delta.before)
1968
+ const afterType = reducedTypes?.afterType || formatSourceTypeLabel(delta.after)
1844
1969
  if (beforeType || afterType) {
1845
1970
  return buildSchemaDiffLine(
1846
1971
  delta.op,
@@ -1871,8 +1996,11 @@
1871
1996
  `removed ${delta.path}: ${formatRouteSchemaEntry(delta.before)}`,
1872
1997
  )
1873
1998
  }
1874
- const beforeType = formatEntryTypeLabel(delta.before) || 'unknown'
1875
- const afterType = formatEntryTypeLabel(delta.after) || 'unknown'
1999
+ const reducedTypes = reduceEnumTypeDiff(delta.before, delta.after)
2000
+ const beforeType =
2001
+ reducedTypes?.beforeType || formatEntryTypeLabel(delta.before) || 'unknown'
2002
+ const afterType =
2003
+ reducedTypes?.afterType || formatEntryTypeLabel(delta.after) || 'unknown'
1876
2004
  const pathLabel =
1877
2005
  formatPathLabel(delta.path) || delta.path
1878
2006
  return buildSchemaDiffLine(delta.op, beforeType, pathLabel, afterType)
@@ -1899,10 +2027,14 @@
1899
2027
  function eventVisualByType(type) {
1900
2028
  switch (type) {
1901
2029
  case 'route_added':
1902
- return { actionText: 'added', actionClass: 'event-action-added', icon: 'route' }
2030
+ return {
2031
+ actionText: 'added route',
2032
+ actionClass: 'event-action-added',
2033
+ icon: 'route',
2034
+ }
1903
2035
  case 'route_removed':
1904
2036
  return {
1905
- actionText: 'removed',
2037
+ actionText: 'removed route',
1906
2038
  actionClass: 'event-action-removed',
1907
2039
  icon: 'route',
1908
2040
  }
@@ -1919,18 +2051,22 @@
1919
2051
  icon: 'config',
1920
2052
  }
1921
2053
  case 'source_object_added':
1922
- return { actionText: 'added', actionClass: 'event-action-added', icon: 'source' }
2054
+ return {
2055
+ actionText: 'added source object',
2056
+ actionClass: 'event-action-added',
2057
+ icon: 'source',
2058
+ }
1923
2059
  case 'source_object_removed':
1924
2060
  return {
1925
- actionText: 'removed',
2061
+ actionText: 'removed source object',
1926
2062
  actionClass: 'event-action-removed',
1927
2063
  icon: 'source',
1928
2064
  }
1929
2065
  case 'source_schema_changed':
1930
2066
  return {
1931
- actionText: 'modified source object',
2067
+ actionText: 'modified schema',
1932
2068
  actionClass: 'event-action-modified',
1933
- icon: 'source',
2069
+ icon: 'schema',
1934
2070
  }
1935
2071
  default:
1936
2072
  return {
@@ -2022,6 +2158,18 @@
2022
2158
  ),
2023
2159
  )
2024
2160
  }
2161
+ if (
2162
+ Number.isInteger(options?.item?.dedupedRouteCount) &&
2163
+ options.item.dedupedRouteCount > 0
2164
+ ) {
2165
+ card.appendChild(
2166
+ el(
2167
+ 'div',
2168
+ 'meta',
2169
+ `deduped route schema events: ${options.item.dedupedRouteCount}`,
2170
+ ),
2171
+ )
2172
+ }
2025
2173
  return card
2026
2174
  }
2027
2175
 
@@ -2038,6 +2186,34 @@
2038
2186
  .join(' - ')
2039
2187
  }
2040
2188
 
2189
+ function renderCommitSeparator(commitSha, commitBySha) {
2190
+ const box = el('div', 'changelog-commit-separator')
2191
+ const commit = commitBySha.get(commitSha)
2192
+ const commitDateTime = formatCommitDateTime(commit?.authorDate)
2193
+ const label = `commit ${String(commitSha || '').slice(0, 12)}`
2194
+ const meta = [commitDateTime, commit?.authorName || null, commit?.subject || null]
2195
+ .filter(Boolean)
2196
+ .join(' - ')
2197
+ box.appendChild(el('div', 'changelog-commit-label mono', label))
2198
+ if (meta) {
2199
+ box.appendChild(el('div', 'meta', meta))
2200
+ }
2201
+ return box
2202
+ }
2203
+
2204
+ function renderChangelogWithCommitSeparators(items, commitBySha, renderItem) {
2205
+ if (!Array.isArray(items) || items.length === 0) return
2206
+ let previousCommitSha = null
2207
+ items.forEach((item) => {
2208
+ const currentCommitSha = item?.event?.commitSha || null
2209
+ if (currentCommitSha !== previousCommitSha) {
2210
+ resultsEl.appendChild(renderCommitSeparator(currentCommitSha, commitBySha))
2211
+ previousCommitSha = currentCommitSha
2212
+ }
2213
+ resultsEl.appendChild(renderItem(item))
2214
+ })
2215
+ }
2216
+
2041
2217
  function renderChangelogEventLines(item, commitBySha) {
2042
2218
  const event = item.event
2043
2219
  const row = el('div', 'changelog-line-item')
@@ -2072,6 +2248,18 @@
2072
2248
  ),
2073
2249
  )
2074
2250
  }
2251
+ if (
2252
+ Number.isInteger(item?.dedupedRouteCount) &&
2253
+ item.dedupedRouteCount > 0
2254
+ ) {
2255
+ row.appendChild(
2256
+ el(
2257
+ 'div',
2258
+ 'meta mono',
2259
+ `deduped route schema events: ${item.dedupedRouteCount}`,
2260
+ ),
2261
+ )
2262
+ }
2075
2263
 
2076
2264
  return row
2077
2265
  }
@@ -2103,6 +2291,60 @@
2103
2291
  )
2104
2292
  }
2105
2293
 
2294
+ function schemaEntrySignature(entry) {
2295
+ if (!entry || typeof entry !== 'object') return ''
2296
+ return JSON.stringify({
2297
+ type: typeof entry.type === 'string' ? entry.type : 'unknown',
2298
+ nullable: Boolean(entry.nullable),
2299
+ optional: Boolean(entry.optional),
2300
+ literal: entry.literal,
2301
+ })
2302
+ }
2303
+
2304
+ function normalizeSignatureToken(signature) {
2305
+ if (typeof signature !== 'string') return ''
2306
+ const parsed = parseSignatureEntry(signature)
2307
+ if (!parsed) return signature
2308
+ return schemaEntrySignature(parsed)
2309
+ }
2310
+
2311
+ function normalizedSchemaDeltaParts(delta) {
2312
+ if (!delta || typeof delta !== 'object') return null
2313
+ const op = String(delta.op || 'changed')
2314
+ if ('path' in delta) {
2315
+ const location = String(delta.path || '')
2316
+ const before = delta.before ? [schemaEntrySignature(delta.before)] : []
2317
+ const after = delta.after ? [schemaEntrySignature(delta.after)] : []
2318
+ return { op, location, before, after }
2319
+ }
2320
+ if ('section' in delta) {
2321
+ const location = `${delta.section}.${delta.path}`
2322
+ const before = Array.isArray(delta.before)
2323
+ ? delta.before.map((item) => normalizeSignatureToken(item)).filter(Boolean)
2324
+ : []
2325
+ const after = Array.isArray(delta.after)
2326
+ ? delta.after.map((item) => normalizeSignatureToken(item)).filter(Boolean)
2327
+ : []
2328
+ before.sort()
2329
+ after.sort()
2330
+ return { op, location, before, after }
2331
+ }
2332
+ return null
2333
+ }
2334
+
2335
+ function schemaDiffFingerprint(schemaDiff) {
2336
+ if (!Array.isArray(schemaDiff) || schemaDiff.length === 0) return ''
2337
+ const lines = schemaDiff
2338
+ .map((delta) => normalizedSchemaDeltaParts(delta))
2339
+ .filter(Boolean)
2340
+ .map(
2341
+ (part) =>
2342
+ `${part.op}|${part.location}|${part.before.join('&')}|${part.after.join('&')}`,
2343
+ )
2344
+ .sort()
2345
+ return lines.join('||')
2346
+ }
2347
+
2106
2348
  function buildChronologicalEvents(payload) {
2107
2349
  if (!payload) return []
2108
2350
  const commitBySha = new Map(
@@ -2113,9 +2355,43 @@
2113
2355
  )
2114
2356
  const rows = []
2115
2357
  let sequence = 0
2358
+ const sourceSchemaCoverage = new Map()
2359
+
2360
+ Object.entries(payload.bySourceObject || {}).forEach(
2361
+ ([sourceObjectId, events]) => {
2362
+ ;(events || []).forEach((event) => {
2363
+ if (event?.type !== 'source_schema_changed') return
2364
+ const fingerprint = schemaDiffFingerprint(event.schemaDiff)
2365
+ if (!fingerprint) return
2366
+ const key = `${event.commitSha}|${fingerprint}`
2367
+ let coverage = sourceSchemaCoverage.get(key)
2368
+ if (!coverage) {
2369
+ coverage = {
2370
+ sourceObjectIds: new Set(),
2371
+ impactedRoutes: new Set(),
2372
+ dedupedRouteKeys: new Set(),
2373
+ }
2374
+ sourceSchemaCoverage.set(key, coverage)
2375
+ }
2376
+ coverage.sourceObjectIds.add(sourceObjectId)
2377
+ ;(event.impactedRoutes || []).forEach((routeKey) => {
2378
+ coverage.impactedRoutes.add(routeKey)
2379
+ })
2380
+ })
2381
+ },
2382
+ )
2116
2383
 
2117
2384
  Object.entries(payload.byRoute || {}).forEach(([routeKey, events]) => {
2118
2385
  ;(events || []).forEach((event) => {
2386
+ if (event?.type === 'schema_changed') {
2387
+ const fingerprint = schemaDiffFingerprint(event.schemaDiff)
2388
+ const key = `${event.commitSha}|${fingerprint}`
2389
+ const coverage = sourceSchemaCoverage.get(key)
2390
+ if (coverage && coverage.impactedRoutes.has(routeKey)) {
2391
+ coverage.dedupedRouteKeys.add(routeKey)
2392
+ return
2393
+ }
2394
+ }
2119
2395
  rows.push({
2120
2396
  eventKind: 'route',
2121
2397
  routeKey,
@@ -2131,10 +2407,21 @@
2131
2407
  Object.entries(payload.bySourceObject || {}).forEach(
2132
2408
  ([sourceObjectId, events]) => {
2133
2409
  ;(events || []).forEach((event) => {
2410
+ const fingerprint =
2411
+ event?.type === 'source_schema_changed'
2412
+ ? schemaDiffFingerprint(event.schemaDiff)
2413
+ : ''
2414
+ const coverage = sourceSchemaCoverage.get(
2415
+ `${event.commitSha}|${fingerprint}`,
2416
+ )
2134
2417
  rows.push({
2135
2418
  eventKind: 'source',
2136
2419
  sourceObjectId,
2137
2420
  displayName: event.displayName || sourceObjectId,
2421
+ dedupedRouteCount:
2422
+ event?.type === 'source_schema_changed'
2423
+ ? coverage?.dedupedRouteKeys.size || 0
2424
+ : 0,
2138
2425
  event,
2139
2426
  commit: commitBySha.get(event.commitSha),
2140
2427
  commitSort: commitTimestamp(commitBySha.get(event.commitSha)),
@@ -2158,7 +2445,10 @@
2158
2445
  function changelogItemsByTab(tabId) {
2159
2446
  const payload = state.changelogPayload
2160
2447
  if (!payload) return []
2161
- if (tabId === VIEW_IDS.changelog) {
2448
+ if (
2449
+ tabId === VIEW_IDS.changelog ||
2450
+ tabId === VIEW_IDS.changelogGrouped
2451
+ ) {
2162
2452
  return buildChronologicalEvents(payload)
2163
2453
  }
2164
2454
  return []
@@ -2187,6 +2477,10 @@
2187
2477
  function renderTabButtons() {
2188
2478
  setPressed(tabLeavesBtn, state.activeTab === VIEW_IDS.leaves)
2189
2479
  setPressed(tabChangelogBtn, state.activeTab === VIEW_IDS.changelog)
2480
+ setPressed(
2481
+ tabChangelogGroupedBtn,
2482
+ state.activeTab === VIEW_IDS.changelogGrouped,
2483
+ )
2190
2484
  tabLeavesBtn.style.display = state.availableTabs.has(VIEW_IDS.leaves)
2191
2485
  ? ''
2192
2486
  : 'none'
@@ -2195,6 +2489,11 @@
2195
2489
  )
2196
2490
  ? ''
2197
2491
  : 'none'
2492
+ tabChangelogGroupedBtn.style.display = state.availableTabs.has(
2493
+ VIEW_IDS.changelogGrouped,
2494
+ )
2495
+ ? ''
2496
+ : 'none'
2198
2497
  const isLeavesTab = state.activeTab === VIEW_IDS.leaves
2199
2498
  coreFieldsBtn.style.display = isLeavesTab ? '' : 'none'
2200
2499
  schemasOnlyFieldsBtn.style.display = isLeavesTab ? '' : 'none'
@@ -2247,30 +2546,26 @@
2247
2546
  commit,
2248
2547
  ]),
2249
2548
  )
2250
- if (state.viewerMode === VIEWER_MODE.changelog) {
2251
- renderCollection(
2252
- filtered,
2253
- (item) => renderChangelogEventLines(item, commitBySha),
2254
- 'No matches.',
2549
+ if (filtered.length === 0) {
2550
+ resultsEl.appendChild(el('div', 'empty', 'No matches.'))
2551
+ } else if (state.activeTab === VIEW_IDS.changelog) {
2552
+ renderChangelogWithCommitSeparators(filtered, commitBySha, (item) =>
2553
+ renderChangelogEventLines(item, commitBySha),
2255
2554
  )
2256
2555
  } else {
2257
- renderCollection(
2258
- filtered,
2259
- (item) => {
2260
- const details = el('details', 'leaf')
2261
- const summary = el('summary')
2262
- summary.appendChild(renderEventTitle(item))
2263
- details.appendChild(summary)
2264
- details.appendChild(
2265
- renderEventCard(item.event, commitBySha, {
2266
- showTitle: false,
2267
- item,
2268
- }),
2269
- )
2270
- return details
2271
- },
2272
- 'No matches.',
2273
- )
2556
+ renderChangelogWithCommitSeparators(filtered, commitBySha, (item) => {
2557
+ const details = el('details', 'leaf')
2558
+ const summary = el('summary')
2559
+ summary.appendChild(renderEventTitle(item))
2560
+ details.appendChild(summary)
2561
+ details.appendChild(
2562
+ renderEventCard(item.event, commitBySha, {
2563
+ showTitle: false,
2564
+ item,
2565
+ }),
2566
+ )
2567
+ return details
2568
+ })
2274
2569
  }
2275
2570
  }
2276
2571
  syncFilterStateToUrl()
@@ -2296,7 +2591,8 @@
2296
2591
  {
2297
2592
  id: 'tabFields',
2298
2593
  label:
2299
- state.activeTab === VIEW_IDS.changelog
2594
+ state.activeTab === VIEW_IDS.changelog ||
2595
+ state.activeTab === VIEW_IDS.changelogGrouped
2300
2596
  ? 'Changelog fields'
2301
2597
  : 'Source fields',
2302
2598
  fieldIds: fieldDefs.map((field) => field.id),
@@ -2484,7 +2780,8 @@
2484
2780
  isHydratingFromUrl = true
2485
2781
  if (
2486
2782
  parsed.activeTab === VIEW_IDS.leaves ||
2487
- parsed.activeTab === VIEW_IDS.changelog
2783
+ parsed.activeTab === VIEW_IDS.changelog ||
2784
+ parsed.activeTab === VIEW_IDS.changelogGrouped
2488
2785
  ) {
2489
2786
  state.activeTab = parsed.activeTab
2490
2787
  }
@@ -2573,12 +2870,19 @@
2573
2870
  }
2574
2871
  if (state.changelogPayload) {
2575
2872
  state.availableTabs.add(VIEW_IDS.changelog)
2873
+ if (state.viewerMode === VIEWER_MODE.bundle) {
2874
+ state.availableTabs.add(VIEW_IDS.changelogGrouped)
2875
+ }
2576
2876
  }
2577
2877
 
2578
2878
  if (!state.availableTabs.has(state.activeTab)) {
2579
- state.activeTab = state.availableTabs.has(VIEW_IDS.leaves)
2580
- ? VIEW_IDS.leaves
2581
- : VIEW_IDS.changelog
2879
+ if (state.availableTabs.has(VIEW_IDS.leaves)) {
2880
+ state.activeTab = VIEW_IDS.leaves
2881
+ } else if (state.availableTabs.has(VIEW_IDS.changelog)) {
2882
+ state.activeTab = VIEW_IDS.changelog
2883
+ } else {
2884
+ state.activeTab = VIEW_IDS.changelogGrouped
2885
+ }
2582
2886
  }
2583
2887
 
2584
2888
  if (state.leavesPayload && state.changelogPayload) {
@@ -2641,6 +2945,9 @@
2641
2945
  tabChangelogBtn.addEventListener('click', () =>
2642
2946
  setActiveTab(VIEW_IDS.changelog),
2643
2947
  )
2948
+ tabChangelogGroupedBtn.addEventListener('click', () =>
2949
+ setActiveTab(VIEW_IDS.changelogGrouped),
2950
+ )
2644
2951
 
2645
2952
  selectAllFieldsBtn.addEventListener('click', () =>
2646
2953
  setFieldSelection(new Set(currentFieldDefs().map((field) => field.id))),