diff --git a/form/js/widgets.js b/form/js/widgets.js index 30d2942..b448f86 100644 --- a/form/js/widgets.js +++ b/form/js/widgets.js @@ -44,7 +44,16 @@ const help = (ui && ui['ui:help']) || ''; const placeholder = (ui && ui['ui:placeholder']) || ''; const widget = (ui && ui['ui:widget']) || ''; - const readonly = !!(ui && ui['ui:readonly']); + // readonly is honored from either source: an explicit UI override + // (ui:readonly: true) or the schema's readOnly field. The latter + // is set by the server when augmenting from cascade-locked + // records: entries and for audit fields declared readOnly in the + // *.form.yaml. + const readonly = !!(schema.readOnly) || !!(ui && ui['ui:readonly']); + // x-labels: { code → label } turns a bare enum into a labeled + // dropdown ("ACM — Acme Inc" rather than just "ACM"). Injected + // by the server from the cascade's field_codes:codes map. + const labels = (schema && schema['x-labels']) || null; const autofocus = !!(ui && ui['ui:autofocus']); let input; @@ -90,17 +99,22 @@ if (widget === 'radio') { input = u.h('div', { className: 'form-field__radio-group' }); opts.forEach(function (opt, idx) { + const codeStr = String(opt); const radioId = id + '-' + idx; - const radio = u.h('input', { type: 'radio', name: id, id: radioId, value: String(opt) }); + const radio = u.h('input', { type: 'radio', name: id, id: radioId, value: codeStr }); if (value === opt) { radio.checked = true; } if (readonly) { radio.disabled = true; } + let displayText = codeStr; + if (labels && Object.prototype.hasOwnProperty.call(labels, codeStr)) { + displayText = codeStr + ' — ' + labels[codeStr]; + } const lbl = u.h('label', { for: radioId }); lbl.appendChild(radio); - lbl.appendChild(document.createTextNode(' ' + String(opt))); + lbl.appendChild(document.createTextNode(' ' + displayText)); input.appendChild(lbl); }); read = function () { @@ -113,7 +127,12 @@ input.appendChild(u.h('option', { value: '' }, '— select —')); } opts.forEach(function (opt) { - const o = u.h('option', { value: String(opt) }, String(opt)); + const codeStr = String(opt); + let displayText = codeStr; + if (labels && Object.prototype.hasOwnProperty.call(labels, codeStr)) { + displayText = codeStr + ' — ' + labels[codeStr]; + } + const o = u.h('option', { value: codeStr }, displayText); if (value === opt) { o.selected = true; } @@ -184,6 +203,12 @@ if (autofocus) { input.autofocus = true; } + // Schema-driven HTML pattern attribute. Used as a UX hint + // only — authoritative validation runs server-side via the + // cascade's field_codes. + if (schema.pattern && input.tagName === 'INPUT') { + input.pattern = schema.pattern; + } read = function () { return input.value === '' ? undefined : input.value; }; diff --git a/tables/js/save.js b/tables/js/save.js index c6f0f2c..ebd97b9 100644 --- a/tables/js/save.js +++ b/tables/js/save.js @@ -237,7 +237,23 @@ // Success: clear drafts + invalid marks, capture new ETag. const newEtag = resp.headers.get('ETag'); if (newEtag) row.etag = newEtag.replace(/"/g, ''); - row.data = merged; + // For record-typed writes the server echoes the stamped + // YAML (with server-managed audit fields) back as the + // response body — parse it and overwrite row.data so the + // table sees the same bytes that just landed on disk. + // Falls back to the local merge when the server didn't + // echo a body (non-record write or older server). + let serverData = null; + const ct = (resp.headers.get('Content-Type') || '').toLowerCase(); + if (ct.includes('yaml') && window.jsyaml) { + try { + const text = await resp.text(); + if (text && text.trim()) serverData = window.jsyaml.load(text); + } catch (e) { + console.warn('[tables] server response YAML parse failed; using local merge', e); + } + } + row.data = serverData || merged; delete app.state.drafts[rowId]; clearCellInvalid(rowId); setRowState(rowId, '');