Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions cache-bust.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "4571b2de4aa33cd0",
"version": "edb06878dc13225f",
"generatedBy": "scripts/updateCacheBusting.mjs",
"assets": {
"assets/app-icon-16.png": "6645731d86da1071",
Expand All @@ -14,7 +14,7 @@
"assets/favicon-32.png": "c9b411f8fdc302f2",
"assets/favicon.svg": "8596af3b51a53409",
"backgroundNotificationServiceWorker.js": "2f8933df91d32a1a",
"index.html": "120ac11359474cd7",
"index.html": "4529b829b6a02986",
"site.webmanifest": "7641f91a81ee63a2",
"src/appModules.js": "f4135fb45be489bb",
"src/components/date-picker/index.js": "c9a9363a3fdae5ee",
Expand Down Expand Up @@ -54,7 +54,7 @@
"src/core/formatting/valueFormatting.js": "3f1e24d4cdc46608",
"src/core/hydrationEta.js": "250ec4ee2633be0f",
"src/core/icons.js": "51f534abf118b7e1",
"src/core/libraryScopes.js": "9fd73251bf6e3fcc",
"src/core/libraryScopes.js": "73010d8dc269a106",
"src/core/mobileScrollLock.js": "d197a1dc4a683409",
"src/core/mockQueryBackend.js": "9147b5dea4adbced",
"src/core/operatorSelectUtils.js": "4986a94dce50dcd9",
Expand Down Expand Up @@ -190,11 +190,11 @@
"src/lib/workbook-export/workbookXmlUtils.js": "2cc0fcdda2a1d411",
"src/lib/workbook-export/xlsxZipWriter.js": "5f4c1f6e98fbcb06",
"src/styles/api-settings.css": "c5f76b60fb8684bb",
"src/styles/app.css": "410351104445b653",
"src/styles/app.css": "faab68de08b308c6",
"src/styles/base.css": "8a925806001990b6",
"src/styles/bib-compare.css": "144f2a1093e00dcf",
"src/styles/controls.css": "30d36ccc65181c21",
"src/styles/dashboard.css": "3d11ffe059411be7",
"src/styles/dashboard.css": "d674dda33be597fe",
"src/styles/filters.css": "9e7297e65dd68a3c",
"src/styles/form-mode.css": "7f332bdcf58c2df0",
"src/styles/help.css": "1e75cc7af857e66a",
Expand Down Expand Up @@ -237,7 +237,7 @@
"src/ui/controls/selectorListPasteInput.js": "4e7f16d036dfd795",
"src/ui/controls/tableNameInput.js": "b6fc884c36961de3",
"src/ui/controls/virtualList.js": "bd7a8d24c7c48493",
"src/ui/dashboard/kpiDashboard.js": "6f73f5e9f2b62c49",
"src/ui/dashboard/kpiDashboard.js": "e2688fe53204cea3",
"src/ui/dashboard/kpiDashboardModel.js": "fa4c42b18c1245e5",
"src/ui/dashboard/kpiDashboardView.js": "82508c550975ada6",
"src/ui/dashboard/libraryDashboardExport.js": "b756c909cbadc7e9",
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@ <h2 class="mt-8 mb-0 px-3 py-2 font-semibold flex items-center justify-between">
<button type="button" role="tab" aria-selected="false" data-kpi-view="operations">Query activity</button>
</div>
<div class="kpi-dashboard-toolbar" aria-label="Dashboard filters">
<label for="kpi-dashboard-library"><span>Library or system</span><select id="kpi-dashboard-library"><option value="all">All library systems</option></select></label>
<div class="kpi-dashboard-filter"><span>Library or system</span><div id="kpi-dashboard-library" class="kpi-dashboard-library-selector"></div></div>
<label for="kpi-dashboard-item-type"><span>Item type</span><select id="kpi-dashboard-item-type"><option value="all">All item types</option></select></label>
<label for="kpi-dashboard-window"><span>Reporting period</span><select id="kpi-dashboard-window"><option value="90">Last 90 days</option><option value="365" selected>Last 12 months</option><option value="730">Last 24 months</option></select></label>
<button id="kpi-dashboard-export" class="kpi-dashboard-refresh" type="button">Export</button>
Expand Down
3 changes: 2 additions & 1 deletion scripts/lib/queryCli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function printUsage(stream = process.stdout) {
npm run query:api -- --action ACTION [--payload request.json|-] [--set key=value] [--output response.json]
npm run query:compat -- [--api-url URL] [--json]
npm run query:status -- [--api-url URL] [--json]
npm run query:dashboard -- [--library CODE] [--item-type CODE] [--active-window-days 90|365|730] [--output dashboard.json]
npm run query:dashboard -- [--library CODE] [--item-type CODE] [--active-window-days 90|365|730] [--reporting-period PERIOD] [--output dashboard.json]
npm run query:plan -- --config query.json [--output plan.json]
npm run query:cancel -- --query-id QUERY_ID
npm run query:results -- --query-id QUERY_ID [--format xlsx|csv|json|jsonl] [--output results.xlsx] [--include-duplicates]
Expand Down Expand Up @@ -935,6 +935,7 @@ async function runDashboardCommand(options = {}) {
library: String(options.library || 'all'),
item_type: String(options['item-type'] || options.itemType || 'all'),
active_window_days: Number(options['active-window-days'] || options.activeWindowDays || 365),
reporting_period: String(options['reporting-period'] || options.reportingPeriod || options['active-window-days'] || options.activeWindowDays || 365),
force_refresh: Boolean(options.refresh)
};
const data = await postJson(apiUrl, payload, options);
Expand Down
21 changes: 20 additions & 1 deletion src/core/libraryScopes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,28 @@ function buildLibraryScopeGroups(systems = [], libraries = []) {
].filter(group => group.options.length > 0);
}

function buildLibraryScopeSelectorValues(systems = [], libraries = []) {
const systemOptions = normalizeLibraryScopeOptions(systems).map(option => ({
RawValue: option.value,
Display: option.label,
Group: 'Library systems'
}));
const libraryOptions = normalizeLibraryScopeOptions(libraries).map(option => ({
RawValue: option.value,
Display: option.label,
Group: systemCodeForLibraryScope(option.value) || 'Item libraries'
}));
return [...systemOptions, ...libraryOptions];
}

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 };
export {
ALL_LIBRARY_SYSTEMS_LABEL,
buildLibraryScopeGroups,
buildLibraryScopeSelectorValues,
systemCodeForLibraryScope
};
2 changes: 1 addition & 1 deletion src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@import url("./table.css?v=8bc187669db70079");
@import url("./controls.css?v=30d36ccc65181c21");
@import url("./history.css?v=64598bfd565a29af");
@import url("./dashboard.css?v=3d11ffe059411be7");
@import url("./dashboard.css?v=d674dda33be597fe");
@import url("./templates.css?v=1c0a1594bedc65b9");
@import url("./api-settings.css?v=c5f76b60fb8684bb");
@import url("./site-update.css?v=b16b51ef4c7bd942");
Expand Down
14 changes: 13 additions & 1 deletion src/styles/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@
backdrop-filter: blur(12px);
}

