From 4a39100c7e2ee257b447924a81cede1a0da19be6 Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:19 -0500 Subject: [PATCH 01/14] Add reusable library-system scopes (index.html) --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 5982889..56753a4 100644 --- a/index.html +++ b/index.html @@ -833,7 +833,7 @@

- + From 056ea28bc2d714c9bf446257dff95ee5ebf0cc4e Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:21 -0500 Subject: [PATCH 02/14] Add reusable library-system scopes (src/core/mockQueryBackend.js) --- src/core/mockQueryBackend.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/core/mockQueryBackend.js b/src/core/mockQueryBackend.js index b279f5d..fbad2a6 100644 --- a/src/core/mockQueryBackend.js +++ b/src/core/mockQueryBackend.js @@ -104,7 +104,7 @@ function demoCalendarPeriods(now = new Date()) { function buildDemoLibraryDashboard(payload = {}, data = {}) { const library = payload.library || 'all'; const itemType = payload.item_type || 'all'; - const scopeFactor = library === 'all' ? 1 : 0.08; + const scopeFactor = library === 'all' ? 1 : library.startsWith('system:') ? 0.25 : 0.08; const typeFactor = itemType === 'all' ? 1 : 0.22; const factor = scopeFactor * typeFactor; const reportingPeriod = String(payload.reporting_period || payload.active_window_days || 365); @@ -141,11 +141,17 @@ function buildDemoLibraryDashboard(payload = {}, data = {}) { const previousCheckouts = scaled(842110); const previousRenewals = scaled(469220); const filters = { + systems: [ + { value: 'system:MSU', code: 'MSU', label: 'Mississippi State University' }, + { value: 'system:MMRLS', code: 'MMRLS', label: 'Mid-Mississippi Regional Library System' }, + { value: 'system:FRL', code: 'FRL', label: 'First Regional Library' }, + { value: 'system:LILS', code: 'LILS', label: 'Lee-Itawamba Library System' } + ], libraries: [ - { value: 'MSU', label: 'Mississippi State University' }, - { value: 'MMRLS', label: 'Mid-Mississippi Regional Library System' }, - { value: 'FRL', label: 'First Regional Library' }, - { value: 'LILS', label: 'Lee-Itawamba Library System' } + { value: 'MSU-MAIN', label: 'MSU Main Library' }, + { value: 'MMRLS-CARTHAGE', label: 'MMRLS Carthage' }, + { value: 'FRL-HERNANDO', label: 'First Regional Hernando' }, + { value: 'LILS-TUPELO', label: 'Lee-Itawamba Tupelo' } ], item_types: ['BOOK', 'EBOOK', 'DVD', 'AUDIOBOOK', 'KIT'], calendar_periods: calendarPeriods, @@ -157,7 +163,7 @@ function buildDemoLibraryDashboard(payload = {}, data = {}) { sample_data: true, scope: { library, - library_label: library === 'all' ? 'All MLP libraries' : (filters.libraries.find(entry => entry.value === library)?.label || library), + library_label: library === 'all' ? 'All MLP libraries' : ([...filters.systems, ...filters.libraries].find(entry => entry.value === library)?.label || library), item_type: itemType, item_type_label: itemType === 'all' ? 'All item types' : itemType, active_window_days: Number(payload.active_window_days || 365), From 2f5d2e43e2ab8c35ac771ba3221fad63c8c879d2 Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:23 -0500 Subject: [PATCH 03/14] Add reusable library-system scopes (src/ui/dashboard/libraryDashboardModel.js) --- src/ui/dashboard/libraryDashboardModel.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/dashboard/libraryDashboardModel.js b/src/ui/dashboard/libraryDashboardModel.js index ffca4ae..d6052f2 100644 --- a/src/ui/dashboard/libraryDashboardModel.js +++ b/src/ui/dashboard/libraryDashboardModel.js @@ -49,6 +49,7 @@ function normalizeLibraryDashboard(payload = {}) { patronStateBreakdown: normalizeSeries(data.patron_state_breakdown), opportunities: Array.isArray(data.opportunities) ? data.opportunities : [], filters: { + systems: Array.isArray(data.filters?.systems) ? data.filters.systems : [], libraries: Array.isArray(data.filters?.libraries) ? data.filters.libraries : [], itemTypes: Array.isArray(data.filters?.item_types) ? data.filters.item_types : [], calendarPeriods: Array.isArray(data.filters?.calendar_periods) ? data.filters.calendar_periods : [], From 838e21c42e258158ff1fa8be444eaedd1ab444a3 Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:25 -0500 Subject: [PATCH 04/14] Add reusable library-system scopes (src/ui/dashboard/kpiDashboard.js) --- src/ui/dashboard/kpiDashboard.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/ui/dashboard/kpiDashboard.js b/src/ui/dashboard/kpiDashboard.js index d247c87..338659d 100644 --- a/src/ui/dashboard/kpiDashboard.js +++ b/src/ui/dashboard/kpiDashboard.js @@ -1,6 +1,7 @@ import { BackendApi } from '../../core/backendApi.js'; import { appServices } from '../../core/appServices.js'; import { getClientErrorMessage } from '../../core/clientErrorMessages.js'; +import { ALL_LIBRARY_SYSTEMS_LABEL, buildLibraryScopeGroups, systemCodeForLibraryScope } from '../../core/libraryScopes.js'; import { onDOMReady } from '../../core/domReady.js'; import { libraryDashboardHasData, normalizeLibraryDashboard } from './libraryDashboardModel.js'; import { downloadLibraryDashboardCsv } from './libraryDashboardExport.js'; @@ -46,9 +47,28 @@ function replaceOptions(select, baseLabel, options, selected) { select.value = normalized.some(option => option.value === selected) ? selected : 'all'; } +function replaceLibraryOptions(select, systems, libraries, selected) { + if (!select) return; + const groups = buildLibraryScopeGroups(systems, libraries); + const options = groups.flatMap(group => group.options); + const all = new Option(ALL_LIBRARY_SYSTEMS_LABEL, 'all'); + select.replaceChildren(all, ...groups.map(group => { + const element = document.createElement('optgroup'); + element.label = group.label; + element.append(...group.options.map(option => new Option(option.label, option.value))); + return element; + })); + select.value = options.some(option => option.value === selected) ? selected : 'all'; +} + function syncFilterOptions(elements) { if (!libraryData) return; - replaceOptions(elements.library, 'All MLP libraries', libraryData.filters.libraries, elements.library?.value || 'all'); + replaceLibraryOptions( + elements.library, + libraryData.filters.systems, + libraryData.filters.libraries, + elements.library?.value || 'all' + ); replaceOptions(elements.itemType, 'All item types', libraryData.filters.itemTypes, elements.itemType?.value || 'all'); syncPeriodOptions(elements); } @@ -57,7 +77,7 @@ function syncPeriodOptions(elements) { if (!elements.period || !libraryData) return; const selected = elements.period.value || '365'; const library = elements.library?.value || 'all'; - const system = library === 'all' ? '' : String(library).split('-')[0].toUpperCase(); + const system = systemCodeForLibraryScope(library); const rolling = [ { value: '90', label: 'Last 90 days' }, { value: '365', label: 'Last 12 months' }, From 29e2dc3e8dc39640bc84bba1d5c702428b7090dd Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:27 -0500 Subject: [PATCH 05/14] Add reusable library-system scopes (src/ui/form-mode/formModeControls.js) --- src/ui/form-mode/formModeControls.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ui/form-mode/formModeControls.js b/src/ui/form-mode/formModeControls.js index 06fa8a9..ac30216 100644 --- a/src/ui/form-mode/formModeControls.js +++ b/src/ui/form-mode/formModeControls.js @@ -8,6 +8,7 @@ import { SelectorControls } from '../controls/selectorControls.js'; import { fieldDefs, getFieldFilterOperators, isFieldBackendFilterable } from '../../features/filters/fieldDefs.js'; import { CustomDatePicker } from '../controls/customDatePicker.js'; import { createFormModeDateInputState } from './formModeDateInput.js'; +import { ALL_LIBRARY_SYSTEMS_LABEL } from '../../core/libraryScopes.js'; function parseFieldOptions(fieldDef, inputSpec, normalizeOperatorForField) { const source = Array.isArray(inputSpec.options) && inputSpec.options.length > 0 @@ -382,6 +383,8 @@ import { createFormModeDateInputState } from './formModeDateInput.js'; if (SelectorControls.createGroupedSelector) { const selector = SelectorControls.createGroupedSelector(values, isMultiSelect, initialValues, { enableGrouping: shouldGroupValues && hasDashes, + allSelectionLabel: fieldDef?.allSelectionLabel || fieldDef?.all_selection_label || (shouldGroupValues ? ALL_LIBRARY_SYSTEMS_LABEL : ''), + allSelectionDescription: fieldDef?.allSelectionDescription || fieldDef?.all_selection_description || '', groupSelectionLabel: fieldDef?.groupSelectionLabel || fieldDef?.group_selection_label || '', groupSelectionDescription: fieldDef?.groupSelectionDescription || fieldDef?.group_selection_description || '', containerId: null @@ -389,7 +392,7 @@ import { createFormModeDateInputState } from './formModeDateInput.js'; return createPopupListControl( selector, inputSpec.label || (fieldDef && fieldDef.name) || 'Select values', - inputSpec.placeholder || (isMultiSelect ? 'Click to select values…' : 'Click to select a value…') + inputSpec.placeholder || fieldDef?.allSelectionLabel || fieldDef?.all_selection_label || (isMultiSelect ? 'Click to select values…' : 'Click to select a value…') ); } From 563b749b27840b0c51f9144bbc2708638a6afdd7 Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:32 -0500 Subject: [PATCH 06/14] Add reusable library-system scopes (src/ui/controls/selectorControls.js) --- src/ui/controls/selectorControls.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/ui/controls/selectorControls.js b/src/ui/controls/selectorControls.js index b77facc..e86aad9 100644 --- a/src/ui/controls/selectorControls.js +++ b/src/ui/controls/selectorControls.js @@ -132,6 +132,8 @@ function createGroupedSelector(values, isMultiSelect, currentValues = [], option const enableGrouping = options.enableGrouping !== false; const groupSelectionLabel = String(options.groupSelectionLabel || '').trim(); const groupSelectionDescription = String(options.groupSelectionDescription || '').trim(); + const allSelectionLabel = String(options.allSelectionLabel || '').trim(); + const allSelectionDescription = String(options.allSelectionDescription || '').trim(); const containerId = Object.prototype.hasOwnProperty.call(options, 'containerId') ? options.containerId : 'condition-select-container'; @@ -165,6 +167,21 @@ function createGroupedSelector(values, isMultiSelect, currentValues = [], option initializeSearchInputs(searchWrapper); container.appendChild(searchWrapper); + let allSelectionButton = null; + if (allSelectionLabel) { + allSelectionButton = document.createElement('button'); + allSelectionButton.type = 'button'; + allSelectionButton.className = 'group-header selector-all-option'; + allSelectionButton.textContent = allSelectionLabel; + allSelectionButton.title = allSelectionDescription; + allSelectionButton.addEventListener('click', () => { + selectedValues.clear(); + container.dispatchEvent(new Event('change', { bubbles: true })); + rebuildVisibleRows(); + }); + container.appendChild(allSelectionButton); + } + const optionsContainer = document.createElement('div'); optionsContainer.className = 'grouped-options-container grouped-options-container--virtualized'; container.appendChild(optionsContainer); @@ -288,6 +305,11 @@ function createGroupedSelector(values, isMultiSelect, currentValues = [], option const searchTerm = searchInput.value.toLowerCase().trim(); visibleRows = []; optionIndex.clear(); + if (allSelectionButton) { + const selected = selectedValues.size === 0; + allSelectionButton.classList.toggle('is-selected', selected); + allSelectionButton.setAttribute('aria-pressed', selected ? 'true' : 'false'); + } topLevelEntries .slice() From 1c8767ba9da8c31f8c7b443e09d8dae9f3a055aa Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:34 -0500 Subject: [PATCH 07/14] Add reusable library-system scopes (src/styles/controls.css) --- src/styles/controls.css | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/styles/controls.css b/src/styles/controls.css index 1cd6daf..31adf8f 100644 --- a/src/styles/controls.css +++ b/src/styles/controls.css @@ -523,6 +523,20 @@ input[type="search"]:-webkit-autofill, transition: background-color var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast); } +.selector-all-option { + border: 0; + cursor: pointer; + font: inherit; + margin: 0 0 0.35rem; + text-align: left; + width: 100%; +} + +.selector-all-option.is-selected { + box-shadow: inset 3px 0 0 var(--accent-color); + font-weight: 700; +} + /* ========================================== 12. CUSTOM DATE PICKER ========================================== */ From 1b19a98582e1da34efe16c9c84d43fc7224cfc38 Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:36 -0500 Subject: [PATCH 08/14] Add reusable library-system scopes (tests/browser/browserSmoke.mjs) --- tests/browser/browserSmoke.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/browser/browserSmoke.mjs b/tests/browser/browserSmoke.mjs index e68c7d7..7862beb 100644 --- a/tests/browser/browserSmoke.mjs +++ b/tests/browser/browserSmoke.mjs @@ -520,6 +520,7 @@ async function runSmokeTest() { chartCount: panel.querySelectorAll('.kpi-chart-card').length, opportunityRows: panel.querySelectorAll('.kpi-opportunity-table tbody tr').length, libraryOptions: Array.from(panel.querySelectorAll('#kpi-dashboard-library option')).map(option => option.value), + libraryGroups: Array.from(panel.querySelectorAll('#kpi-dashboard-library optgroup')).map(group => group.label), exportVisible: !panel.querySelector('#kpi-dashboard-export')?.classList.contains('hidden'), comparisonText: panel.querySelector('.kpi-card')?.textContent || '', selectedTab: panel.querySelector('[data-kpi-view][aria-selected="true"]')?.dataset.kpiView || '' @@ -530,7 +531,10 @@ async function runSmokeTest() { || dashboardState.cardValues[2] !== '2,813,442' || dashboardState.chartCount !== 6 || dashboardState.opportunityRows !== 1 - || !dashboardState.libraryOptions.includes('MSU') + || !dashboardState.libraryOptions.includes('system:MSU') + || !dashboardState.libraryOptions.includes('MSU-MAIN') + || !dashboardState.libraryGroups.includes('Library systems') + || !dashboardState.libraryGroups.includes('Item libraries') || !dashboardState.exportVisible || !/up 38,119/iu.test(dashboardState.comparisonText) || dashboardState.selectedTab !== 'overview' @@ -554,7 +558,7 @@ async function runSmokeTest() { } await page.locator('#kpi-dashboard-window').selectOption('cy:2026'); await page.waitForFunction(() => document.querySelector('#kpi-dashboard-content .kpi-card')?.textContent?.includes('Calendar Year 2026 to date')); - await page.locator('#kpi-dashboard-library').selectOption('MSU'); + await page.locator('#kpi-dashboard-library').selectOption('system:MSU'); await page.waitForFunction(() => Array.from(document.querySelectorAll('#kpi-dashboard-window option')).some(option => option.value === 'fy:MSU:2027')); if (!await page.locator('#kpi-dashboard-window optgroup[label="Fiscal years"]').count()) { throw new Error('Dashboard should expose fiscal years as a distinct reporting-period group after choosing a system.'); From 4986db4781ab5c1a3622a6fe52cac0900a707307 Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:39 -0500 Subject: [PATCH 09/14] Add reusable library-system scopes (tests/browser/support/browserSmokeSupport.mjs) --- tests/browser/support/browserSmokeSupport.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/browser/support/browserSmokeSupport.mjs b/tests/browser/support/browserSmokeSupport.mjs index 85d7364..b7818fd 100644 --- a/tests/browser/support/browserSmokeSupport.mjs +++ b/tests/browser/support/browserSmokeSupport.mjs @@ -293,7 +293,7 @@ function buildDefaultQueryApiResponse(payload) { body: JSON.stringify({ schema_version: 1, generated_at: new Date().toISOString(), - scope: { library: payload.library || 'all', library_label: payload.library === 'MSU' ? 'Mississippi State University' : 'All MLP libraries', item_type_label: 'All item types', reporting_period: payload.reporting_period || '365' }, + scope: { library: payload.library || 'all', library_label: payload.library === 'system:MSU' ? 'Mississippi State University' : 'All MLP libraries', item_type_label: 'All item types', reporting_period: payload.reporting_period || '365' }, circulation: { checkouts: 880229, renewals: 487605, previous_checkouts: 842110, previous_renewals: 469220, checkout_change: 38119, renewal_change: 18385, checkout_change_rate: 0.045, renewal_change_rate: 0.039, comparison_available: true, comparison_coverage_complete: true, period_label: payload.reporting_period === 'fy:MSU:2027' ? 'FY 2027 to date (Jul 1, 2026–Aug 21, 2026)' : payload.reporting_period === 'cy:2026' ? 'Calendar Year 2026 to date' : 'Recent 365 days', renewal_share: 0.356, holds: 121843, holds_per_100_items: 4.3 }, collection: { items: 2813442, titles: 1601291, lifetime_checkouts: 12844308, lifetime_renewals: 5160244, in_house_uses: 843108, used_recently: 947835, recent_use_rate: 0.337, never_used: 1023995, never_used_rate: 0.364, checkouts_per_item: 4.6, total_value: 42850300, price_coverage: 0.71 }, patrons: { total: 618420, active: 183804, active_rate: 0.297, new: 42640, with_charges: 74482, with_holds: 18814, expiring_soon: 29711 }, @@ -309,7 +309,7 @@ function buildDefaultQueryApiResponse(payload) { patron_city_breakdown: [{ label: 'Tupelo, MS', patrons: 49220 }, { label: 'Other / unknown', patrons: 420700 }], patron_state_breakdown: [{ label: 'Mississippi', patrons: 582400 }, { label: 'Other / unknown', patrons: 36020 }], opportunities: [{ label: 'Older items with no recorded use', count: 618220, detail: 'Created more than five years ago with zero lifetime checkouts.' }], - filters: { libraries: [{ value: 'MSU', label: 'Mississippi State University' }], item_types: ['BOOK', 'EBOOK'], calendar_periods: [{ value: 'cy:2026', label: 'Calendar Year 2026 to date' }, { value: 'cy:2025', label: 'Calendar Year 2025' }], fiscal_periods_by_system: { MSU: [{ value: 'fy:MSU:2027', label: 'FY 2027 to date (Jul 1, 2026–Aug 21, 2026)', date_span: 'Jul 1, 2026–Aug 21, 2026' }, { value: 'fy:MSU:2026', label: 'FY 2026 (Jul 1, 2025–Jun 30, 2026)', date_span: 'Jul 1, 2025–Jun 30, 2026' }] } }, + filters: { systems: [{ value: 'system:MSU', code: 'MSU', label: 'Mississippi State University' }], libraries: [{ value: 'MSU-MAIN', label: 'MSU Main Library' }], item_types: ['BOOK', 'EBOOK'], calendar_periods: [{ value: 'cy:2026', label: 'Calendar Year 2026 to date' }, { value: 'cy:2025', label: 'Calendar Year 2025' }], fiscal_periods_by_system: { MSU: [{ value: 'fy:MSU:2027', label: 'FY 2027 to date (Jul 1, 2026–Aug 21, 2026)', date_span: 'Jul 1, 2026–Aug 21, 2026' }, { value: 'fy:MSU:2026', label: 'FY 2026 (Jul 1, 2025–Jun 30, 2026)', date_span: 'Jul 1, 2025–Jun 30, 2026' }] } }, privacy: { suppression_threshold: 10 }, sources: [{ label: 'Current item snapshot', detail: 'Aggregated test data.' }] }), From be5e1369c2ab531d25be4bee9bfea227ac280154 Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:41 -0500 Subject: [PATCH 10/14] Add reusable library-system scopes (tests/unit/ui/libraryDashboardModelLogic.mjs) --- tests/unit/ui/libraryDashboardModelLogic.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/ui/libraryDashboardModelLogic.mjs b/tests/unit/ui/libraryDashboardModelLogic.mjs index 5c5ee2d..ba09eca 100644 --- a/tests/unit/ui/libraryDashboardModelLogic.mjs +++ b/tests/unit/ui/libraryDashboardModelLogic.mjs @@ -10,6 +10,7 @@ test('library dashboard normalizes aggregate groups and filter metadata', () => patrons: { total: 32 }, library_breakdown: [{ label: 'Main', checkouts: '30' }], filters: { + systems: [{ value: 'system:MAIN', label: 'Main System' }], libraries: [{ value: 'MAIN', label: 'Main' }], item_types: ['BOOK'], calendar_periods: [{ value: 'cy:2026', label: 'Calendar Year 2026 to date' }] @@ -21,6 +22,7 @@ test('library dashboard normalizes aggregate groups and filter metadata', () => assert.equal(dashboard.circulation.coverage_complete, true); assert.equal(dashboard.libraryBreakdown[0].checkouts, 30); assert.equal(dashboard.filters.libraries[0].value, 'MAIN'); + assert.equal(dashboard.filters.systems[0].value, 'system:MAIN'); assert.equal(dashboard.filters.calendarPeriods[0].value, 'cy:2026'); assert.deepEqual(dashboard.availability, { circulation: true, collection: true, patrons: true }); assert.equal(libraryDashboardHasData(dashboard), true); From a462faca10131e3ff088c4c3ea604f3d2176cc08 Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:42 -0500 Subject: [PATCH 11/14] Add reusable library-system scopes (tests/unit/core/mockQueryBackendLogic.mjs) --- tests/unit/core/mockQueryBackendLogic.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/core/mockQueryBackendLogic.mjs b/tests/unit/core/mockQueryBackendLogic.mjs index e46119c..61cfb80 100644 --- a/tests/unit/core/mockQueryBackendLogic.mjs +++ b/tests/unit/core/mockQueryBackendLogic.mjs @@ -75,13 +75,13 @@ test('demo dashboard status includes clearly marked sample operational activity' test('demo library dashboard demonstrates comparisons, fiscal periods, and privacy-safe geography', async () => { const initialResponse = await handleDemoQueryRequest({ - body: JSON.stringify({ action: 'library_dashboard', library: 'MSU' }), + body: JSON.stringify({ action: 'library_dashboard', library: 'system:MSU' }), headers: authHeaders }); const initial = await initialResponse.json(); const reportingPeriod = initial.filters.fiscal_periods_by_system.MSU[0].value; const response = await handleDemoQueryRequest({ - body: JSON.stringify({ action: 'library_dashboard', library: 'MSU', reporting_period: reportingPeriod }), + body: JSON.stringify({ action: 'library_dashboard', library: 'system:MSU', reporting_period: reportingPeriod }), headers: authHeaders }); const payload = await response.json(); @@ -89,6 +89,8 @@ test('demo library dashboard demonstrates comparisons, fiscal periods, and priva assert.equal(payload.scope.reporting_period, reportingPeriod); assert.equal(payload.circulation.comparison_available, true); assert.equal(payload.circulation.fiscal_system, 'MSU'); + assert.equal(payload.scope.library, 'system:MSU'); + assert.ok(payload.filters.systems.some(system => system.value === 'system:MSU')); assert.match(payload.circulation.period_label, /^FY \d{4}.*\([A-Z][a-z]{2} \d{1,2}, \d{4}–[A-Z][a-z]{2} \d{1,2}, \d{4}\)$/u); assert.match(initial.filters.fiscal_periods_by_system.MSU[0].date_span, /^[A-Z][a-z]{2} \d{1,2}, \d{4}–[A-Z][a-z]{2} \d{1,2}, \d{4}$/u); assert.ok(payload.filters.fiscal_periods_by_system.MSU.length >= 3); From dc8b637a5d64f412bf5148f4cf9cf7bc4e7e3d82 Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:45 -0500 Subject: [PATCH 12/14] Add reusable library-system scopes (cache-bust.json) --- cache-bust.json | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/cache-bust.json b/cache-bust.json index c77c43c..d032c79 100644 --- a/cache-bust.json +++ b/cache-bust.json @@ -1,5 +1,5 @@ { - "version": "512de215e15f2828", + "version": "4571b2de4aa33cd0", "generatedBy": "scripts/updateCacheBusting.mjs", "assets": { "assets/app-icon-16.png": "6645731d86da1071", @@ -14,7 +14,7 @@ "assets/favicon-32.png": "c9b411f8fdc302f2", "assets/favicon.svg": "8596af3b51a53409", "backgroundNotificationServiceWorker.js": "2f8933df91d32a1a", - "index.html": "9f9af1d24e2a3e90", + "index.html": "120ac11359474cd7", "site.webmanifest": "7641f91a81ee63a2", "src/appModules.js": "f4135fb45be489bb", "src/components/date-picker/index.js": "c9a9363a3fdae5ee", @@ -54,8 +54,9 @@ "src/core/formatting/valueFormatting.js": "3f1e24d4cdc46608", "src/core/hydrationEta.js": "250ec4ee2633be0f", "src/core/icons.js": "51f534abf118b7e1", + "src/core/libraryScopes.js": "9fd73251bf6e3fcc", "src/core/mobileScrollLock.js": "d197a1dc4a683409", - "src/core/mockQueryBackend.js": "a69b05dccb087495", + "src/core/mockQueryBackend.js": "9147b5dea4adbced", "src/core/operatorSelectUtils.js": "4986a94dce50dcd9", "src/core/queryErrorDetails.js": "5c26531918b7a2c8", "src/core/queryExecution.js": "5fddde4c241c8699", @@ -189,10 +190,10 @@ "src/lib/workbook-export/workbookXmlUtils.js": "2cc0fcdda2a1d411", "src/lib/workbook-export/xlsxZipWriter.js": "5f4c1f6e98fbcb06", "src/styles/api-settings.css": "c5f76b60fb8684bb", - "src/styles/app.css": "09faabcfd35d38aa", + "src/styles/app.css": "410351104445b653", "src/styles/base.css": "8a925806001990b6", "src/styles/bib-compare.css": "144f2a1093e00dcf", - "src/styles/controls.css": "5372b30b175cd1e7", + "src/styles/controls.css": "30d36ccc65181c21", "src/styles/dashboard.css": "3d11ffe059411be7", "src/styles/filters.css": "9e7297e65dd68a3c", "src/styles/form-mode.css": "7f332bdcf58c2df0", @@ -232,15 +233,15 @@ "src/ui/cliPairing.js": "972206be3731df5f", "src/ui/controls/customDatePicker.js": "88e4eb3245ac256c", "src/ui/controls/searchUI.js": "9d46afc995531905", - "src/ui/controls/selectorControls.js": "bfcb8b0a6aba4bd4", + "src/ui/controls/selectorControls.js": "ee4bde178cbce906", "src/ui/controls/selectorListPasteInput.js": "4e7f16d036dfd795", "src/ui/controls/tableNameInput.js": "b6fc884c36961de3", "src/ui/controls/virtualList.js": "bd7a8d24c7c48493", - "src/ui/dashboard/kpiDashboard.js": "a30231379c3c6b6f", + "src/ui/dashboard/kpiDashboard.js": "6f73f5e9f2b62c49", "src/ui/dashboard/kpiDashboardModel.js": "fa4c42b18c1245e5", "src/ui/dashboard/kpiDashboardView.js": "82508c550975ada6", "src/ui/dashboard/libraryDashboardExport.js": "b756c909cbadc7e9", - "src/ui/dashboard/libraryDashboardModel.js": "ff0861a1889a9b32", + "src/ui/dashboard/libraryDashboardModel.js": "526c678143b0373e", "src/ui/dashboard/libraryDashboardView.js": "f46f69224a55ebe3", "src/ui/field-picker/buildableFieldPreview.js": "8abd486bb0a87fe2", "src/ui/field-picker/fieldPicker.js": "526a1bf934f5dd74", @@ -255,7 +256,7 @@ "src/ui/form-mode/formMode.js": "bac8e41c43689e9c", "src/ui/form-mode/formModeCard.js": "ec0b92176b97872b", "src/ui/form-mode/formModeCardBuilder.js": "f906542bfb990667", - "src/ui/form-mode/formModeControls.js": "ce0cb516c6920165", + "src/ui/form-mode/formModeControls.js": "b84b20e082760f7d", "src/ui/form-mode/formModeDateInput.js": "f08093e5d0548828", "src/ui/form-mode/formModeFieldPicker.js": "a6825b92b2a9fbb6", "src/ui/form-mode/formModePresentation.js": "caaa646c76cdf735", From b4422a46be2de64456c20458245b48783a517171 Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:20:51 -0500 Subject: [PATCH 13/14] Add shared library scope utilities --- src/core/libraryScopes.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/core/libraryScopes.js diff --git a/src/core/libraryScopes.js b/src/core/libraryScopes.js new file mode 100644 index 0000000..a3d5426 --- /dev/null +++ b/src/core/libraryScopes.js @@ -0,0 +1,21 @@ +const ALL_LIBRARY_SYSTEMS_LABEL = 'All library systems'; + +function normalizeLibraryScopeOptions(options = []) { + return (Array.isArray(options) ? options : []).map(option => typeof option === 'string' + ? { value: option, label: option } + : { value: option.value ?? option.code, label: option.label ?? option.name ?? option.code }); +} + +function buildLibraryScopeGroups(systems = [], libraries = []) { + return [ + { label: 'Library systems', options: normalizeLibraryScopeOptions(systems) }, + { label: 'Item libraries', options: normalizeLibraryScopeOptions(libraries) } + ].filter(group => group.options.length > 0); +} + +function systemCodeForLibraryScope(scope = '') { + const normalized = String(scope || '').replace(/^system:/u, ''); + return normalized && normalized !== 'all' ? normalized.split('-')[0].toUpperCase() : ''; +} + +export { ALL_LIBRARY_SYSTEMS_LABEL, buildLibraryScopeGroups, systemCodeForLibraryScope }; From d75687f958fbcc79e906e070fb716334e094fd7b Mon Sep 17 00:00:00 2001 From: Brandon Temple Date: Fri, 21 Aug 2026 17:28:19 -0500 Subject: [PATCH 14/14] Refresh stylesheet cache keys --- src/styles/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/app.css b/src/styles/app.css index ae27eb4..6ca2a25 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -5,7 +5,7 @@ @import url("./filters.css?v=9e7297e65dd68a3c"); @import url("./form-mode.css?v=7f332bdcf58c2df0"); @import url("./table.css?v=8bc187669db70079"); -@import url("./controls.css?v=5372b30b175cd1e7"); +@import url("./controls.css?v=30d36ccc65181c21"); @import url("./history.css?v=64598bfd565a29af"); @import url("./dashboard.css?v=3d11ffe059411be7"); @import url("./templates.css?v=1c0a1594bedc65b9");