Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/issue-5821-table-column-editable-mirror.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@object-ui/types': patch
---

Add `editable` to the rich `TableColumnSchema` zod mirror. The `TableColumn` interface declares `editable?: boolean` and `data-table` honours it, but the mirror omitted the key, so a non-strict parse silently stripped it — and since the renderer treats absence as `true`, a column an author locked with `editable: false` came out of validation editable again. Columns locked with `editable: false` now stay locked through any pipeline that parses metadata via `@object-ui/types/zod` (including the CLI `validate` route).
56 changes: 48 additions & 8 deletions packages/types/src/__tests__/static-table-narrow-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ const RETIRED_COLUMN_KEYS: Record<string, unknown> = {
cell: () => 'x',
};

/** Every key the rich `TableColumn` interface declares. `satisfies` keeps the
* list honest against renames; the `Equal` pin keeps it EXHAUSTIVE — a key
* added to the interface without a deliberate mirror decision fails
* type-check here before the zod parity test can even run (objectui#5821). */
const RICH_COLUMN_KEYS = [
'header',
'accessorKey',
'className',
'cellClassName',
'width',
'minWidth',
'align',
'fixed',
'type',
'sortable',
'filterable',
'resizable',
'editable',
'cell',
] as const satisfies readonly (keyof TableColumn)[];
type _RichKeyListExhaustive = Expect<Equal<(typeof RICH_COLUMN_KEYS)[number], keyof TableColumn>>;

const STATIC_TABLE = {
type: 'table',
caption: 'Recent Orders',
Expand Down Expand Up @@ -173,19 +195,33 @@ describe('static `table` — the narrow zod surface refuses the retired keys (ob

describe('rich `TableColumn` — NOT narrowed by the split (ruling scope, objectui#5474)', () => {
it('the rich zod column still accepts every interactive key it declares', () => {
// `editable` is absent here on purpose: the rich ZOD mirror has never
// declared it (the TS interface does) — a pre-existing drift on the rich
// surface, outside this card's scope and tracked separately. Feeding it
// here would test the drift, not the split.
const richKeys = Object.fromEntries(
Object.entries(RETIRED_COLUMN_KEYS).filter(([k]) => k !== 'editable'),
);
// `editable` included since objectui#5821: the rich ZOD mirror declares
// it now, closing the drift the split had to leave tracked separately.
const result = TableColumnSchema.safeParse({
header: 'Amount',
accessorKey: 'amount',
...RETIRED_COLUMN_KEYS,
});
expect(result.success).toBe(true);
});

it('`editable: false` SURVIVES the rich parse — a locked column stays locked (objectui#5821)', () => {
// Acceptance alone cannot pin this: a non-strict z.object() ACCEPTS an
// undeclared key and silently STRIPS it, and the renderer treats absence
// as editable (`col.editable !== false`, data-table.tsx) — so before the
// mirror declared `editable`, this exact parse succeeded green while
// re-opening the locked column. The pin is the key surviving into the
// parsed OUTPUT, not the parse succeeding.
const result = TableColumnSchema.safeParse({
header: 'Amount',
accessorKey: 'amount',
...richKeys,
editable: false,
});
expect(result.success).toBe(true);
if (result.success) {
expect('editable' in result.data).toBe(true);
expect(result.data.editable).toBe(false);
}
});

it('`data-table` columns still parse with the rich keys authored', () => {
Expand Down Expand Up @@ -214,6 +250,10 @@ describe('the split itself — narrow = rich key set, live = the measured read s
);
});

it('the rich zod mirror declares exactly the interface key set — nothing silently strippable (objectui#5821)', () => {
expect(Object.keys(shapeOf(TableColumnSchema)).sort()).toEqual([...RICH_COLUMN_KEYS].sort());
});

it('the static table zod tombstones exactly `hoverable` and `striped`', () => {
expect(tombstonedKeys(TableZod).sort()).toEqual(['hoverable', 'striped']);
});
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/zod/data-display.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const TableColumnSchema = z.object({
sortable: z.boolean().optional().describe('Whether column is sortable'),
filterable: z.boolean().optional().describe('Whether column is filterable'),
resizable: z.boolean().optional().describe('Whether column is resizable'),
editable: z.boolean().optional().describe('Whether column is editable (for inline editing)'),
cell: z.function().optional().describe('Custom cell renderer'),
});

Expand Down
Loading