diff --git a/tables/js/editor.js b/tables/js/editor.js index 4271322..f75b622 100644 --- a/tables/js/editor.js +++ b/tables/js/editor.js @@ -231,6 +231,16 @@ const propSchema = propertySchemaFor(col); + // Read-only cells (schema readOnly:true — e.g. the folder-bound + // originator the server derives from the party folder, or + // server-managed audit fields) can't be edited: any value the + // user typed would be overwritten on write. Suppress edit entry + // entirely; selection still works for keyboard navigation, same + // as the $-prefixed synthesized columns above. + if (propSchema && propSchema.readOnly) { + return; + } + // Complex-type cells (nested object, generic array, oneOf) // can't be inline-edited cleanly — punt to the row's form // editor in a side panel / new page. Phase 2 ships the diff --git a/tables/js/save.js b/tables/js/save.js index 79503ad..7797636 100644 --- a/tables/js/save.js +++ b/tables/js/save.js @@ -376,8 +376,28 @@ const newEtag = (resp.headers.get('ETag') || '').replace(/"/g, ''); row.yamlUrl = location; row.url = location ? location + '.html' : row.url; + // Re-fetch the just-written row so server-derived fields + // surface immediately: folder-bound originator, the composed + // tracking number's components, and audit stamps. The local + // `merged` lacks these (e.g. originator is read-only and + // never typed). Fall back to merged if the GET fails. row.data = merged; - row.etag = newEtag || null; + if (location) { + try { + const back = await fetch(location, { credentials: 'same-origin' }); + if (back.ok) { + const text = await back.text(); + if (text && text.trim() && window.jsyaml) { + row.data = window.jsyaml.load(text) || merged; + } + const fetchedEtag = (back.headers.get('ETag') || '').replace(/"/g, ''); + if (fetchedEtag) row.etag = fetchedEtag; + } + } catch (e) { + console.warn('[tables] post-create re-fetch failed; using local merge', e); + } + } + if (!row.etag) row.etag = newEtag || null; row.isNew = false; // Move the drafts entry (was keyed on the synthetic id) to // the new url, then clear it (data has the merged values). @@ -417,6 +437,20 @@ return { status: 'forbidden' }; } + if (resp.status === 409) { + // The composed tracking number collides with an existing + // row (the server rejects duplicates). Surface it on the + // sequence cell — the usual disambiguator — rather than the + // generic errored state, so the user knows to bump a + // component instead of retrying the same values. + let msg = 'Duplicate tracking number — change a component (e.g. sequence).'; + try { const t = await resp.text(); if (t && t.trim()) msg = t.trim(); } catch (_) { /* ignore */ } + clearCellInvalid(rowId); + markCellInvalid(rowId, 'sequence', msg); + setRowState(rowId, 'invalid'); + return { status: 'duplicate', message: msg }; + } + console.warn('[tables] createRow returned', resp.status); setRowState(rowId, 'errored'); return { status: 'http-error', code: resp.status }; diff --git a/tests/tables.spec.js b/tests/tables.spec.js index 5a4ace2..8ab2f23 100644 --- a/tests/tables.spec.js +++ b/tests/tables.spec.js @@ -536,6 +536,44 @@ test.describe('tables/ — directory-of-YAML table view', () => { expect(target).toContain('.yaml.html'); }); + test('Phase 2: readOnly column suppresses the inline editor', async ({ page }) => { + // Self-contained fixture: a folder-bound / server-managed field + // (schema readOnly:true) must not become an editable cell, while + // a normal sibling column still edits. + await loadTableWithContext(page, { + columns: [ + { field: 'originator', title: 'Originator' }, + { field: 'title', title: 'Title' }, + ], + rows: [{ + url: '/Working/MDL/ACM-PRJ-EL-SPC-0001.yaml.html', + data: { originator: 'ACM', title: 'Spec' }, + editable: true, + }], + rowSchema: { + type: 'object', + properties: { + originator: { type: 'string', readOnly: true }, + title: { type: 'string' }, + }, + }, + }); + await page.waitForSelector('#table-root tbody tr'); + + const ro = page.locator('#table-root tbody tr').nth(0) + .locator('[role="gridcell"]').nth(0); + await ro.dblclick(); + // No inline editor mounts; the displayed value is untouched. + await expect(ro.locator('.zddc-table__cell-input')).toHaveCount(0); + await expect(ro).toHaveText('ACM'); + + // A normal column in the same table still edits. + const editable = page.locator('#table-root tbody tr').nth(0) + .locator('[role="gridcell"]').nth(1); + await editable.dblclick(); + await expect(editable.locator('input.zddc-table__cell-input')).toBeVisible(); + }); + test('Phase 2: no rowSchema → falls back to plain text editor', async ({ page }) => { await loadTableWithContext(page, { // No rowSchema in the context — same as a directory with