feat(archive,landing): local-mode ?projects= filter + ?v= propagation

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/<version>) 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) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-04-28 17:24:07 -05:00
parent 89c5ec064d
commit f8a3da2ea1
3 changed files with 70 additions and 1 deletions

View file

@ -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)) {

View file

@ -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() {

View file

@ -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 });