From d5638e96977cc1b85b2f07cb42ac691de1521c3b Mon Sep 17 00:00:00 2001 From: ZDDC Date: Mon, 11 May 2026 11:25:14 -0500 Subject: [PATCH] feat(landing): MDL card with party dropdown on project view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Master Deliverables List section was a long prose block ("To edit the MDL: 1. open the archive, 2. click into a party folder, 3. click mdl…") followed by a bullet list of party links — visually inconsistent with the four stage cards above it. Replaced by a fifth card in the .stages grid styled like the others: heading + short description + an inline select + Open button. The select populates from the same fetchParties() helper that backed the old ; selecting a party + clicking Open navigates to //archive//mdl/. Empty/error states: - No parties yet: select shows "(no party folders yet)"; hint copy expands to explain the URL-based fallback (zddc-server still auto-renders archive//mdl/ even when the folder is missing). - Network error: select shows "(could not enumerate parties)"; user can navigate via the URL bar. Updated landing.spec.js — the old "lists existing parties as direct MDL links" test now asserts on #mdlPartySelect contents + click-to-nav. Co-Authored-By: Claude Opus 4.7 (1M context) --- landing/css/landing.css | 54 ++++++++++++++++++++++ landing/js/landing.js | 99 ++++++++++++++++++++++++++++++----------- landing/template.html | 47 ++++++++----------- tests/landing.spec.js | 32 ++++++++----- 4 files changed, 165 insertions(+), 67 deletions(-) diff --git a/landing/css/landing.css b/landing/css/landing.css index 25b7a14..c02e91f 100644 --- a/landing/css/landing.css +++ b/landing/css/landing.css @@ -451,6 +451,60 @@ body { font-size: 0.875rem; } +/* MDL card variant: same outer styling as a stage card but contains + an interactive control (party populated with party folders and an + // Open button that opens the chosen party's MDL. The view + // auto-renders at any archive//mdl/ URL even when the + // folder doesn't exist on disk (zddc-server commit 3fc3717), + // so we offer the operator-supplied party list directly AND + // a "type a new party name" affordance via a free-text last + // option. + var mdlSelect = document.getElementById('mdlPartySelect'); + var mdlOpenBtn = document.getElementById('mdlOpenBtn'); + var mdlHint = document.getElementById('mdlHint'); + if (!mdlSelect || !mdlOpenBtn) return; + + // Wire the Open button regardless of fetch outcome — even if + // party enumeration fails, an operator can still navigate by + // typing the party folder name in the URL bar. + function openSelectedMdl() { + var party = mdlSelect.value; + if (!party) return; + var url = '/' + p + '/archive/' + encodeURIComponent(party) + '/mdl/'; + window.location.assign(url); + } + mdlOpenBtn.addEventListener('click', openSelectedMdl); + mdlSelect.addEventListener('change', function () { + mdlOpenBtn.disabled = !mdlSelect.value; + }); + // Enter inside the select also opens. + mdlSelect.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + openSelectedMdl(); + } + }); var parties = await fetchParties(p); + // Repopulate the select. mdlSelect starts with a single + // "Loading…" option; replace its contents either way. + mdlSelect.innerHTML = ''; if (parties == null) { - // Network error or unauthenticated — show neither list nor - // explicit "none" message. The page is still usable. - partySection.innerHTML = ''; + // Network error or unauthenticated. Leave the select + // disabled but visible; user can still navigate via URL. + var optErr = document.createElement('option'); + optErr.value = ''; + optErr.textContent = '(could not enumerate parties)'; + mdlSelect.appendChild(optErr); + mdlSelect.disabled = true; + mdlOpenBtn.disabled = true; return; } if (parties.length === 0) { - partySection.innerHTML = - '

No party folders yet. The MDL view auto-renders at any ' - + 'archive/<party>/mdl/ URL, even when the folder doesn\'t exist on ' - + 'disk — so you can start editing an MDL before any transmittals have been exchanged.

'; + // No parties yet, but per the hint the URL still works for + // any party name. Give a placeholder option that disables + // Open until the user has typed something — except we + // don't have a text input. So just say "(none yet)" and + // disable. Operator can still navigate via the URL bar. + var optNone = document.createElement('option'); + optNone.value = ''; + optNone.textContent = '(no party folders yet)'; + mdlSelect.appendChild(optNone); + mdlSelect.disabled = true; + mdlOpenBtn.disabled = true; + if (mdlHint) { + mdlHint.innerHTML = + 'No archive/<party>/ folders yet. The MDL view still ' + + 'auto-renders at any such URL, even before the folder exists — type a ' + + 'party name into the URL bar (or wait for the first transmittal) to start editing.'; + } return; } - var html = '

