feat(form): ui:mirrorFrom — reflect a sibling field into a read-only field
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: <sibling>` 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) <noreply@anthropic.com>
This commit is contained in:
parent
9341c47937
commit
7dfedc2342
4 changed files with 66 additions and 0 deletions
|
|
@ -71,6 +71,29 @@
|
||||||
fs.appendChild(childWidget.el);
|
fs.appendChild(childWidget.el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cross-field mirror: a field with `ui:mirrorFrom: <sibling>`
|
||||||
|
// 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 {
|
return {
|
||||||
el: fs,
|
el: fs,
|
||||||
path: path,
|
path: path,
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,39 @@ test.describe('form/ — safety check-in renderer', () => {
|
||||||
await expect(page.locator('#table-title')).toContainText('Safety Check-In');
|
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 }) => {
|
test('add/remove hazard rows works', async ({ page }) => {
|
||||||
await loadFormWithContext(page, {
|
await loadFormWithContext(page, {
|
||||||
schema: SAFETY_SCHEMA,
|
schema: SAFETY_SCHEMA,
|
||||||
|
|
|
||||||
|
|
@ -122,5 +122,10 @@ schema:
|
||||||
title: Previous SHA
|
title: Previous SHA
|
||||||
readOnly: true
|
readOnly: true
|
||||||
ui:
|
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:
|
notes:
|
||||||
ui:widget: textarea
|
ui:widget: textarea
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,11 @@ schema:
|
||||||
title: Previous SHA
|
title: Previous SHA
|
||||||
readOnly: true
|
readOnly: true
|
||||||
ui:
|
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:
|
description:
|
||||||
ui:widget: textarea
|
ui:widget: textarea
|
||||||
mitigation:
|
mitigation:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue