feat(tables): read-only cells, show server-derived fields on create, clearer conflict
- editor.js: suppress edit entry for cells whose schema is readOnly (folder-bound originator, server-managed audit fields) — mirrors the $-prefixed synthesized-column guard. The server overwrites these, so inline-editing them was misleading and the value was silently lost. - save.js createRow: on 201, re-fetch the written row so server-derived fields (originator from the party folder, the composed tracking number's components, audit stamps) surface immediately instead of staying blank until reload. Falls back to the local merge if the GET fails. - save.js createRow: handle 409 (duplicate composed tracking number) with a clear message on the sequence cell instead of the generic errored state. Test: tables.spec.js — a readOnly column doesn't mount an inline editor while a normal sibling still edits. The 409 + re-fetch paths go through the in-dir create POST (formCreateUrl), which the file:// Playwright harness can't intercept; both are covered by the server e2e. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
875827d484
commit
9341c47937
3 changed files with 83 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue