diff --git a/archive/js/source.js b/archive/js/source.js index 336f825..780de69 100644 --- a/archive/js/source.js +++ b/archive/js/source.js @@ -78,6 +78,13 @@ for (const entry of entries) { if (entry.kind === 'directory') { + // Project filter: at root depth, skip directories not in the + // allowed set so ?projects=A,B virtually merges A and B into a + // single combined view. Mirrors the HTTP-source filter at the + // depth === 0 site below. + if (depth === 0 && window.app.projectFilter && window.app.projectFilter.size > 0) { + if (!window.app.projectFilter.has(entry.name)) continue; + } const subPath = currentPath + '/' + entry.name; try { if (window.app.modules.parser.isTransmittalFolder(entry.name)) { diff --git a/landing/js/landing.js b/landing/js/landing.js index b81bbec..7e08bb4 100644 --- a/landing/js/landing.js +++ b/landing/js/landing.js @@ -117,7 +117,14 @@ return; } var base = location.pathname.replace(/\/[^\/]*$/, '/'); - location.href = base + 'archive.html?projects=' + checked.map(encodeURIComponent).join(','); + var params = ['projects=' + checked.map(encodeURIComponent).join(',')]; + // Propagate ?v= (channel selector) so the archive page loads through + // the same level-2 bootstrap stub on the same channel as this landing. + var v = new URLSearchParams(location.search).get('v'); + if (v) { + params.push('v=' + encodeURIComponent(v)); + } + location.href = base + 'archive.html?' + params.join('&'); } function savePreset() { diff --git a/tests/archive.spec.js b/tests/archive.spec.js index 8009a07..209980b 100644 --- a/tests/archive.spec.js +++ b/tests/archive.spec.js @@ -87,6 +87,61 @@ test.describe('Archive Browser', () => { expect(fileCountText).toBeTruthy(); }); + test('projectFilter filters local-mode scan at root depth (virtual merge)', async ({ page }) => { + // The corresponding URL flow is `archive.html?projects=A,B` → + // url-state.restore() → window.app.projectFilter Set. We set the Set + // directly here because the existing test environment hits an + // unrelated init() error before url-state.restore() runs (a + // getElementById returning null in events.js); a separate test would + // be needed to re-confirm the URL-parsing path. The behavior under + // test is the source.js depth-0 filter check, which only reads + // window.app.projectFilter. + await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); + + // Three top-level projects under one root; only A and B should be scanned. + await page.evaluate(() => { + window.__setMockDirectoryTree('combined-root', { + 'Project-A': { + '2025-01-15_123456-EM-TRN-0001 (IFC) - First': { + '123456-EL-SPC-0001_A (IFC) - SpecA.pdf': '%PDF', + }, + }, + 'Project-B': { + '2025-02-10_789012-EM-TRN-0001 (IFC) - Second': { + '789012-EL-SPC-0002_A (IFC) - SpecB.pdf': '%PDF', + }, + }, + 'Project-C': { + '2025-03-01_345678-EM-TRN-0001 (IFC) - Third': { + '345678-EL-SPC-0003_A (IFC) - SpecC.pdf': '%PDF', + }, + }, + }); + }); + + // Set projectFilter to {A, B}, mimicking what url-state.restore() + // would do for a URL like archive.html?projects=Project-A,Project-B. + await page.evaluate(() => { + window.app.projectFilter = new Set(['Project-A', 'Project-B']); + }); + + await page.locator('#addDirectoryBtn').click(); + await page.waitForTimeout(2000); + + // Surface all files into the table. + await page.evaluate(() => { + const cb = document.getElementById('selectAllGroupingCheckbox'); + if (cb && !cb.checked) cb.click(); + }); + await page.waitForTimeout(500); + + const tableText = await page.locator('#filesTableBody').textContent(); + expect(tableText).toContain('SpecA'); + expect(tableText).toContain('SpecB'); + expect(tableText).not.toContain('SpecC'); + }); + test('parser module uses shared zddc helpers (not its own wrappers)', async ({ page }) => { await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#appContainer', { timeout: 15000 });