Skip to content

fix(types): exportOptions matches the spec's object form — pdf retired, streaming typed (#4535) - #4584

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4535-export-options-reconcile
Aug 13, 2026
Merged

fix(types): exportOptions matches the spec's object form — pdf retired, streaming typed (#4535)#4584
yinlianghui merged 1 commit into
mainfrom
claude/issue-4535-export-options-reconcile

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Part of #4535

Lands the objectui half of the exportOptions reconciliation that objectstack#8010 opened. Items 1–3 of the card; item 4 is measured, answered, and left as the card's residue for the reason below.

Gate-check first: the pin does NOT carry the new spec form

The card's four items split on whether objectui's pinned @objectstack/spec already carries ListViewExportOptionsSchema (the objectstack#8324 change). Measured on this branch's base (eb7f586b6):

  • packages/types/package.json and packages/plugin-grid/package.json both request ^17.0.0-rc.6; pnpm-lock.yaml resolves @objectstack/spec@17.0.0-rc.6.
  • That dist declares the legacy bare array, 'pdf' included, and no streaming key at all:
node_modules/@objectstack/spec/dist/ui/index.js:4052
exportOptions: zod.z.array(zod.z.enum(["csv", "xlsx", "pdf", "json"])).optional()
.describe("Available export format options"),
node_modules/@objectstack/spec/dist/view.zod-CN_gQt7k.d.ts:1380
exportOptions: z.ZodOptional< z.ZodArray< z.ZodEnum< {
json: "json"; csv: "csv"; xlsx: "xlsx"; pdf: "pdf";
} > > >;

So the new object form is not importable from the pin, and this PR restates the five keys locally instead of deriving them from the spec symbol. The shape it restates IS the new spec shape, so nothing here changes when the pin bumps — only the option of deriving it opens up. That is stated in the type's own doc comment rather than left for the next reader to rediscover.

Per the ruling, this is the items-1-to-3 branch, and the PR opens Part of #4535 rather than Fixes.

What landed

Item 1 — the type and its false comment. The five keys are now one exported ListViewExportOptions in packages/types/src/objectql.ts: formats, maxRecords, includeHeaders, fileNamePrefix, streaming. The old comment claimed alignment with the spec's ListViewSchema.exportOptions while the spec declared an array — false in both directions. The replacement names the symbol (ListViewExportOptionsSchema), the version (@objectstack/spec 17.0.0), the issues (objectstack#8010 / objectstack#8324), and the pin lag, so the claim is checkable.

NamedListView.exportOptions — the saved-view half in the same file, which declared the same four keys separately — now references the same type. Two authoring surfaces for one spec key cannot drift apart if there is only one declaration. This was measured before it was done: the downstream consumer sweep (below) is green, so nothing was relying on the two being different.

Item 2 — 'pdf' retired. The local format union is now the exported ListViewExportFormat = 'csv' | 'xlsx' | 'json'. PDF export was declined platform-side (objectstack#1301 NOT_PLANNED) and the value left the spec's enum in 17.0.0, where authoring it is a parse-time refusal carrying os migrate meta --from 16. ObjectGrid.handleExport takes that type instead of the inline four-member union.

Item 3 — streaming typed, both casts deleted.(schema.exportOptions as any)?.streaming and (exportConfig as any)?.streaming are now plain reads.

Item 4: the renderer has no array tolerance to keep — and raw arrays can still reach it

The card asks to confirm which path feeds ObjectGrid its schema and to keep the renderer's own array tolerance only if raw un-parsed metadata can still reach it. Measured, the presumption behind the question does not hold, so reporting the measurement rather than forcing the form:

ObjectGrid has never had array tolerance. It reads schema.exportOptions?.formats and nothing else; there is no Array.isArray branch anywhere in its export region. plugin-list's ListView is the component that normalizes both spellings (ListView.tsx:1104-1113) — and it does not forward exportOptions to the child grid at all: the object-grid schema it builds (ListView.tsx:1844-1868) is an explicit key list that omits it, because ListView renders its own export toolbar. So nothing was kept or dropped here.

Raw bare arrays can still reach ObjectGrid, on a path that no parse touches: packages/react/src/spec-bridge/bridges/list-view.ts builds a node of type: 'object-grid' (:125) and assigns node.exportOptions = spec.exportOptions verbatim (:158). The bridge's input is a TypeScript Partial of the spec's ListView — there is no zod safeParse or parse anywhere under packages/react/src/spec-bridge/, so the spec's parse-time array lift never runs on this path, at this pin or after a bump. Its own pinned test asserts the passthrough (P1SpecBridge.test.ts:390-397, expecting node.exportOptions to equal ['csv', 'xlsx']). At the current pin the spec's canonical form IS the array, so this is the ordinary case, not an edge one, and the grid reads .formats off an array as undefined and falls back to the csv/json default.

Adding tolerance in the renderer is not the fix and is not in this card: a consumer-side Array.isArray fallback is a second de-facto contract, and the producer is where it belongs — the bridge should lift, or its host should hand it parsed metadata. Filed separately rather than smuggled in here; the residue is recorded on #4535, which stays open.

Red-first

Deleting the casts against the unfixed four-key type, before touching packages/types:

src/ObjectGrid.tsx(1701,32): error TS2339: Property 'streaming' does not exist on type '{ formats?: ("csv" | "json" | "xlsx" | "pdf")[] | undefined; maxRecords?: number | undefined; includeHeaders?: boolean | undefined; fileNamePrefix?: string | undefined; }'.
src/ObjectGrid.tsx(1744,24): error TS2339: Property 'streaming' does not exist on type '{ formats?: ("csv" | "json" | "xlsx" | "pdf")[] | undefined; maxRecords?: number | undefined; includeHeaders?: boolean | undefined; fileNamePrefix?: string | undefined; }'.

Both compile once the key is declared. That red is the whole of item 3: the cast was load-bearing only because the declaration was missing.

The card's constraint, as a test

"No sixth undeclared-but-read key" is now mechanical, in the shape of the objectui#4302 package-door guard.

packages/plugin-grid/src/__tests__/ObjectGrid.exportOptionsKeys.test.ts scans ObjectGrid.tsx for every property read off exportOptions — through schema.exportOptions and through any identifier bound to it — and scans ListViewExportOptions in packages/types for what it declares, then fails on anything read but not declared. It tolerates a wrapping paren, an as T assertion and ?. between the root and the key, precisely because a cast is how streaming stayed invisible for releases. packages/types/src/__tests__/objectql.exportOptions.test.ts pins the other direction at compile time: an exact keyof equality against the spec's five, plus @ts-expect-error pins on a retired 'pdf' and on an undeclared sixth key.

Reverse-verified, each restored byte-exact afterwards (sha256-checked, via patch and git checkout):

injectedresult
a sixth read key (exportConfig?.compression)drift pin red: expected [ 'compression' ] to deeply equal [], and the cast guard red too
a sixth declared key (compression?: string)TS2344 on the key-set assertion, TS2578 unused @ts-expect-error, and the pin's exact-five test red
'pdf' restored to the unionTS2344 on the format assertion, TS2578 unused @ts-expect-error

The drift pin also caught a false positive in its own first run — const declared = schema.exportOptions?.formats was being read as an alias of the options, so declared.filter(...) scanned as an undeclared key filter. Fixed with a lookahead and written down in the file, since that is the failure mode a future edit would hit again.

Pin moves, declared

exportGate.test.tsx's two 'pdf' cases are kept, re-worded, not deleted. The card expected the console.warn drop path to become dead; measured, it does not. The filter is format-agnostic (declared.filter(f => supported.includes(f))) with no 'pdf' branch, so it remains live for the case it was written for — xlsx declared with no server stream — and it keeps covering the legacy case for free: metadata stored before the retirement still carries 'pdf' until os migrate meta --from 16 rewrites it, and such a value must keep reaching the user as "absent from the menu" rather than as a dead menu item. Deleting those cases would drop live coverage to satisfy a presumption that measurement did not support. The test schema is already any, so no cast was needed and no assertion changed — only the prose saying what they now pin, and one title (drops pdf becomes drops a legacy pdf).

Verification


Generated by Claude Code

…d, streaming typed (#4535)
`ObjectGridSchema.exportOptions` carried four keys under a comment claiming
alignment with `@objectstack/spec`'s `ListViewSchema.exportOptions`. The comment
was false in both directions: the spec declared a bare format ARRAY, and the
renderer read a fifth key — `streaming` — that appeared in no declaration at
all, reachable only through an `as any` cast.
objectstack#8010 closed that upstream by declaring `ListViewExportOptionsSchema`
with exactly the five keys this renderer reads. This lands the objectui half:
- the five keys become one exported `ListViewExportOptions`, shared by
`ObjectGridSchema` and `NamedListView`, with a comment naming the spec symbol
and version it mirrors;
- `streaming` is declared and both `as any` casts are deleted;
- `'pdf'` is retired from the local format union (`ListViewExportFormat`) —
declined platform-side as objectstack#1301, gone from the spec enum in 17.0.0.
Two guards keep it from re-opening: a compile-time key-set assertion on the
type, and a source scan of every property `ObjectGrid` reads off `exportOptions`
that fails on any key the type does not declare.
Part of #4535
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 13, 2026 1:36pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-CU4y74xm.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)38.46KB10.17KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)489.33KB108.47KB
core (index.js)3.37KB1.34KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)163.56KB44.83KB
fields (index.js)230.37KB57.17KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.86KB12.91KB
plugin-charts (index.js)62.10KB17.67KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)121.04KB31.57KB
plugin-designer (index.js)212.58KB42.83KB
plugin-detail (index.js)239.93KB60.01KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.30KB40.02KB
plugin-grid (index.js)189.37KB50.33KB
plugin-kanban (index.js)52.74KB14.53KB
plugin-list (index.js)111.13KB27.12KB
plugin-map (index.js)18.16KB5.81KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.29KB11.05KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.09KB20.56KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.64KB9.44KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.26KB0.67KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 13, 2026 13:52
@yinlianghui
yinlianghui added this pull request to the merge queueAug 13, 2026
Merged via the queue into main with commit 1f9b905Aug 13, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4535-export-options-reconcile branch August 13, 2026 13:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yinlianghui@claude