Direct links — parties currently in archive/:

' - + '
    '; + // Populate the select with each party. + var optPlaceholder = document.createElement('option'); + optPlaceholder.value = ''; + optPlaceholder.textContent = 'Choose a party…'; + mdlSelect.appendChild(optPlaceholder); for (var j = 0; j < parties.length; j++) { - var name = parties[j].name; - var url = parties[j].url; // server-provided absolute URL - html += '
  • ' + escapeHtml(name) + ' MDL →
  • '; + var opt = document.createElement('option'); + opt.value = parties[j].name; + opt.textContent = parties[j].name; + mdlSelect.appendChild(opt); } - html += '
'; - partySection.innerHTML = html; + mdlSelect.disabled = false; + // Open button stays disabled until the user picks something. + mdlOpenBtn.disabled = true; } // Returns an array of {name, url} for each party folder in the diff --git a/landing/template.html b/landing/template.html index e8e1436..58786ae 100644 --- a/landing/template.html +++ b/landing/template.html @@ -162,38 +162,27 @@

Reviewing

Pending review responses — inbound submittals paired with their in-progress drafts.

+ +
+

Master Deliverables List

+

The editable list of expected deliverables for each counterparty.

+
+ + + +
+

+ The MDL view renders even when archive/<party>/mdl/ doesn't yet exist — + you can start editing before any transmittals have been exchanged. +

+

Browse all files →

- -

Master Deliverables List (MDL)

-

Each counterparty in the archive has an MDL — an editable - table of expected deliverables. The default columns mirror - the ZDDC tracking-number components (originator, - phase, project, area, - discipline, type, - sequence, suffix) plus - title, plannedRevision, - plannedDate, status, and - owner.

- -

To edit the MDL for any party:

-
    -
  1. Open the project archive:
  2. -
  3. Click into a party's folder (e.g. PartyA)
  4. -
  5. Click mdl inside the party folder
  6. -
- -
- -
- -

To customize the columns or schema for a specific party, drop - a table.yaml and form.yaml into - archive/<party>/mdl/. Operator-supplied - files override the embedded defaults entirely.

diff --git a/tests/landing.spec.js b/tests/landing.spec.js index 033e9a6..b656c87 100644 --- a/tests/landing.spec.js +++ b/tests/landing.spec.js @@ -300,20 +300,28 @@ test.describe('Landing project mode', () => { }); }); - test('lists existing parties as direct MDL links', async ({ page }) => { + test('MDL card lists existing parties in a dropdown', async ({ page }) => { await page.goto(`${baseUrl}/Project-1`, { waitUntil: 'load' }); - // Wait for the async party fetch to populate. - await page.waitForSelector('.party-list a', { timeout: 5000 }); + // Wait for the async party fetch to populate the select. + await page.waitForFunction(() => { + const sel = document.getElementById('mdlPartySelect'); + return sel && !sel.disabled && sel.options.length > 1; + }, { timeout: 5000 }); - const links = await page.locator('.party-list a').allTextContents(); - expect(links.sort()).toEqual(['PartyA MDL →', 'PartyB MDL →']); - // Hidden dot-prefixed entry was filtered out. - const hrefs = await page.evaluate(() => - [...document.querySelectorAll('.party-list a')].map(a => a.getAttribute('href')) - ); - expect(hrefs).toContain('/Project-1/Archive/PartyA/mdl/'); - expect(hrefs).toContain('/Project-1/Archive/PartyB/mdl/'); - expect(hrefs.some(h => h.includes('.hidden'))).toBe(false); + const options = await page.locator('#mdlPartySelect option').allTextContents(); + // First option is the placeholder; the rest are party names. + expect(options[0]).toBe('Choose a party…'); + expect(options.slice(1).sort()).toEqual(['PartyA', 'PartyB']); + + // Selecting a party enables the Open button; clicking it navigates + // to the canonical //archive//mdl/ URL. + await page.selectOption('#mdlPartySelect', 'PartyA'); + await expect(page.locator('#mdlOpenBtn')).toBeEnabled(); + const [navUrl] = await Promise.all([ + page.waitForURL(/\/Project-1\/archive\/PartyA\/mdl\/$/, { timeout: 5000 }).then(() => page.url()), + page.click('#mdlOpenBtn'), + ]); + expect(navUrl).toMatch(/\/Project-1\/archive\/PartyA\/mdl\/$/); }); test('legacy presets are migrated to groups on first load', async ({ page }) => {