feat(landing): single-project click navigates to <project>/archive.html

Previously every project click — single or group — built
archive.html?projects=<list> and let the archive tool's URL-state
detection fan out from there. For a single project that's a
single-page-app trick that obscures the canonical URL.

Now single-project clicks navigate to <project>/archive.html instead.
The benefit is direct URL manipulation: the user can swap archive.html
for working/, staging/, reviewing/, archive/<party>/mdl/table.html etc.
in the address bar without going back through landing. zddc-server's
availability.go already auto-serves the right tool at each canonical
folder, so the destinations resolve without any server change.

Multi-project clicks (groups) keep the ?projects=A,B form because
there's no single subtree root. ACL-trimmed groups that collapse to
one project also take the new single-project path, since the result
is effectively a single-project view either way.

The ?v= channel selector continues to carry across both paths.

Two existing landing.spec.js assertions updated to match the new
single-project URL shape; multi-project assertion unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-09 19:46:14 -05:00
parent 8ba029612e
commit cc515b0f56
2 changed files with 23 additions and 3 deletions

View file

@ -429,8 +429,22 @@
function openArchiveWith(names) {
if (!names || names.length === 0) return;
var base = location.pathname.replace(/\/[^\/]*$/, '/');
var params = ['projects=' + names.map(encodeURIComponent).join(',')];
var v = new URLSearchParams(location.search).get('v');
if (names.length === 1) {
// Single project → canonical project-subtree URL so the user
// can edit the address bar to swap archive.html for
// working/, staging/, reviewing/, etc. zddc-server's
// availability.go auto-serves the right tool at each.
// Multi-project (the `else` branch) keeps the ?projects=
// form because there's no single subtree root.
var url = base + encodeURIComponent(names[0]) + '/archive.html';
if (v) url += '?v=' + encodeURIComponent(v);
navigate(url);
return;
}
var params = ['projects=' + names.map(encodeURIComponent).join(',')];
if (v) params.push('v=' + encodeURIComponent(v));
navigate(base + 'archive.html?' + params.join('&'));
}

View file

@ -66,7 +66,10 @@ test.describe('Landing page', () => {
await page.locator('.project-table-row[data-name="197072"]').click();
const navTo = await page.evaluate(() => window.__navTo);
expect(navTo).toContain('archive.html?projects=197072');
// Single-project click navigates to the project's canonical subtree
// (so the user can swap archive.html for working/, staging/, etc.).
expect(navTo).toContain('/197072/archive.html');
expect(navTo).not.toContain('?projects=');
});
test('column filters narrow the table; filters persist in URL', async ({ page }) => {
@ -217,8 +220,11 @@ test.describe('Landing page', () => {
await page.locator('#openSelectedBtn').click();
const navTo = await page.evaluate(() => window.__navTo);
expect(navTo).toContain('archive.html?projects=176109');
// After the visibility-filter trims to one project, this collapses
// to the single-project path (no ?projects= form).
expect(navTo).toContain('/176109/archive.html');
expect(navTo).not.toContain('197072');
expect(navTo).not.toContain('?projects=');
});
test('legacy presets are migrated to groups on first load', async ({ page }) => {