From 7dfedc23421983cea46a5356b77975efe667012c Mon Sep 17 00:00:00 2001 From: ZDDC Date: Thu, 21 May 2026 15:44:43 -0500 Subject: [PATCH] =?UTF-8?q?feat(form):=20ui:mirrorFrom=20=E2=80=94=20refle?= =?UTF-8?q?ct=20a=20sibling=20field=20into=20a=20read-only=20field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The project-rollup forms derive originator from the selected Package (party folder) server-side, so the field is read-only and was blank until submit. Add a declarative `ui:mirrorFrom: ` hint: the object renderer wires the named sibling's input to the field so the read-only originator updates live as the user picks a party — the composing tracking number is visible while filling the form. Display only; the server stays authoritative via the cascade's folder_fields. Set `ui:mirrorFrom: party` on originator in the embedded default-project-{mdl,rsk}.form.yaml. Generic hint, not hardcoded field names, so operators can reuse it. Test: form-safety.spec.js — filling the source field updates the read-only target; the target is not editable. Co-Authored-By: Claude Opus 4.7 (1M context) --- form/js/object.js | 23 +++++++++++++ tests/form-safety.spec.js | 33 +++++++++++++++++++ .../handler/default-project-mdl.form.yaml | 5 +++ .../handler/default-project-rsk.form.yaml | 5 +++ 4 files changed, 66 insertions(+) diff --git a/form/js/object.js b/form/js/object.js index 32adc9f..b6ab950 100644 --- a/form/js/object.js +++ b/form/js/object.js @@ -71,6 +71,29 @@ fs.appendChild(childWidget.el); } + // Cross-field mirror: a field with `ui:mirrorFrom: ` + // shows the live value of that sibling. Used by the project- + // rollup forms so the read-only `originator` reflects the + // selected Package (party) — the party folder is the + // originator's source of truth. Display-only: the server is + // still authoritative via the cascade's folder_fields. + for (let i = 0; i < ordered.length; i++) { + const name = ordered[i]; + const mirrorFrom = ui && ui[name] && ui[name]['ui:mirrorFrom']; + if (!mirrorFrom || !children[name] || !children[mirrorFrom]) { + continue; + } + const targetInput = children[name].el.querySelector('input, select, textarea'); + const sourceInput = children[mirrorFrom].el.querySelector('input, select, textarea'); + if (!targetInput || !sourceInput) { + continue; + } + const sync = function () { targetInput.value = sourceInput.value; }; + sourceInput.addEventListener('input', sync); + sourceInput.addEventListener('change', sync); + sync(); // initialize from any pre-filled party value + } + return { el: fs, path: path, diff --git a/tests/form-safety.spec.js b/tests/form-safety.spec.js index 50d4acc..59e4c6f 100644 --- a/tests/form-safety.spec.js +++ b/tests/form-safety.spec.js @@ -91,6 +91,39 @@ test.describe('form/ — safety check-in renderer', () => { await expect(page.locator('#table-title')).toContainText('Safety Check-In'); }); + test('ui:mirrorFrom reflects a sibling field into a read-only field', async ({ page }) => { + // The project-rollup forms use this so the read-only originator + // shows the selected Package (party) — the party folder is the + // originator's source of truth. + await loadFormWithContext(page, { + title: 'Rollup deliverable', + schema: { + type: 'object', + required: ['party'], + properties: { + party: { type: 'string', title: 'Package' }, + originator: { type: 'string', title: 'Originator', readOnly: true }, + }, + }, + ui: { originator: { 'ui:mirrorFrom': 'party' } }, + data: null, + submitUrl: '/test/rollup.form.html', + }); + await page.waitForFunction( + () => document.getElementById('form-root') && document.getElementById('form-root').children.length > 0, + null, + { timeout: 5000 }, + ); + + const inputs = page.locator('#form-root input[type="text"]'); + await expect(inputs).toHaveCount(2); + const party = inputs.nth(0); + const originator = inputs.nth(1); + await expect(originator).not.toBeEditable(); // read-only + await party.fill('0330C1'); + await expect(originator).toHaveValue('0330C1'); + }); + test('add/remove hazard rows works', async ({ page }) => { await loadFormWithContext(page, { schema: SAFETY_SCHEMA, diff --git a/zddc/internal/handler/default-project-mdl.form.yaml b/zddc/internal/handler/default-project-mdl.form.yaml index 318f68b..7fac862 100644 --- a/zddc/internal/handler/default-project-mdl.form.yaml +++ b/zddc/internal/handler/default-project-mdl.form.yaml @@ -122,5 +122,10 @@ schema: title: Previous SHA readOnly: true ui: + # originator is server-derived from the selected Package (party + # folder); mirror the party value into its read-only field so the + # composing tracking number is visible as the user fills the form. + originator: + ui:mirrorFrom: party notes: ui:widget: textarea diff --git a/zddc/internal/handler/default-project-rsk.form.yaml b/zddc/internal/handler/default-project-rsk.form.yaml index 94ebd0d..5b04c3d 100644 --- a/zddc/internal/handler/default-project-rsk.form.yaml +++ b/zddc/internal/handler/default-project-rsk.form.yaml @@ -152,6 +152,11 @@ schema: title: Previous SHA readOnly: true ui: + # originator is server-derived from the selected Package (party + # folder); mirror the party value into its read-only field so the + # composing tracking number is visible as the user fills the form. + originator: + ui:mirrorFrom: party description: ui:widget: textarea mitigation: