From f8a3da2ea13bfc96397b36860ca3b014d530630c Mon Sep 17 00:00:00 2001 From: ZDDC Date: Tue, 28 Apr 2026 17:24:07 -0500 Subject: [PATCH] feat(archive,landing): local-mode ?projects= filter + ?v= propagation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small additions to the project-filter / channel-selector flow that already worked end-to-end for HTTP-mode but were missing in the local File-System-Access path and across landing→archive navigation: * archive: scanLocalRecursive now applies window.app.projectFilter at depth 0, mirroring the HTTP source's existing filter at source.js:316. Loading archive.html?projects=A,B in local mode (file://) now virtually merges A and B into one combined view, same as HTTP mode does today. * landing: openArchive() reads ?v= from its own URL and passes it through to the archive.html link it generates. This keeps the user on the same channel (alpha/beta/stable/) when they cross from the project picker to the archive — without it, alpha-channel users would silently drop back to whatever the deployment-default channel is at the archive.html boundary. Test exercises the local-mode filter via the existing mock-fs-api fixture: three top-level projects, projectFilter set to {A, B}, scan produces only A's and B's files. (The url-state.restore() URL parsing path is well-trodden in the HTTP case — the test sets projectFilter directly to isolate the new source.js change from a pre-existing init() fragility in the mock environment.) Co-Authored-By: Claude Opus 4.7 (1M context) --- archive/js/source.js | 7 ++++++ landing/js/landing.js | 9 ++++++- tests/archive.spec.js | 55 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) 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 });