Skip to content

SpecBridge hands ObjectGrid a bare exportOptions array, so a spec-authored view's declared formats are silently ignored #4585

Description

@yinlianghui

Found while landing objectui#4535 (PR #4584). Out of that card's surface — filed rather than fixed there.

What happens

packages/react/src/spec-bridge/bridges/list-view.ts builds a renderer node of type: 'object-grid' (:125) and copies the spec's export options across verbatim:

if (spec.exportOptions) node.exportOptions = spec.exportOptions;   // :158

That node is rendered by ObjectGridRenderer (ComponentRegistry.register('object-grid', …)), i.e. by ObjectGrid. ObjectGrid reads the OBJECT form and only that:

const declared = schema.exportOptions?.formats || ['csv', 'json'];   // ObjectGrid.tsx:1697

At objectui's pinned @objectstack/spec@17.0.0-rc.6, ListView.exportOptions is a bare format ARRAY — ('csv' | 'xlsx' | 'json' | 'pdf')[] — not an object. So .formats on it is undefined, the default ['csv', 'json'] wins, and the view's declared formats are dropped with no error, no warning and no console line.

Net effect for an author: a spec-canonical list view declaring exportOptions: ['csv', 'xlsx'] and routed through SpecBridge renders an export menu offering csv and json. The declared xlsx never appears; an undeclared json does. !!schema.exportOptions is still truthy (a non-empty array), so the export button itself shows — the failure is silent rather than absent.

Why the spec's array lift does not save it

objectstack#8010 gave ListViewExportOptionsSchema a parse-time lift: a stored bare array becomes { formats: [...] } when the spec schema parses it. That lift never runs on this path. The bridge's input is a TypeScript type — type ListViewSpec = Partial< ListView > — not a parsed value, and there is no parse or safeParse anywhere under packages/react/src/spec-bridge/. So the bridge forwards whatever its host handed it. A host that parses first is fine; a host that passes raw metadata is not, and nothing in the bridge distinguishes them.

This is why bumping the spec pin alone will not close it.

Pinned in the current tests, so it reads as intended

packages/react/src/spec-bridge/__tests__/P1SpecBridge.test.ts:390-397 asserts the passthrough:

it('should pass through exportOptions string[] format', () => {
  // …
  expect(node.exportOptions).toEqual(['csv', 'xlsx']);
});

The assertion is about the bridge's output shape and never renders it, so the bridge is green while the grid downstream cannot read what it produced.

Repro

  1. Build a spec ListView with exportOptions: ['csv', 'xlsx'].
  2. Run it through SpecBridge and render the resulting node.
  3. Open the export menu: it offers CSV and JSON. Expected: CSV and XLSX (XLSX subject to the server-stream gate).

Direction (for triage, not a decision)

The producer is where this belongs — a consumer-side Array.isArray fallback in ObjectGrid would be a second de-facto contract for one spec key, which is the shape objectstack#8010 was filed against. Two candidates worth weighing:

  • Lift in the bridge: normalize a bare array to { formats: [...] } at :158, mirroring the spec's own parse-time lift, so the bridge emits one shape regardless of what the host hands it. Cheap, local, and keeps the renderer strict.
  • Parse in the bridge: run the spec schema over the input so the lift and every other spec-side coercion apply. Larger change and a behavior change for hosts currently passing fragments (the input is deliberately Partial).

ListView (plugin-list) already normalizes both spellings for its own toolbar (ListView.tsx:1104-1113), so whichever lands, the bridge path is the one that lacks it.

Related


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions