@emeryld/rrroutes-export 1.0.13 → 1.0.14

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.
@@ -529,6 +529,23 @@
529
529
  </div>
530
530
  </details>
531
531
 
532
+ <div class="scope-row">
533
+ <div class="scope-group">
534
+ <div class="scope-group-title">Views</div>
535
+ <div class="scope-group-chips">
536
+ <button id="tabLeaves" class="field-chip" type="button" aria-pressed="true">
537
+ Leaves
538
+ </button>
539
+ <button id="tabChangelogRoute" class="field-chip" type="button" aria-pressed="false">
540
+ Changelog / By Route
541
+ </button>
542
+ <button id="tabChangelogSource" class="field-chip" type="button" aria-pressed="false">
543
+ Changelog / By Source
544
+ </button>
545
+ </div>
546
+ </div>
547
+ </div>
548
+
532
549
  <div class="active-filters-row">
533
550
  <div class="active-filters-title">Active filters</div>
534
551
  <div id="activeFilterChips" class="chips filters"></div>
@@ -612,6 +629,68 @@
612
629
  },
613
630
  ]
614
631
 
632
+ const CHANGELOG_ROUTE_FIELDS = [
633
+ {
634
+ id: 'routeKey',
635
+ label: 'route',
636
+ get: (item) => [item.routeKey],
637
+ },
638
+ {
639
+ id: 'eventType',
640
+ label: 'event type',
641
+ get: (item) => item.events.map((event) => event.type),
642
+ },
643
+ {
644
+ id: 'schemaDiff',
645
+ label: 'schema diff',
646
+ get: (item) =>
647
+ item.events.flatMap((event) =>
648
+ Array.isArray(event.schemaDiff)
649
+ ? event.schemaDiff.map((delta) => JSON.stringify(delta))
650
+ : [],
651
+ ),
652
+ },
653
+ {
654
+ id: 'cfgDiff',
655
+ label: 'cfg diff',
656
+ get: (item) =>
657
+ item.events.flatMap((event) =>
658
+ Array.isArray(event.cfgDiff)
659
+ ? event.cfgDiff.map((delta) => JSON.stringify(delta))
660
+ : [],
661
+ ),
662
+ },
663
+ ]
664
+
665
+ const CHANGELOG_SOURCE_FIELDS = [
666
+ {
667
+ id: 'sourceObject',
668
+ label: 'source object',
669
+ get: (item) => [item.sourceId, item.displayName],
670
+ },
671
+ {
672
+ id: 'eventType',
673
+ label: 'event type',
674
+ get: (item) => item.events.map((event) => event.type),
675
+ },
676
+ {
677
+ id: 'impactedRoutes',
678
+ label: 'impacted routes',
679
+ get: (item) =>
680
+ item.events.flatMap((event) => (Array.isArray(event.impactedRoutes) ? event.impactedRoutes : [])),
681
+ },
682
+ {
683
+ id: 'schemaDiff',
684
+ label: 'schema diff',
685
+ get: (item) =>
686
+ item.events.flatMap((event) =>
687
+ Array.isArray(event.schemaDiff)
688
+ ? event.schemaDiff.map((delta) => JSON.stringify(delta))
689
+ : [],
690
+ ),
691
+ },
692
+ ]
693
+
615
694
  const SCHEMA_SECTIONS = ['params', 'query', 'body', 'output']
616
695
  const SCHEMA_FIELD_IDS = new Set(SCHEMA_SECTIONS)
617
696
  const SOURCE_FIELD_IDS = new Set(['sourceDefinition', 'sourceSchemas'])
@@ -645,11 +724,39 @@
645
724
  },
646
725
  ]
647
726
 
727
+ const VIEW_IDS = {
728
+ leaves: 'leaves',
729
+ changelogRoute: 'changelogRoute',
730
+ changelogSource: 'changelogSource',
731
+ }
732
+
733
+ const TAB_CONFIG = {
734
+ [VIEW_IDS.leaves]: {
735
+ fieldDefs: SEARCH_FIELDS,
736
+ defaultSelected: new Set(SEARCH_FIELDS.map((field) => field.id)),
737
+ },
738
+ [VIEW_IDS.changelogRoute]: {
739
+ fieldDefs: CHANGELOG_ROUTE_FIELDS,
740
+ defaultSelected: new Set(CHANGELOG_ROUTE_FIELDS.map((field) => field.id)),
741
+ },
742
+ [VIEW_IDS.changelogSource]: {
743
+ fieldDefs: CHANGELOG_SOURCE_FIELDS,
744
+ defaultSelected: new Set(CHANGELOG_SOURCE_FIELDS.map((field) => field.id)),
745
+ },
746
+ }
747
+
648
748
  const state = {
649
749
  payload: null,
750
+ leavesPayload: null,
751
+ changelogPayload: null,
650
752
  leaves: [],
651
- mode: 'snapshot',
652
- selectedFieldIds: new Set(SEARCH_FIELDS.map((field) => field.id)),
753
+ activeTab: VIEW_IDS.leaves,
754
+ availableTabs: new Set([VIEW_IDS.leaves]),
755
+ selectedFieldIdsByTab: {
756
+ [VIEW_IDS.leaves]: new Set(TAB_CONFIG[VIEW_IDS.leaves].defaultSelected),
757
+ [VIEW_IDS.changelogRoute]: new Set(TAB_CONFIG[VIEW_IDS.changelogRoute].defaultSelected),
758
+ [VIEW_IDS.changelogSource]: new Set(TAB_CONFIG[VIEW_IDS.changelogSource].defaultSelected),
759
+ },
653
760
  }
654
761
 
655
762
  const fileInput = document.getElementById('fileInput')
@@ -669,6 +776,9 @@
669
776
  const statusEl = document.getElementById('status')
670
777
  const resultsEl = document.getElementById('results')
671
778
  const controlsEl = document.querySelector('.controls')
779
+ const tabLeavesBtn = document.getElementById('tabLeaves')
780
+ const tabChangelogRouteBtn = document.getElementById('tabChangelogRoute')
781
+ const tabChangelogSourceBtn = document.getElementById('tabChangelogSource')
672
782
 
673
783
  const URL_PARAM_KEY = 'filters'
674
784
  let isHydratingFromUrl = false
@@ -1316,188 +1426,153 @@
1316
1426
  )
1317
1427
  }
1318
1428
 
1319
- function renderChangelogPayload(payload) {
1320
- state.mode = 'changelog'
1321
- state.payload = payload
1322
- state.leaves = []
1323
-
1324
- if (controlsEl) {
1325
- controlsEl.style.display = 'none'
1326
- }
1327
-
1328
- const commitBySha = new Map(
1329
- (payload.commits || []).map((commit) => [commit.sha, commit]),
1330
- )
1331
- const routeEntries = Object.entries(payload.byRoute || {}).sort(
1332
- ([a], [b]) => a.localeCompare(b),
1333
- )
1334
- const sourceEntries = Object.entries(payload.bySourceObject || {}).sort(
1335
- ([a], [b]) => a.localeCompare(b),
1429
+ function isBundlePayload(value) {
1430
+ return (
1431
+ value &&
1432
+ value.kind === 'finalized-leaves-bundle' &&
1433
+ value.leavesPayload &&
1434
+ value.changelogPayload
1336
1435
  )
1436
+ }
1337
1437
 
1338
- statusEl.textContent = `Loaded changelog payload with ${payload.commits.length} commits.`
1339
- resultsEl.innerHTML = ''
1438
+ function currentFieldDefs() {
1439
+ return TAB_CONFIG[state.activeTab]?.fieldDefs || []
1440
+ }
1340
1441
 
1341
- const tabs = el('div', 'chips')
1342
- const byRouteBtn = el('button', 'chip chip-btn', 'By Route')
1343
- const bySourceBtn = el('button', 'chip chip-btn', 'By Source Object')
1344
- byRouteBtn.type = 'button'
1345
- bySourceBtn.type = 'button'
1346
- tabs.appendChild(byRouteBtn)
1347
- tabs.appendChild(bySourceBtn)
1348
- resultsEl.appendChild(tabs)
1349
-
1350
- const view = el('div')
1351
- resultsEl.appendChild(view)
1352
-
1353
- const formatCommitDateTime = (value) => {
1354
- if (!value) return null
1355
- const parsed = new Date(value)
1356
- if (Number.isNaN(parsed.getTime())) return String(value)
1357
- return parsed.toLocaleString()
1358
- }
1442
+ function formatCommitDateTime(value) {
1443
+ if (!value) return null
1444
+ const parsed = new Date(value)
1445
+ if (Number.isNaN(parsed.getTime())) return String(value)
1446
+ return parsed.toLocaleString()
1447
+ }
1359
1448
 
1360
- const formatRouteSchemaEntry = (entry) => {
1361
- if (!entry || typeof entry !== 'object') return 'n/a'
1362
- const parts = [
1363
- `type=${entry.type}`,
1364
- `nullable=${Boolean(entry.nullable)}`,
1365
- `optional=${Boolean(entry.optional)}`,
1366
- ]
1367
- if ('literal' in entry) {
1368
- parts.push(`literal=${JSON.stringify(entry.literal)}`)
1369
- }
1370
- return parts.join(', ')
1449
+ function formatRouteSchemaEntry(entry) {
1450
+ if (!entry || typeof entry !== 'object') return 'n/a'
1451
+ const parts = [
1452
+ `type=${entry.type}`,
1453
+ `nullable=${Boolean(entry.nullable)}`,
1454
+ `optional=${Boolean(entry.optional)}`,
1455
+ ]
1456
+ if ('literal' in entry) {
1457
+ parts.push(`literal=${JSON.stringify(entry.literal)}`)
1371
1458
  }
1459
+ return parts.join(', ')
1460
+ }
1372
1461
 
1373
- const formatSchemaDiffLine = (delta) => {
1374
- if (!delta || typeof delta !== 'object') return null
1375
-
1376
- // Source-object schema diff shape: { section, path, op, before?, after? }
1377
- if ('section' in delta) {
1378
- const location = `${delta.section}.${delta.path}`
1379
- if (delta.op === 'added') {
1380
- return `added ${location}: ${JSON.stringify(delta.after || [])}`
1381
- }
1382
- if (delta.op === 'removed') {
1383
- return `removed ${location}: ${JSON.stringify(delta.before || [])}`
1384
- }
1385
- return `changed ${location}: ${JSON.stringify(delta.before || [])} -> ${JSON.stringify(
1386
- delta.after || [],
1387
- )}`
1388
- }
1389
-
1390
- // Route schema diff shape: { path, op, before?, after? }
1391
- if ('path' in delta) {
1392
- if (delta.op === 'added') {
1393
- return `added ${delta.path}: ${formatRouteSchemaEntry(delta.after)}`
1394
- }
1395
- if (delta.op === 'removed') {
1396
- return `removed ${delta.path}: ${formatRouteSchemaEntry(delta.before)}`
1397
- }
1398
- return `changed ${delta.path}: ${formatRouteSchemaEntry(
1399
- delta.before,
1400
- )} -> ${formatRouteSchemaEntry(delta.after)}`
1401
- }
1402
-
1403
- return null
1462
+ function formatSchemaDiffLine(delta) {
1463
+ if (!delta || typeof delta !== 'object') return null
1464
+ if ('section' in delta) {
1465
+ const location = `${delta.section}.${delta.path}`
1466
+ if (delta.op === 'added') return `added ${location}: ${JSON.stringify(delta.after || [])}`
1467
+ if (delta.op === 'removed') return `removed ${location}: ${JSON.stringify(delta.before || [])}`
1468
+ return `changed ${location}: ${JSON.stringify(delta.before || [])} -> ${JSON.stringify(delta.after || [])}`
1404
1469
  }
1405
-
1406
- const formatCfgDiffLine = (diff) => {
1407
- if (!diff || typeof diff !== 'object') return null
1408
- return `${diff.field}: ${JSON.stringify(diff.before)} -> ${JSON.stringify(diff.after)}`
1470
+ if ('path' in delta) {
1471
+ if (delta.op === 'added') return `added ${delta.path}: ${formatRouteSchemaEntry(delta.after)}`
1472
+ if (delta.op === 'removed') return `removed ${delta.path}: ${formatRouteSchemaEntry(delta.before)}`
1473
+ return `changed ${delta.path}: ${formatRouteSchemaEntry(delta.before)} -> ${formatRouteSchemaEntry(delta.after)}`
1409
1474
  }
1410
-
1411
- const renderEventDetails = (event) => {
1412
- const card = el('div', 'leaf-content')
1413
- const commit = commitBySha.get(event.commitSha)
1414
- const commitDateTime = formatCommitDateTime(commit?.authorDate)
1415
- const commitMeta = [
1416
- event.commitSha.slice(0, 12),
1417
- commitDateTime,
1418
- commit?.authorName || null,
1419
- commit?.subject || null,
1420
- ]
1421
- .filter(Boolean)
1422
- .join(' - ')
1423
- card.appendChild(
1424
- el(
1425
- 'div',
1426
- 'meta',
1427
- commitMeta,
1428
- ),
1429
- )
1430
- card.appendChild(el('div', 'mono', event.type))
1431
- if (Array.isArray(event.schemaDiff) && event.schemaDiff.length > 0) {
1432
- card.appendChild(el('div', 'meta', `schema changes: ${event.schemaDiff.length}`))
1433
- const schemaChanges = el('div')
1434
- event.schemaDiff.forEach((delta) => {
1435
- const line = formatSchemaDiffLine(delta)
1436
- if (!line) return
1437
- schemaChanges.appendChild(el('div', 'meta', line))
1438
- })
1439
- card.appendChild(schemaChanges)
1440
- }
1441
- if (Array.isArray(event.cfgDiff) && event.cfgDiff.length > 0) {
1442
- card.appendChild(el('div', 'meta', `cfg changes: ${event.cfgDiff.length}`))
1443
- const cfgChanges = el('div')
1444
- event.cfgDiff.forEach((diff) => {
1445
- const line = formatCfgDiffLine(diff)
1446
- if (!line) return
1447
- cfgChanges.appendChild(el('div', 'meta', line))
1448
- })
1449
- card.appendChild(cfgChanges)
1450
- }
1451
- if (Array.isArray(event.impactedRoutes) && event.impactedRoutes.length > 0) {
1452
- card.appendChild(
1453
- el('div', 'meta', `impacted routes: ${event.impactedRoutes.length}`),
1454
- )
1455
- }
1456
- return card
1475
+ return null
1476
+ }
1477
+
1478
+ function formatCfgDiffLine(diff) {
1479
+ if (!diff || typeof diff !== 'object') return null
1480
+ return `${diff.field}: ${JSON.stringify(diff.before)} -> ${JSON.stringify(diff.after)}`
1481
+ }
1482
+
1483
+ function renderEventCard(event, commitBySha) {
1484
+ const card = el('div', 'leaf-content')
1485
+ const commit = commitBySha.get(event.commitSha)
1486
+ const commitDateTime = formatCommitDateTime(commit?.authorDate)
1487
+ const commitMeta = [
1488
+ event.commitSha.slice(0, 12),
1489
+ commitDateTime,
1490
+ commit?.authorName || null,
1491
+ commit?.subject || null,
1492
+ ]
1493
+ .filter(Boolean)
1494
+ .join(' - ')
1495
+ card.appendChild(el('div', 'meta', commitMeta))
1496
+ card.appendChild(el('div', 'mono', event.type))
1497
+ if (Array.isArray(event.schemaDiff) && event.schemaDiff.length > 0) {
1498
+ card.appendChild(el('div', 'meta', `schema changes: ${event.schemaDiff.length}`))
1499
+ const schemaChanges = el('div')
1500
+ event.schemaDiff.forEach((delta) => {
1501
+ const line = formatSchemaDiffLine(delta)
1502
+ if (!line) return
1503
+ schemaChanges.appendChild(el('div', 'meta', line))
1504
+ })
1505
+ card.appendChild(schemaChanges)
1457
1506
  }
1458
-
1459
- const renderByRoute = () => {
1460
- view.innerHTML = ''
1461
- if (routeEntries.length === 0) {
1462
- view.appendChild(el('div', 'empty', 'No route changes found in this range.'))
1463
- return
1464
- }
1465
-
1466
- routeEntries.forEach(([routeKey, events]) => {
1467
- const details = el('details', 'leaf')
1468
- details.appendChild(el('summary', '', `${routeKey} (${events.length})`))
1469
- events.forEach((event) => details.appendChild(renderEventDetails(event)))
1470
- view.appendChild(details)
1507
+ if (Array.isArray(event.cfgDiff) && event.cfgDiff.length > 0) {
1508
+ card.appendChild(el('div', 'meta', `cfg changes: ${event.cfgDiff.length}`))
1509
+ const cfgChanges = el('div')
1510
+ event.cfgDiff.forEach((diff) => {
1511
+ const line = formatCfgDiffLine(diff)
1512
+ if (!line) return
1513
+ cfgChanges.appendChild(el('div', 'meta', line))
1471
1514
  })
1515
+ card.appendChild(cfgChanges)
1472
1516
  }
1517
+ if (Array.isArray(event.impactedRoutes) && event.impactedRoutes.length > 0) {
1518
+ card.appendChild(el('div', 'meta', `impacted routes: ${event.impactedRoutes.length}`))
1519
+ }
1520
+ return card
1521
+ }
1473
1522
 
1474
- const renderBySource = () => {
1475
- view.innerHTML = ''
1476
- if (sourceEntries.length === 0) {
1477
- view.appendChild(
1478
- el('div', 'empty', 'No source-object changes found in this range.'),
1479
- )
1480
- return
1481
- }
1482
-
1483
- sourceEntries.forEach(([sourceId, events]) => {
1484
- const details = el('details', 'leaf')
1485
- const display = events[0]?.displayName || sourceId
1486
- details.appendChild(el('summary', '', `${display} (${events.length})`))
1487
- events.forEach((event) => details.appendChild(renderEventDetails(event)))
1488
- view.appendChild(details)
1489
- })
1523
+ function changelogItemsByTab(tabId) {
1524
+ const payload = state.changelogPayload
1525
+ if (!payload) return []
1526
+ if (tabId === VIEW_IDS.changelogRoute) {
1527
+ return Object.entries(payload.byRoute || {})
1528
+ .sort(([a], [b]) => a.localeCompare(b))
1529
+ .map(([routeKey, events]) => ({ routeKey, events }))
1490
1530
  }
1531
+ if (tabId === VIEW_IDS.changelogSource) {
1532
+ return Object.entries(payload.bySourceObject || {})
1533
+ .sort(([a], [b]) => a.localeCompare(b))
1534
+ .map(([sourceId, events]) => ({
1535
+ sourceId,
1536
+ events,
1537
+ displayName: events[0]?.displayName || sourceId,
1538
+ }))
1539
+ }
1540
+ return []
1541
+ }
1491
1542
 
1492
- byRouteBtn.addEventListener('click', renderByRoute)
1493
- bySourceBtn.addEventListener('click', renderBySource)
1494
- renderByRoute()
1543
+ function matchesGenericItem(item, fieldDefs, engine, selectedIds) {
1544
+ if (!engine.active) return true
1545
+ return fieldDefs.some((field) => {
1546
+ if (selectedIds.length > 0 && !selectedIds.includes(field.id)) return false
1547
+ const tokens = toTokens(field.get(item))
1548
+ return tokens.some((token) => engine.test(token))
1549
+ })
1495
1550
  }
1496
1551
 
1497
- function renderResults() {
1498
- if (state.mode === 'changelog') {
1552
+ function renderCollection(items, renderItem, emptyMessage) {
1553
+ if (items.length === 0) {
1554
+ resultsEl.appendChild(el('div', 'empty', emptyMessage))
1499
1555
  return
1500
1556
  }
1557
+ items.forEach((item) => {
1558
+ resultsEl.appendChild(renderItem(item))
1559
+ })
1560
+ }
1561
+
1562
+ function renderTabButtons() {
1563
+ setPressed(tabLeavesBtn, state.activeTab === VIEW_IDS.leaves)
1564
+ setPressed(tabChangelogRouteBtn, state.activeTab === VIEW_IDS.changelogRoute)
1565
+ setPressed(tabChangelogSourceBtn, state.activeTab === VIEW_IDS.changelogSource)
1566
+ tabLeavesBtn.style.display = state.availableTabs.has(VIEW_IDS.leaves) ? '' : 'none'
1567
+ tabChangelogRouteBtn.style.display = state.availableTabs.has(VIEW_IDS.changelogRoute) ? '' : 'none'
1568
+ tabChangelogSourceBtn.style.display = state.availableTabs.has(VIEW_IDS.changelogSource) ? '' : 'none'
1569
+ const isLeavesTab = state.activeTab === VIEW_IDS.leaves
1570
+ coreFieldsBtn.style.display = isLeavesTab ? '' : 'none'
1571
+ schemasOnlyFieldsBtn.style.display = isLeavesTab ? '' : 'none'
1572
+ sourceOnlyFieldsBtn.style.display = isLeavesTab ? '' : 'none'
1573
+ }
1574
+
1575
+ function renderResults() {
1501
1576
  const engine = createSearchEngine(searchInput.value.trim(), {
1502
1577
  caseSensitive: isPressed(caseSensitiveToggle),
1503
1578
  regex: isPressed(regexSearchToggle),
@@ -1515,51 +1590,84 @@
1515
1590
  }
1516
1591
 
1517
1592
  const selectedIds = selectedFieldIds()
1518
- const filtered = state.leaves.filter((leaf) => matchesLeaf(leaf, engine, selectedIds))
1519
-
1520
- statusEl.textContent = `${filtered.length} / ${state.leaves.length} routes matched.`
1521
1593
  resultsEl.innerHTML = ''
1594
+ renderTabButtons()
1522
1595
  renderActiveFilterChips({ selectedIds, hasRegexError: false })
1523
-
1524
- if (filtered.length === 0) {
1525
- resultsEl.appendChild(el('div', 'empty', 'No matches.'))
1526
- syncFilterStateToUrl()
1527
- return
1596
+ if (state.activeTab === VIEW_IDS.leaves) {
1597
+ const filtered = state.leaves.filter((leaf) => matchesLeaf(leaf, engine, selectedIds))
1598
+ statusEl.textContent = `${filtered.length} / ${state.leaves.length} routes matched.`
1599
+ renderCollection(filtered, (leaf) => renderLeaf(leaf, engine, selectedIds), 'No matches.')
1600
+ } else {
1601
+ const fieldDefs = currentFieldDefs()
1602
+ const items = changelogItemsByTab(state.activeTab)
1603
+ const filtered = items.filter((item) => matchesGenericItem(item, fieldDefs, engine, selectedIds))
1604
+ statusEl.textContent =
1605
+ state.activeTab === VIEW_IDS.changelogRoute
1606
+ ? `${filtered.length} / ${items.length} route changelog groups matched.`
1607
+ : `${filtered.length} / ${items.length} source changelog groups matched.`
1608
+ const commitBySha = new Map(
1609
+ (state.changelogPayload?.commits || []).map((commit) => [commit.sha, commit]),
1610
+ )
1611
+ renderCollection(
1612
+ filtered,
1613
+ (item) => {
1614
+ const details = el('details', 'leaf')
1615
+ const title =
1616
+ state.activeTab === VIEW_IDS.changelogRoute
1617
+ ? `${item.routeKey} (${item.events.length})`
1618
+ : `${item.displayName} (${item.events.length})`
1619
+ details.appendChild(el('summary', '', title))
1620
+ item.events.forEach((event) => details.appendChild(renderEventCard(event, commitBySha)))
1621
+ return details
1622
+ },
1623
+ 'No matches.',
1624
+ )
1528
1625
  }
1529
-
1530
- filtered.forEach((leaf) => {
1531
- resultsEl.appendChild(renderLeaf(leaf, engine, selectedIds))
1532
- })
1533
1626
  syncFilterStateToUrl()
1534
1627
  }
1535
1628
 
1536
1629
  function selectedFieldIds() {
1537
- return SEARCH_FIELDS.filter((field) => state.selectedFieldIds.has(field.id)).map(
1630
+ const selected = state.selectedFieldIdsByTab[state.activeTab] || new Set()
1631
+ return currentFieldDefs().filter((field) => selected.has(field.id)).map(
1538
1632
  (field) => field.id,
1539
1633
  )
1540
1634
  }
1541
1635
 
1542
1636
  function renderFieldChips() {
1637
+ const fieldDefs = currentFieldDefs()
1638
+ const selected = state.selectedFieldIdsByTab[state.activeTab] || new Set()
1543
1639
  fieldChipGroups.innerHTML = ''
1544
- FIELD_GROUPS.forEach((group) => {
1640
+ const groups =
1641
+ state.activeTab === VIEW_IDS.leaves
1642
+ ? FIELD_GROUPS
1643
+ : [
1644
+ {
1645
+ id: 'tabFields',
1646
+ label: state.activeTab === VIEW_IDS.changelogRoute ? 'Route fields' : 'Source fields',
1647
+ fieldIds: fieldDefs.map((field) => field.id),
1648
+ },
1649
+ ]
1650
+
1651
+ groups.forEach((group) => {
1545
1652
  const groupWrap = el('div', 'scope-group')
1546
1653
  groupWrap.appendChild(el('div', 'scope-group-title', group.label))
1547
1654
  const chipsWrap = el('div', 'scope-group-chips')
1548
1655
 
1549
1656
  group.fieldIds.forEach((fieldId) => {
1550
- const field = SEARCH_FIELDS.find((item) => item.id === fieldId)
1657
+ const field = fieldDefs.find((item) => item.id === fieldId)
1551
1658
  if (!field) return
1552
1659
  const chip = document.createElement('button')
1553
1660
  chip.type = 'button'
1554
1661
  chip.className = 'field-chip'
1555
1662
  chip.textContent = field.label
1556
- setPressed(chip, state.selectedFieldIds.has(field.id))
1663
+ setPressed(chip, selected.has(field.id))
1557
1664
  chip.addEventListener('click', () => {
1558
- if (state.selectedFieldIds.has(field.id)) {
1559
- state.selectedFieldIds.delete(field.id)
1665
+ if (selected.has(field.id)) {
1666
+ selected.delete(field.id)
1560
1667
  } else {
1561
- state.selectedFieldIds.add(field.id)
1668
+ selected.add(field.id)
1562
1669
  }
1670
+ state.selectedFieldIdsByTab[state.activeTab] = selected
1563
1671
  renderFieldChips()
1564
1672
  renderResults()
1565
1673
  })
@@ -1573,8 +1681,8 @@
1573
1681
 
1574
1682
  function setFieldSelection(allowedIds, options = {}) {
1575
1683
  const { rerender = true } = options
1576
- state.selectedFieldIds = new Set(
1577
- SEARCH_FIELDS.map((field) => field.id).filter((id) => allowedIds.has(id)),
1684
+ state.selectedFieldIdsByTab[state.activeTab] = new Set(
1685
+ currentFieldDefs().map((field) => field.id).filter((id) => allowedIds.has(id)),
1578
1686
  )
1579
1687
  renderFieldChips()
1580
1688
  if (rerender) renderResults()
@@ -1586,7 +1694,7 @@
1586
1694
  setPressed(regexSearchToggle, false)
1587
1695
  typeMatchModeInput.value = 'contains'
1588
1696
  setPressed(schemaRowsMatchOnlyToggle, false)
1589
- setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id)), { rerender: false })
1697
+ setFieldSelection(new Set(currentFieldDefs().map((field) => field.id)), { rerender: false })
1590
1698
  renderResults()
1591
1699
  }
1592
1700
 
@@ -1642,7 +1750,7 @@
1642
1750
  }
1643
1751
  if (hasRegexError) chipModels.push({ text: 'regex error' })
1644
1752
 
1645
- const allIds = SEARCH_FIELDS.map((field) => field.id)
1753
+ const allIds = currentFieldDefs().map((field) => field.id)
1646
1754
  if (selectedIds.length !== allIds.length) {
1647
1755
  chipModels.push({
1648
1756
  text: `fields: ${selectedIds.join(', ') || 'none'}`,
@@ -1669,12 +1777,15 @@
1669
1777
 
1670
1778
  function collectFilterState() {
1671
1779
  return {
1780
+ activeTab: state.activeTab,
1672
1781
  search: searchInput.value,
1673
1782
  caseSensitive: isPressed(caseSensitiveToggle),
1674
1783
  regex: isPressed(regexSearchToggle),
1675
1784
  typeMatchMode: typeMatchModeInput.value === 'exact' ? 'exact' : 'contains',
1676
1785
  schemaRowsMatchOnly: isPressed(schemaRowsMatchOnlyToggle),
1677
- selectedFields: selectedFieldIds(),
1786
+ selectedFieldsByTab: Object.fromEntries(
1787
+ Object.entries(state.selectedFieldIdsByTab).map(([key, value]) => [key, Array.from(value)]),
1788
+ ),
1678
1789
  }
1679
1790
  }
1680
1791
 
@@ -1699,6 +1810,13 @@
1699
1810
  try {
1700
1811
  const parsed = JSON.parse(decodeURIComponent(raw))
1701
1812
  isHydratingFromUrl = true
1813
+ if (
1814
+ parsed.activeTab === VIEW_IDS.leaves ||
1815
+ parsed.activeTab === VIEW_IDS.changelogRoute ||
1816
+ parsed.activeTab === VIEW_IDS.changelogSource
1817
+ ) {
1818
+ state.activeTab = parsed.activeTab
1819
+ }
1702
1820
  if (typeof parsed.search === 'string') searchInput.value = parsed.search
1703
1821
  setPressed(caseSensitiveToggle, Boolean(parsed.caseSensitive))
1704
1822
  setPressed(regexSearchToggle, Boolean(parsed.regex))
@@ -1707,8 +1825,12 @@
1707
1825
  }
1708
1826
  setPressed(schemaRowsMatchOnlyToggle, Boolean(parsed.schemaRowsMatchOnly))
1709
1827
 
1710
- if (Array.isArray(parsed.selectedFields)) {
1711
- setFieldSelection(new Set(parsed.selectedFields), { rerender: false })
1828
+ if (parsed.selectedFieldsByTab && typeof parsed.selectedFieldsByTab === 'object') {
1829
+ Object.entries(parsed.selectedFieldsByTab).forEach(([tabId, ids]) => {
1830
+ if (!Array.isArray(ids)) return
1831
+ if (!state.selectedFieldIdsByTab[tabId]) return
1832
+ state.selectedFieldIdsByTab[tabId] = new Set(ids)
1833
+ })
1712
1834
  }
1713
1835
  } catch (error) {
1714
1836
  // Ignore malformed hash state.
@@ -1720,40 +1842,84 @@
1720
1842
  async function handleFile(file) {
1721
1843
  const text = await file.text()
1722
1844
  const parsed = JSON.parse(text)
1723
- if (isChangelogPayload(parsed)) {
1724
- renderChangelogPayload(parsed)
1725
- return
1726
- }
1727
- if (!parsed || !Array.isArray(parsed.leaves)) {
1845
+ if (
1846
+ !isBundlePayload(parsed) &&
1847
+ !isChangelogPayload(parsed) &&
1848
+ (!parsed || !Array.isArray(parsed.leaves))
1849
+ ) {
1728
1850
  throw new Error(
1729
- 'Invalid export file: expected top-level "leaves" array or changelog payload.',
1851
+ 'Invalid export file: expected leaves payload, changelog payload, or unified bundle.',
1730
1852
  )
1731
1853
  }
1854
+ applyLoadedPayload(parsed)
1855
+ renderResults()
1856
+ }
1732
1857
 
1733
- state.mode = 'snapshot'
1734
- if (controlsEl) {
1735
- controlsEl.style.display = ''
1736
- }
1737
- state.payload = parsed
1738
- state.leaves = parsed.leaves
1858
+ function setActiveTab(nextTab) {
1859
+ if (!state.availableTabs.has(nextTab)) return
1860
+ state.activeTab = nextTab
1861
+ renderFieldChips()
1739
1862
  renderResults()
1740
1863
  }
1741
1864
 
1865
+ function applyLoadedPayload(payload) {
1866
+ state.payload = payload
1867
+ state.availableTabs = new Set()
1868
+ state.leavesPayload = null
1869
+ state.changelogPayload = null
1870
+ state.leaves = []
1871
+
1872
+ if (isBundlePayload(payload)) {
1873
+ state.leavesPayload = payload.leavesPayload
1874
+ state.changelogPayload = payload.changelogPayload
1875
+ } else if (isChangelogPayload(payload)) {
1876
+ state.changelogPayload = payload
1877
+ } else if (payload && Array.isArray(payload.leaves)) {
1878
+ state.leavesPayload = payload
1879
+ }
1880
+
1881
+ if (state.leavesPayload) {
1882
+ state.availableTabs.add(VIEW_IDS.leaves)
1883
+ state.leaves = state.leavesPayload.leaves || []
1884
+ }
1885
+ if (state.changelogPayload) {
1886
+ state.availableTabs.add(VIEW_IDS.changelogRoute)
1887
+ state.availableTabs.add(VIEW_IDS.changelogSource)
1888
+ }
1889
+
1890
+ if (!state.availableTabs.has(state.activeTab)) {
1891
+ state.activeTab = state.availableTabs.has(VIEW_IDS.leaves)
1892
+ ? VIEW_IDS.leaves
1893
+ : state.availableTabs.has(VIEW_IDS.changelogRoute)
1894
+ ? VIEW_IDS.changelogRoute
1895
+ : VIEW_IDS.changelogSource
1896
+ }
1897
+
1898
+ if (state.leavesPayload && state.changelogPayload) {
1899
+ statusEl.textContent =
1900
+ `Loaded unified bundle with ${state.leaves.length} routes and ${
1901
+ state.changelogPayload.commits?.length || 0
1902
+ } commits.`
1903
+ } else if (state.leavesPayload) {
1904
+ statusEl.textContent = `Loaded payload with ${state.leaves.length} routes.`
1905
+ } else if (state.changelogPayload) {
1906
+ statusEl.textContent =
1907
+ `Loaded changelog payload with ${state.changelogPayload.commits?.length || 0} commits.`
1908
+ } else {
1909
+ statusEl.textContent = 'No payload loaded.'
1910
+ }
1911
+ }
1912
+
1742
1913
  function initializeFromBakedPayload() {
1743
1914
  const baked = window.__FINALIZED_LEAVES_PAYLOAD
1744
- if (isChangelogPayload(baked)) {
1745
- renderChangelogPayload(baked)
1915
+ if (
1916
+ !isBundlePayload(baked) &&
1917
+ !isChangelogPayload(baked) &&
1918
+ (!baked || !Array.isArray(baked.leaves))
1919
+ ) {
1746
1920
  return
1747
1921
  }
1748
- if (!baked || !Array.isArray(baked.leaves)) return
1749
-
1750
- state.mode = 'snapshot'
1751
- if (controlsEl) {
1752
- controlsEl.style.display = ''
1753
- }
1754
- state.payload = baked
1755
- state.leaves = baked.leaves
1756
- statusEl.textContent = `Loaded baked payload with ${state.leaves.length} routes.`
1922
+ applyLoadedPayload(baked)
1757
1923
  renderResults()
1758
1924
  }
1759
1925
 
@@ -1784,8 +1950,12 @@
1784
1950
  renderResults()
1785
1951
  })
1786
1952
 
1953
+ tabLeavesBtn.addEventListener('click', () => setActiveTab(VIEW_IDS.leaves))
1954
+ tabChangelogRouteBtn.addEventListener('click', () => setActiveTab(VIEW_IDS.changelogRoute))
1955
+ tabChangelogSourceBtn.addEventListener('click', () => setActiveTab(VIEW_IDS.changelogSource))
1956
+
1787
1957
  selectAllFieldsBtn.addEventListener('click', () =>
1788
- setFieldSelection(new Set(SEARCH_FIELDS.map((field) => field.id))),
1958
+ setFieldSelection(new Set(currentFieldDefs().map((field) => field.id))),
1789
1959
  )
1790
1960
  clearAllFieldsBtn.addEventListener('click', () => setFieldSelection(new Set()))
1791
1961
  coreFieldsBtn.addEventListener('click', () => setFieldSelection(CORE_FIELD_IDS))