From 752ba281b546fd99f453171a29b6049b6a927a47 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 00:22:28 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(plugin-grid):=20object-grid=20=E7=9A=84?= =?UTF-8?q?=20data=20=E8=BE=93=E5=85=A5=E6=8C=89=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=E4=B8=BA=20ViewData=20=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `GRID_QUERY_INPUTS` 把 `data` 声明成 `{ type: 'array', label: 'Static Data', description: 'Inline rows, …' }`,而契约是 `ObjectGridSchema.data?: ViewData` —— spec 以 `provider` 辨识的联合,四支全是 strict 对象(object / api / value / schema),没有数组那一支。也就是说这条声明描述的是 `staticData` 的形状,而 `staticData` 正是 #4648 裁定明确不发布的已弃用别名。 两个方向都会伤到作者:照设计器面板(或生成的 sdui-intrinsics.d.ts)写 `data: [ …行… ]`,渲染能跑但 `tsc` 报 TS2322、spec 解析也不过;而唯一同时满足 两者的 `{ provider: 'value', items: [...] }` 反被本仓自己的保存门禁报 `type-mismatch` —— 因为声明的 `array` 支只接受数组。平台在它唯一合法的写法上 自相矛盾,正是 #4041 的形状换了一个键。 改动只在声明面:type 改为 `object`,label 改为 `Data Source`,description 如实 列出四支 provider 并说明内联行走 `{ provider: 'value', items: [...] }`。 `ObjectGrid.tsx` 的数组容忍支路不动(#5068 家族),它与 `staticData` 同等地位 —— 作为后向兼容被读取,但不作为授权面发布。 钉子取提取式:声明面的支从注册表读出,期望值由 `ViewDataSchema` 自己的判定推导, 两侧任一漂移即红;编译期一半靠 `@ts-expect-error` 钉住 `ViewData` 没有数组支。 Fixes #5090 Co-authored-by: Claude --- .../grid-data-input-declares-viewdata-5090.md | 30 +++ .../__tests__/gridDataInputContract.test.ts | 171 ++++++++++++++++++ packages/plugin-grid/src/index.tsx | 23 ++- 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 .changeset/grid-data-input-declares-viewdata-5090.md create mode 100644 packages/plugin-grid/src/__tests__/gridDataInputContract.test.ts diff --git a/.changeset/grid-data-input-declares-viewdata-5090.md b/.changeset/grid-data-input-declares-viewdata-5090.md new file mode 100644 index 0000000000..8e4c787beb --- /dev/null +++ b/.changeset/grid-data-input-declares-viewdata-5090.md @@ -0,0 +1,30 @@ +--- +'@object-ui/plugin-grid': patch +--- + +`object-grid` now declares its `data` input as the object its contract actually +accepts, instead of as an array (objectui#5090). + +The declaration published `{ name: 'data', type: 'array', label: 'Static Data', +description: 'Inline rows, …' }` — which is the shape of `staticData`, the +deprecated alias the objectui#4648 carve-out deliberately leaves unpublished, +under the canonical key's name. The contract is +`ObjectGridSchema.data?: ViewData`: the spec's discriminated union on +`provider`, four strict object arms (`object` / `api` / `value` / `schema`), +none of them an array. + +Both halves of that misdeclaration were user-visible. An author following the +designer panel or the generated `sdui-intrinsics.d.ts` wrote `data: [ …rows… ]` +and got a grid that renders but a document `tsc` rejects (TS2322) and spec +parsing refuses; meanwhile the one form that satisfies both, +`{ provider: 'value', items: [...] }`, was reported as `type-mismatch` by the +save gate, because a declared `array` arm accepts only arrays. Writing the +inline-rows form the README already documents now validates clean, and the +designer labels the key `Data Source` with a description that names all four +providers rather than only the deprecated shortcut's shape. + +The renderer is unchanged: a bare array is still honoured as back-compat +(`getDataConfig` folds it to `{ provider: 'value', items }`), it is simply no +longer advertised as authoring surface — the same standing `staticData` has. +Authors who wrote the array shorthand keep working and will now see a +`type-mismatch` hint pointing at the spec-valid spelling. diff --git a/packages/plugin-grid/src/__tests__/gridDataInputContract.test.ts b/packages/plugin-grid/src/__tests__/gridDataInputContract.test.ts new file mode 100644 index 0000000000..bcff74d40c --- /dev/null +++ b/packages/plugin-grid/src/__tests__/gridDataInputContract.test.ts @@ -0,0 +1,171 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `object-grid` — the DECLARED coarse type of `data` must be the one its + * contract accepts (objectui#5090). + * + * ## What was wrong + * + * `GRID_QUERY_INPUTS` declared `{ name: 'data', type: 'array', label: 'Static + * Data', description: 'Inline rows, …' }`, but the contract is + * `ObjectGridSchema.data?: ViewData` — the spec's discriminated union on + * `provider`, four strict OBJECT arms (`object` / `api` / `value` / `schema`), + * none of them an array. The declaration published the shape of `staticData`, + * the deprecated alias the objectui#4648 carve-out deliberately refuses to + * publish, under the canonical key's name. + * + * Both halves failed quietly, in opposite directions — objectui#4041's shape, + * one field over: + * + * - an author following the published declaration wrote `data: [ …rows… ]`, + * which the renderer does honour (`getDataConfig`'s `Array.isArray` branch) + * but which `tsc` refuses (`TS2322`, measured) and `ViewDataSchema` refuses; + * - the one form that satisfies both — `{ provider: 'value', items: [...] }` — + * drew `type-mismatch` from this repo's own save gate, because + * `checkType`'s `'array'` arm (`sdui-parser/src/validate.ts`) accepts only + * arrays. The platform reported the only legal write as a type error. + * + * ## What these tests pin, and why it is DERIVED + * + * The subject is the declaration read out of the registry, never a constant + * restated here — a test spelling `'object'` on both sides would keep passing + * through exactly the drift it exists to catch. So the expected arms come from + * `ViewDataSchema`'s own verdicts: one representative value per coarse arm, in + * the vocabulary `armAccepts` uses, filtered by what the schema accepts. Either + * side moving turns the comparison red — a spec release that widens `ViewData` + * to accept an array, or a declaration that grows an arm the contract rejects. + * That derivation is the discipline `component-input-union-specimens.test.ts` + * and `text-input-inputs-spec-parity.test.ts` adopted, and it is the only gate + * on this fact: whether a declared arm matches the contract is otherwise + * nobody's (objectui#4971). + * + * The renderer's array tolerance is NOT asserted away here. It stays as + * back-compat and is objectui#5068's family; what this file forbids is + * ADVERTISING it, which is the carve-out's own reasoning applied to an arm + * instead of a key. + * + * The compile-time half needs this package's `tsconfig.test.json` + * (objectui#3181) — without it the `@ts-expect-error` below is erased before + * vitest runs and proves nothing. + */ + +import { describe, it, expect } from 'vitest'; +import { ComponentRegistry } from '@object-ui/core'; +import { ViewDataSchema } from '@objectstack/spec/ui'; +import type { ObjectGridSchema } from '@object-ui/types'; +// Registers `object-grid` and its `view:grid` alias. +import '../index'; + +/** The two tags this one renderer is published under, sharing one input list. */ +const GRID_TAGS = [ + { label: 'object-grid', type: 'object-grid', namespace: undefined }, + { label: 'view:grid', type: 'grid', namespace: 'view' }, +] as const; + +/** + * One representative value per coarse arm, in the vocabulary `armAccepts` uses + * (`packages/sdui-parser/src/validate.ts`). + * + * The `object` probe is the INLINE ROWS form specifically — the shape the old + * declaration was reaching for and got wrong — so a spec rename of `items` + * empties the derived set and reds this file with a message about the probe + * rather than silently agreeing with a broken declaration. + */ +const COARSE_ARM_PROBES = [ + ['string', 'users'], + ['number', 42], + ['boolean', true], + ['object', { provider: 'value', items: [{ id: 1 }] }], + ['array', [{ id: 1 }]], +] as const; + +/** The coarse arms the CONTRACT accepts, from the schema's own verdicts. */ +const contractAcceptedArms = (): string[] => + COARSE_ARM_PROBES.filter(([, value]) => ViewDataSchema.safeParse(value).success).map( + ([arm]) => arm, + ); + +/** The `data` input as the registration publishes it. */ +function declaredDataInput(type: string, namespace?: string) { + const inputs = (ComponentRegistry.getConfig(type, namespace) as any)?.inputs ?? []; + return inputs.find((i: any) => i?.name === 'data'); +} + +/** Declared arms, normalised across the single-kind and array forms. */ +const declaredArms = (type: string, namespace?: string): string[] => { + const declared = declaredDataInput(type, namespace)?.type; + return Array.isArray(declared) ? [...declared] : [declared]; +}; + +describe('object-grid `data` — declaration matches the contract (objectui#5090)', () => { + it.each(GRID_TAGS)('$label registers and declares a `data` input', ({ type, namespace }) => { + // Reachability before absence: an unregistered or renamed block would + // satisfy every assertion below by never resolving an input at all. + expect(ComponentRegistry.getConfig(type, namespace), `${type} is not registered`).toBeDefined(); + expect(declaredDataInput(type, namespace), `${type} declares no \`data\` input`).toBeDefined(); + }); + + it('the contract accepts exactly the object arm (the derivation, stated)', () => { + // Non-vacuity for everything below: if `ViewDataSchema` stopped resolving + // through `lazySchema`, or the probe went stale, this set would empty out + // and the comparison would pass on nothing. + expect(contractAcceptedArms()).toEqual(['object']); + }); + + it('each of the four providers parses, and none of them is an array', () => { + // What makes "the object arm" a real four-armed union rather than one + // accidental shape, and the direct measurement of the card's premise. + const providers = [ + { provider: 'object', object: 'users' }, + { provider: 'api', read: { url: 'https://example.test/rows' } }, + { provider: 'value', items: [{ id: 1 }] }, + { provider: 'schema', schemaId: 'report' }, + ]; + for (const value of providers) { + expect(ViewDataSchema.safeParse(value).success, `${value.provider} provider`).toBe(true); + expect(Array.isArray(value)).toBe(false); + } + }); + + it.each(GRID_TAGS)('$label declares the arms the contract accepts', ({ type, namespace }) => { + expect(declaredArms(type, namespace)).toEqual(contractAcceptedArms()); + }); + + it.each(GRID_TAGS)('$label does not declare the array shorthand', ({ type, namespace }) => { + // The regression named. `data: [ …rows… ]` is honoured by the renderer as + // back-compat (objectui#5068's family) but refused by `ViewData`, so + // declaring the arm would publish a shape `tsc` and the spec both reject — + // the same reason the objectui#4648 carve-out leaves `staticData` undeclared. + expect(declaredArms(type, namespace)).not.toContain('array'); + expect(ViewDataSchema.safeParse([{ id: 1 }]).success).toBe(false); + }); + + it('declares one shape across both tags, so the alias cannot drift', () => { + const [a, b] = GRID_TAGS.map(({ type, namespace }) => declaredArms(type, namespace)); + expect(a).toEqual(b); + }); + + it('is pinned at compile time too', () => { + // The runtime half above reads a declaration; this half reads the contract + // the declaration is supposed to describe. Erased before vitest runs — its + // value is that `tsc -p tsconfig.test.json` compiles it. + + // The inline-rows form the description now teaches must type-check. + const inlineRows: ObjectGridSchema['data'] = { provider: 'value', items: [{ id: 1 }] }; + expect(inlineRows).toBeDefined(); + + // …and the array shorthand must NOT. If `ViewData` ever widens to accept an + // array, this directive stops suppressing anything and `tsc` fails with + // "Unused '@ts-expect-error' directive" — which is the tripwire firing: + // re-derive the declared arms above instead of deleting this line. + // @ts-expect-error `ViewData` has no array arm — objectui#5090. + const bareArray: ObjectGridSchema['data'] = [{ id: 1 }]; + expect(bareArray).toBeDefined(); + }); +}); diff --git a/packages/plugin-grid/src/index.tsx b/packages/plugin-grid/src/index.tsx index 65564ebcfd..60714017a4 100644 --- a/packages/plugin-grid/src/index.tsx +++ b/packages/plugin-grid/src/index.tsx @@ -141,6 +141,27 @@ export const ObjectGridRenderer: React.FC<{ schema: any; [key: string]: any }> = * spellings — `columns`, `data`, `selection`, `pagination`, `searchableFields`, * `sort`, `filter`, `resizable`, `label` — are all declared here, and each * description below says so, so the exemption teaches rather than merely omits. + * + * ## `data` declares the CONTRACT's shape, not the shortcut's (objectui#5090) + * + * The key landed above with `type: 'array'`, labelled "Static Data" and described + * as inline rows — which is the shape of `staticData`, the very alias the + * carve-out above refuses to publish, not the shape of `data`. The contract is + * `ObjectGridSchema.data?: ViewData` (`packages/types/src/objectql.ts`), and + * `ViewData` is the spec's discriminated union on `provider` — four strict object + * arms (`object` / `api` / `value` / `schema`), none of them an array. So the + * declaration published a shape `tsc` rejects (`TS2322`) and `ViewDataSchema` + * refuses, while the one form that satisfies both — `{ provider: 'value', items: + * [...] }` — drew `type-mismatch` from this repo's own save gate, because + * `checkType`'s `'array'` arm accepts only arrays. The platform contradicted + * itself on the only legal write: objectui#4041's shape again, one field over. + * + * `ObjectGrid`'s renderer does still accept a bare array (`getDataConfig` folds + * it to `{ provider: 'value', items }`), and that tolerance is deliberately left + * alone here — it is objectui#5068's family, and declaring an arm for it would + * publish a shape the contract rejects, which is precisely the carve-out's own + * reasoning. An array `data` therefore has the same standing as `staticData`: + * read as back-compat, not advertised as authoring surface. */ const GRID_QUERY_INPUTS: ComponentInput[] = [ { name: 'objectName', type: 'string', label: 'Object Name', required: true }, @@ -152,7 +173,7 @@ const GRID_QUERY_INPUTS: ComponentInput[] = [ { name: 'sort', type: 'array', label: 'Sort', description: 'Initial sort order, `[{ field, order }]`. The canonical spelling — the deprecated single-sort `defaultSort` is only read when this is absent.' }, { name: 'pagination', type: 'object', label: 'Pagination', description: 'Pagination config, `{ pageSize, pageSizeOptions, … }`. Its presence is what enables paging; prefer it over the deprecated flat `pageSize` / `showPagination` pair.' }, { name: 'searchableFields', type: 'array', label: 'Searchable Fields', description: 'Fields the toolbar search box queries. A non-empty list is what enables search — prefer it over the deprecated boolean `showSearch`, which cannot say WHICH fields to search.' }, - { name: 'data', type: 'array', label: 'Static Data', description: 'Inline rows, which bypass the object query entirely. For demos and fixtures; the canonical spelling — the deprecated `staticData` is the same thing.' }, + { name: 'data', type: 'object', label: 'Data Source', description: 'Data source configuration — a `ViewData` object discriminated by `provider`: `{ provider: "object", object }` (what an omitted `data` falls back to, using `objectName`), `{ provider: "api", read, write }`, `{ provider: "value", items: [...] }` for inline rows that bypass the object query, or `{ provider: "schema", schemaId }`. The canonical spelling — the deprecated `staticData` is the array-only shortcut for the `value` provider, so inline rows go under `items` here rather than in a bare array.' }, // ── presentation ────────────────────────────────────────────────────────── { name: 'rowHeight', type: 'enum', label: 'Row Height', enum: ['compact', 'short', 'medium', 'tall', 'extra_tall'], description: 'Row density. An unrecognised value falls back to `compact` rather than erroring.' }, { name: 'frozenColumns', type: 'number', label: 'Frozen Columns', description: 'How many leading columns stay pinned while the grid scrolls horizontally.' }, From 516f6ab11118279b38c0c38b73e1b9faae8d8187 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 00:44:05 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(plugin-grid):=20README=20=E7=9A=84=20s?= =?UTF-8?q?rc/index.tsx=20=E8=A1=8C=E5=8F=B7=E9=94=9A=E7=82=B9=E8=B7=9F?= =?UTF-8?q?=E4=B8=8A=E5=A3=B0=E6=98=8E=E5=9D=97=E7=9A=84=E4=BD=8D=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一提交在 `GRID_QUERY_INPUTS` 前插入了 #5090 的说明段,`const` 与其后三处 `ComponentRegistry.register(` 的行号整体后移 21 行,README 里四处按行号指路的 引用因此失准(145 → 166、181 → 202、193 → 214、216 → 237)。 只改行号,不改任何叙述;`src/index.tsx:80` / `:88` 两处在插入点之前,未动。 Co-authored-by: Claude --- packages/plugin-grid/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugin-grid/README.md b/packages/plugin-grid/README.md index d35ca9e622..0f2527edc2 100644 --- a/packages/plugin-grid/README.md +++ b/packages/plugin-grid/README.md @@ -48,9 +48,9 @@ three calls in `src/index.tsx` claim exactly these keys: | `register(…)` call | Namespaced key | Bare fallback | | --- | --- | --- | -| `('object-grid', ObjectGridRenderer, { namespace: 'plugin-grid' })` — `src/index.tsx:181` | `plugin-grid:object-grid` | `object-grid` | -| `('grid', ObjectGridRenderer, { namespace: 'view', skipFallback: true })` — `src/index.tsx:193` | `view:grid` | **none** — `skipFallback: true` | -| `('import-wizard', ImportWizardRenderer, { namespace: 'plugin-grid' })` — `src/index.tsx:216` | `plugin-grid:import-wizard` | `import-wizard` | +| `('object-grid', ObjectGridRenderer, { namespace: 'plugin-grid' })` — `src/index.tsx:202` | `plugin-grid:object-grid` | `object-grid` | +| `('grid', ObjectGridRenderer, { namespace: 'view', skipFallback: true })` — `src/index.tsx:214` | `view:grid` | **none** — `skipFallback: true` | +| `('import-wizard', ImportWizardRenderer, { namespace: 'plugin-grid' })` — `src/index.tsx:237` | `plugin-grid:import-wizard` | `import-wizard` | **Bare `grid` is deliberately not ours.** `skipFallback: true` on the second call keeps this plugin from claiming it, because `grid` belongs to the CSS Grid *layout* @@ -148,7 +148,7 @@ contract rather than this package's component API. A grid node is an `ObjectGridSchema`: one required `objectName`, and keys drawn from the list this package **declares** as its authoring surface -(`GRID_QUERY_INPUTS`, `src/index.tsx:145`) — the same list that feeds the designer +(`GRID_QUERY_INPUTS`, `src/index.tsx:166`) — the same list that feeds the designer panel and the generated `sdui-intrinsics.d.ts`, so what is authorable here is what the renderer reads.