.kpi-dashboard-toolbar label {
.kpi-dashboard-toolbar label,
.kpi-dashboard-filter {
display: grid;
gap: 0.28rem;
color: var(--theme-text-muted);
Expand All @@ -112,6 +113,17 @@
text-transform: uppercase;
}

.kpi-dashboard-library-selector,
.kpi-dashboard-library-selector .form-mode-popup-list-control {
min-width: 0;
width: 100%;
}

.kpi-dashboard-library-selector .form-mode-popup-list-trigger {
min-height: 2.55rem;
width: 100%;
}

.kpi-dashboard-toolbar select,
.kpi-dashboard-refresh {
min-height: 2.55rem;
Expand Down
52 changes: 36 additions & 16 deletions src/ui/dashboard/kpiDashboard.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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 { ALL_LIBRARY_SYSTEMS_LABEL, buildLibraryScopeSelectorValues, systemCodeForLibraryScope } from '../../core/libraryScopes.js';
import { onDOMReady } from '../../core/domReady.js';
import { SelectorControls } from '../controls/selectorControls.js';
import { libraryDashboardHasData, normalizeLibraryDashboard } from './libraryDashboardModel.js';
import { downloadLibraryDashboardCsv } from './libraryDashboardExport.js';
import { normalizeDashboardRuns, summarizeDashboardRuns } from './kpiDashboardModel.js';
Expand Down Expand Up @@ -47,18 +48,37 @@ 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 selectedLibraryScope(control) {
const selected = control?.getSelectedValues?.() || [];
return selected[0] || 'all';
}

function replaceLibraryOptions(container, systems, libraries, selected) {
if (!container) return;
const values = buildLibraryScopeSelectorValues(systems, libraries);
const validSelection = values.some(option => option.RawValue === selected) ? [selected] : [];
const signature = JSON.stringify(values.map(option => [option.RawValue, option.Display, option.Group]));
if (container.dataset.optionsSignature === signature && container.getSelectedValues) {
container.setSelectedValues(validSelection);
return;
}

container.querySelector('.form-mode-popup-list-control')?._cleanupPopup?.();
const selector = SelectorControls.createGroupedSelector(values, false, validSelection, {
enableGrouping: true,
allSelectionLabel: ALL_LIBRARY_SYSTEMS_LABEL,
allSelectionDescription: 'Include every library system.',
containerId: null
});
const popup = SelectorControls.createPopupListControl(
selector,
'Library or system',
ALL_LIBRARY_SYSTEMS_LABEL
);
container.replaceChildren(popup);
container.dataset.optionsSignature = signature;
container.getSelectedValues = () => popup.getSelectedValues();
container.setSelectedValues = valuesToSet => popup.setSelectedValues(valuesToSet);
}

function syncFilterOptions(elements) {
Expand All @@ -67,7 +87,7 @@ function syncFilterOptions(elements) {
elements.library,
libraryData.filters.systems,
libraryData.filters.libraries,
elements.library?.value || 'all'
selectedLibraryScope(elements.library)
);
replaceOptions(elements.itemType, 'All item types', libraryData.filters.itemTypes, elements.itemType?.value || 'all');
syncPeriodOptions(elements);
Expand All @@ -76,7 +96,7 @@ function syncFilterOptions(elements) {
function syncPeriodOptions(elements) {
if (!elements.period || !libraryData) return;
const selected = elements.period.value || '365';
const library = elements.library?.value || 'all';
const library = selectedLibraryScope(elements.library);
const system = systemCodeForLibraryScope(library);
const rolling = [
{ value: '90', label: 'Last 90 days' },
Expand Down Expand Up @@ -139,7 +159,7 @@ function requestPayload(elements) {
const reportingPeriod = elements.period?.value || '365';
return {
action: 'library_dashboard',
library: elements.library?.value || 'all',
library: selectedLibraryScope(elements.library),
item_type: elements.itemType?.value || 'all',
active_window_days: /^\d+$/.test(reportingPeriod) ? Number(reportingPeriod) : 365,
reporting_period: reportingPeriod
Expand Down
28 changes: 17 additions & 11 deletions tests/browser/browserSmoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -519,8 +519,8 @@ async function runSmokeTest() {
cardValues: Array.from(panel.querySelectorAll('.kpi-card__value')).map(node => node.textContent.trim()),
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),
librarySelection: panel.querySelector('#kpi-dashboard-library')?.getSelectedValues?.() || [],
librarySummary: panel.querySelector('#kpi-dashboard-library .form-mode-popup-list-summary')?.textContent?.trim() || '',
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 || ''
Expand All @@ -531,10 +531,8 @@ async function runSmokeTest() {
|| dashboardState.cardValues[2] !== '2,813,442'
|| dashboardState.chartCount !== 6
|| dashboardState.opportunityRows !== 1
|| !dashboardState.libraryOptions.includes('system:MSU')
|| !dashboardState.libraryOptions.includes('MSU-MAIN')
|| !dashboardState.libraryGroups.includes('Library systems')
|| !dashboardState.libraryGroups.includes('Item libraries')
|| dashboardState.librarySelection.length !== 0
|| dashboardState.librarySummary !== 'All library systems'
|| !dashboardState.exportVisible
|| !/up 38,119/iu.test(dashboardState.comparisonText)
|| dashboardState.selectedTab !== 'overview'
Expand All @@ -558,7 +556,15 @@ 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('system:MSU');
await page.locator('#kpi-dashboard-library .form-mode-popup-list-trigger').click();
const libraryDialog = page.getByRole('dialog', { name: 'Library or system' });
if (!await libraryDialog.getByRole('button', { name: 'All library systems' }).count()) {
throw new Error('Dashboard should reuse the grouped selector and expose the all-systems choice.');
}
await libraryDialog.getByPlaceholder('Search options...').fill('Mississippi State University');
await libraryDialog.getByText('Mississippi State University', { exact: true }).click();
await libraryDialog.getByRole('button', { name: 'Done' }).click();
await page.waitForFunction(() => document.querySelector('#kpi-dashboard-library')?.getSelectedValues?.()[0] === '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.');
Expand Down Expand Up @@ -2071,7 +2077,7 @@ async function runSmokeTest() {
await mobilePage.locator('#post-filter-value-picker-host .form-mode-popup-list-trigger').click();
await mobilePage.locator('.form-mode-popup-list-popup:not([hidden])').waitFor({ state: 'visible', timeout: 5000 });
await expectElementWithinViewport(mobilePage, '.form-mode-popup-list-popup:not([hidden])', 'Mobile popup list picker');
await expectLightInput(mobilePage, '.form-mode-popup-list-popup input[type="search"]', 'Mobile popup list search input');
await expectLightInput(mobilePage, '.form-mode-popup-list-popup:not([hidden]) input[type="search"]', 'Mobile popup list search input');
const popupAutoFocus = await mobilePage.locator('.form-mode-popup-list-popup:not([hidden])').evaluate(popup => {
const active = document.activeElement;
return {
Expand All @@ -2083,10 +2089,10 @@ async function runSmokeTest() {
if (!popupAutoFocus.popupFocused || ['INPUT', 'TEXTAREA', 'SELECT'].includes(popupAutoFocus.activeTag)) {
throw new Error(`Mobile popup list should open without auto-focusing a text control: ${JSON.stringify(popupAutoFocus)}`);
}
await expectMobileEditableFocusContained(mobilePage, '.form-mode-popup-list-popup input[type="search"]', '.form-mode-popup-list-popup-body', 'Mobile popup list search input');
await expectMinimumTapTarget(mobilePage, '.form-mode-popup-list-done', 'Mobile popup list done control');
await expectMobileEditableFocusContained(mobilePage, '.form-mode-popup-list-popup:not([hidden]) input[type="search"]', '.form-mode-popup-list-popup:not([hidden]) .form-mode-popup-list-popup-body', 'Mobile popup list search input');
await expectMinimumTapTarget(mobilePage, '.form-mode-popup-list-popup:not([hidden]) .form-mode-popup-list-done', 'Mobile popup list done control');
await expectNoHorizontalOverflow(mobilePage, 'Mobile popup list picker');
await mobilePage.locator('.form-mode-popup-list-done').click();
await mobilePage.locator('.form-mode-popup-list-popup:not([hidden]) .form-mode-popup-list-done').click();
await mobilePage.locator('#post-filter-done-btn').click();
await expectMobileScrollLockReleased(mobilePage, 'Mobile post filter dialog');
await cleanupMobilePageScroll(mobilePage);
Expand Down
6 changes: 4 additions & 2 deletions tests/unit/features/queryCliLogic.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -315,18 +315,20 @@ test('dashboard CLI requests the same scoped aggregate used by the interface', a
const outputPath = join(tmpdir(), `query-cli-dashboard-${Date.now()}.json`);
try {
const result = await runDashboardCommand({
library: 'MSU',
library: 'system:MSU',
'item-type': 'EBOOK',
'active-window-days': '90',
'reporting-period': 'fy:MSU:2027',
output: outputPath,
'api-url': 'https://example.test/query',
sessionStore: { read: async () => ({ token: 'test-session-token' }) }
});
assert.deepEqual(payload, {
action: 'library_dashboard',
library: 'MSU',
library: 'system:MSU',
item_type: 'EBOOK',
active_window_days: 90,
reporting_period: 'fy:MSU:2027',
force_refresh: false
});
assert.equal(result.schemaVersion, 1);
Expand Down