Skip to content

feat(grid): compute all eleven spec column summary aggregations (#2890) - #2897

Merged
os-zhuang merged 2 commits into
mainfrom
claude/unify-object-ui-types-zod-hc6qoc
Jul 28, 2026
Merged

feat(grid): compute all eleven spec column summary aggregations (#2890)#2897
os-zhuang merged 2 commits into
mainfrom
claude/unify-object-ui-types-zod-hc6qoc

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Closes the useColumnSummary items tracked in #2890.

The gap

ColumnSummarySchema accepts eleven aggregation names. useColumnSummary computed five. The other six — none, count_empty, count_filled, count_unique, percent_empty, percent_filledpassed validation at authoring time and then rendered a blank footer cell, with no error raised on either side.

objectstack#3761 widened the reachable surface: it promoted the { type, field } object form into the spec, and that form's type reuses this same enum.

What changed

The computation now splits into two families:

  • count/percent read raw cell values, before the numeric parse. So count_unique works on text, select and lookup columns, and a value that does not parse as a number still counts as a filled row.
  • sum/avg/min/max keep the existing numeric parse and column formatting.

A cell is empty when it is null, undefined, "" or an empty array — the convention already used by audit history display and form dirty-checking — so an unset multi-select reads as empty rather than as a filled []. count_unique keys objects and arrays by value; a raw Set compares by reference and would call every row distinct.

Two behavior changes fall out of the enum carrying both names

  • count is now every row. count_filled is the non-empty variant, and with both in the enum they cannot mean the same thing. Existing tests use data whose rows are all filled, so they do not distinguish the two; the only column that renders differently than before is one whose values are all empty.
  • A zero count renders Empty: 0 instead of collapsing to a blank cell. Zero is the answer to "how many are empty", not the absence of one — the old count > 0 ? count : null swallowed it.

Formatting

Column currency/percent formatting is now gated to the numeric family in one place, rather than the scattered type !== 'count' guards. Without this, count_unique on a currency column would render $3.00 instead of Unique: 3.

none

none and unrecognized names skip the entry entirely instead of registering a blank one, so a view whose columns all opt out renders no footer row rather than an empty one.

Types

The objectui-local { type, field } arm of ListColumnSchema was stuck at the same five names, which left the per-column field override unavailable for the six new aggregations. It now takes its vocabulary from SpecColumnSummarySchema by reference, so the shorthand and the object form cannot accept different names — and the delta drops to zero when that spec release lands and the .extend() goes away.

Removed: the second useColumnSummary

@object-ui/react exported a hook of the same name with no callers anywhere in the repo. Not a duplicate implementation so much as a competing one:

  • a different API — (data, config, locale) against the grid's (columns, data, fieldMetadata) — and a colliding ColumnSummaryConfig export of a different shape;
  • a doc comment claiming it implements "ColumnSummarySchema from @objectstack/spec v2.0.7", six majors behind;
  • a distinct aggregation not in the spec vocabulary at all — the spec calls that count_unique.

That last point is why it was worth removing rather than leaving inert: a publicly exported name the spec does not define is the second de-facto contract Commandment #0.1 warns about, and the parity guard below does not reach it.

Anti-drift guard

New summary-spec-parity.test.ts asserts the renderer's supported set equals the spec enum in both directions:

  • a spec name the renderer omits is the bug this PR fixes;
  • a renderer name the spec omits would be local dialect (Commandment #0).

Both new tests were verified against a deliberately broken implementation rather than assumed to work:

Sabotage Result
Roll the vocabulary back to the original 5 parity test lists the 5 unimplemented names
Make count_unique compare by reference array-by-value case fails (expected 2 to be 1)

Docs

Per Commandment #2. The footer summary was undocumented in both the package README and the plugin docs page; both now carry the full eleven-value table, the emptiness rule, and the formatting boundary.

Verification

Check Result
unit project (full) 271 files / 3703 tests pass
DOM project (full) 349 files / 3793 tests pass
new summary tests 55 DOM + 26 unit pass
type-check (types + plugin-grid) pass
lint 0 errors; new code contributes no any
clean rebuild of @object-ui/react after the removal pass, no stale exports

@objectstack/spec added as a devDependency of plugin-grid for the parity test — six other plugin-* packages already depend on it.

Noted, not changed

The footer renders {col.header}: {summary.label} while the label already carries its own prefix, producing Amount: Sum: 600. Pre-existing, and changing it is a design decision rather than a bug fix.

`ColumnSummarySchema` accepts eleven aggregation names; `useColumnSummary`
computed five. The other six — `none`, `count_empty`, `count_filled`,
`count_unique`, `percent_empty`, `percent_filled` — passed validation at
authoring time and then rendered a blank footer cell, with no error raised on
either side. objectstack#3761 widened the reachable surface further by
promoting the `{ type, field }` object form, whose `type` reuses this enum.

Implement the six, splitting the computation into two families:

- count/percent read *raw* cell values, before the numeric parse, so they work
  on text, select and lookup columns and a value that does not parse as a
  number still counts as a filled row. A cell is empty when it is null,
  undefined, "" or an empty array — the convention already used by audit
  history display and form dirty-checking — so an unset multi-select reads as
  empty rather than as a filled `[]`. `count_unique` keys objects and arrays by
  value; a raw Set compares by reference and would call every row distinct.
- sum/avg/min/max keep the existing numeric parse and column formatting.

Two behavior changes fall out of the enum having both names:

- `count` is now every row. `count_filled` is the non-empty variant, and with
  both in the enum they cannot mean the same thing. Only a column whose values
  are all empty renders differently than before.
- a zero count renders "Empty: 0" instead of collapsing to a blank cell. Zero
  is the answer to "how many are empty", not the absence of one.

Column currency/percent formatting is now gated to the numeric family in one
place rather than scattered `type !== 'count'` guards, so `count_unique` on a
currency column reads "Unique: 3" and not "$3.00".

`none` and unrecognized names skip the entry entirely instead of registering a
blank one, so a view whose columns all opt out renders no footer row.

Widen the objectui-local `{ type, field }` arm of `ListColumnSchema` to take
its vocabulary from `SpecColumnSummarySchema` by reference. It was stuck at the
same five names, which left the per-column `field` override unavailable for the
six new aggregations, and it collapses to zero delta when that spec release
lands and the `.extend()` goes away.

Guard both directions with a parity test: a spec name the renderer omits is the
bug this commit fixes, and a renderer name the spec omits would be local
dialect (Commandment #0).

The footer summary was undocumented in both the package README and the plugin
docs page; both now carry the full table and the emptiness rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3EDGLKNvbcpDTLzyiAd3Q
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 28, 2026 4:10am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies package: types plugin tests labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 27.9 KB 350 KB
Entry file index-DsTKD2ru.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.17KB 0.96KB
auth (org-roles.js) 5.50KB 2.36KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 449.81KB 97.81KB
core (index.js) 2.12KB 0.77KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 127.78KB 32.15KB
fields (index.js) 218.37KB 53.54KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 10.67KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 4.42KB 1.27KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 1.77KB 0.77KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 6.84KB 2.42KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.00KB 1.23KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 57.26KB 16.24KB
plugin-chatbot (index.js) 179.50KB 42.71KB
plugin-dashboard (index.js) 109.60KB 28.33KB
plugin-designer (index.js) 210.56KB 42.56KB
plugin-detail (index.js) 214.86KB 52.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 103.47KB 25.10KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 179.25KB 46.97KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 98.64KB 23.33KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 37.07KB 9.81KB
plugin-timeline (index.js) 25.03KB 7.11KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.68KB 20.85KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.55KB 0.67KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 3.19KB 1.38KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 18.70KB 6.09KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.00KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 2.16KB 0.94KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 1.86KB 0.91KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.75KB 0.46KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

`@object-ui/react` exported a hook of the same name as the grid's, with no
callers anywhere in the repo. It was not a duplicate implementation so much as
a competing one:

- a different API — `(data, config, locale)` against the grid's
  `(columns, data, fieldMetadata)` — and a colliding `ColumnSummaryConfig`
  export of a different shape;
- a doc comment claiming it implements "ColumnSummarySchema from
  @objectstack/spec v2.0.7", six majors behind;
- a `distinct` aggregation that is not in the spec vocabulary at all. The spec
  calls that `count_unique`.

That last point is what makes it worth removing rather than leaving inert: a
publicly exported name the spec does not define is the second de-facto contract
Commandment #0.1 warns about, and the parity guard added alongside the grid
hook does not reach it.

Callers wanting footer aggregations use `useColumnSummary` from
`@object-ui/plugin-grid`, which now implements the spec enum in full.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3EDGLKNvbcpDTLzyiAd3Q
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 27.9 KB 350 KB
Entry file index-BobOMn3Y.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.17KB 0.96KB
auth (org-roles.js) 5.50KB 2.36KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 449.62KB 97.73KB
core (index.js) 2.12KB 0.77KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 127.78KB 32.15KB
fields (index.js) 218.37KB 53.54KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 10.67KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 4.42KB 1.27KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 1.77KB 0.77KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 6.84KB 2.42KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.00KB 1.23KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 57.26KB 16.24KB
plugin-chatbot (index.js) 179.50KB 42.71KB
plugin-dashboard (index.js) 109.60KB 28.33KB
plugin-designer (index.js) 210.56KB 42.56KB
plugin-detail (index.js) 214.86KB 52.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 103.47KB 25.10KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 179.25KB 46.97KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 98.64KB 23.33KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 37.07KB 9.81KB
plugin-timeline (index.js) 25.03KB 7.11KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.68KB 20.85KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.55KB 0.67KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 3.19KB 1.38KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 18.70KB 6.09KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.00KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 2.16KB 0.94KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 1.86KB 0.91KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.75KB 0.46KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 04:24
@os-zhuang
os-zhuang merged commit fc13718 into main Jul 28, 2026
14 checks passed
@os-zhuang
os-zhuang deleted the claude/unify-object-ui-types-zod-hc6qoc branch July 28, 2026 04:24
os-zhuang added a commit that referenced this pull request Jul 28, 2026
Audits every named enum export in `@objectstack/spec@16.0.0-rc.0` `ui/` against
its renderer counterpart, in both directions, plus field types and filter
operators. 24 confirmed Direction A gaps that produce a user-visible defect, 11
inert ones, 10 surfaces carrying renderer-local dialect.

Corrects the issue's premise: `ChartTypeSchema` is 19 values in the spec, not 7.
The 7-value list is an objectui fork re-exported under the spec's own symbol
name — one of five such forks.

Triaged by runtime symptom rather than by direction, because that orders the
work by how long a defect survives undetected: silently-wrong output (6) is
worse than the silently-absent case (15) that prompted #2897, and the loud
red-box tier (3) is cheapest precisely because it cannot hide.

Refs #2901, #2897, #2231, #2731

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…silently wrong (#2941) (#2993)

Every row here validated at authoring time and produced output that looked
correct and wasn't (#2901 Tier 1 — worse than a blank cell, because nobody
asks why a plausible number is wrong):

- PivotTable: an out-of-vocabulary aggregation (`count_distinct`,
  `array_agg`, `string_agg` — engine names with no client renderer) returned
  a SUM through the `default:` branch. It now refuses loudly with a visible
  notice; the implemented set is pinned to the spec's 5-name
  `ChartAggregateFunctionSchema`.
- Report chart: 10 of 19 `ChartTypeSchema` values silently drew a bar.
  `planReportChart` now classifies all 19 — the 12 series families reach the
  generic chart verbatim (`horizontal-bar` stays horizontal), the 5
  single-value families render the measure as a server-aggregated number
  (dimensionless dataset query — no client math), `table`/`pivot` add no
  duplicate chart, and out-of-spec values get a visible notice.
- `selection.type: 'single'`: `selectable` was a bare truthy, so single
  rendered the full multi-select UX. The data-table now enforces
  replace-on-select with no select-all header, and ObjectGrid stops offering
  the cross-page "select all N matching" escalation in single mode.
- Filter `type: 'select'`: the spec names both `select` and `multi-select`,
  so `select` is single-choice. It now renders radios and replaces the pick
  (badge × clears); restored/default multi-value selections clamp to one.
  Inferred (omitted) types keep the historical multi-check UX.
- `addRecord.position: 'both'` collapsed to `top` through a binary ternary;
  both buttons now render.
- `tabular` vs `summary` was resolved from whether `rows` was non-empty; the
  DECLARED type now picks the branch: summary (and degraded matrix) carries
  a server-computed grand-total footer (`totals: { groupings: [[]] }`, ADR-0021
  red line respected), tabular is the same selection as a plain list.

Each fix lands with a spec-parity guard per the #2897 template
(`summary-spec-parity.test.ts`) plus behavior tests; `components`,
`plugin-dashboard` and `plugin-report` gain the `@objectstack/spec`
devDependency that makes those guards possible (`components` also pins
`zod@^4` so the spec resolves to the same store instance as the rest of the
repo instead of forking a second `zod@3` peer variant via shadcn's MCP SDK).

Refs #2941, #2901

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
xuyushun441-sys pushed a commit that referenced this pull request Jul 30, 2026
… into nothing (#2942)

Every row below is the #2897 shape — validates at authoring time, renders
nothing (or a dead control), no test fails and no warning fires:

- UserFilters `element: 'toggle'`: `default: return null` deleted the ENTIRE
  filter bar for stored toggle configs. The existing-but-unreachable
  ToggleFilters branch is wired; authoring stays untypeable (ADR-0053) while
  stored metadata keeps rendering (spec ADR-0047 §3.4a).
- UserFilters `date-range` / `text`: dead "No options" popovers become a
  from/to date pair (emits >=/<= bounds) and a contains search input.
- useAnimation: preset/easing maps re-keyed to the spec's underscore
  vocabulary (+ rotate/flip via usePageTransition's classes); hyphen dialect
  and `scale-fade` stay accepted for stored configs. The
  `EASING_MAP[easing] || easing` fallthrough no longer emits invalid CSS.
- NotificationContext: displayType materialized (spec default toast, legacy
  `modal` presents as alert) and the unions now match NotificationTypeSchema
  / NotificationPositionSchema instead of claiming to.
- useNavigationOverlay: the spec `size` buckets resolve to viewport-clamped
  widths off app-shell too (explicit `width` still wins; `auto` stays
  host-derived).
- Both ThemeProviders: `mode: 'auto'` follows the OS instead of adding a
  dead `auto` class that locked the light theme; `system` kept as the
  pre-spec spelling.
- AdvancedChart: the single-value families (gauge/solid-gauge/metric/kpi/
  bullet) render the measure as a number, table/pivot name their owning
  component, unknown types are named — never the bar SHELL with null series
  marks that was indistinguishable from an empty dataset (reachable via
  ChartRenderer's `schema.chartType ?? spec.chartType` bypass).
- Timeline: the spec `scale` key is read at last (legacy `timeScale` kept);
  hour/quarter/year generate real gantt header buckets instead of a blank
  axis.
- Toaster: position (all six spec values) and `limit` reach sonner instead
  of being discarded by a bare `<SonnerToaster />`.
- useSpecGesture: the DECLARED `config.type` drives recognition —
  pan/drag/rotate/double_tap no longer collapse to tap; useGesture gains
  real double-tap (two taps, not one) and two-touch pinch/rotate deltas.
- ReportViewer: `aggregation: 'distinct'` computes a distinct count instead
  of a blank summary cell.
- FieldEditWidget: inline resolution goes through the form's alias table, so
  `json` gets the code editor, `tree` the lookup picker, and composite/
  record/repeater/video/audio/autonumber resolve to their documented
  exclusions; ObjectGrid's editability gate consults the same contract, so
  a `composite` cell is read-only instead of a value-corrupting text box.
- FilterBuilder: $startsWith/$endsWith/$null/$exists become authorable
  (startsWith/endsWith/isNull/isNotNull/exists/notExists) and round-trip
  through condToMongo/kvToCondition — every FieldOperatorsSchema token is
  now reachable from the UI.

The export-menu row (PDF silently downloading nothing) was fixed
concurrently by #2999, which this branch rebases onto and defers to.

Each fix lands with a spec-parity guard per the #2897 template; `fields`,
`mobile`, `plugin-charts` and `providers` gain the `@objectstack/spec`
devDependency that makes those guards possible (no second zod peer variant
materialized — verified in the lockfile).

Refs #2942, #2901

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 30, 2026
… into nothing (#2942) (#3008)

Every row below is the #2897 shape — validates at authoring time, renders
nothing (or a dead control), no test fails and no warning fires:

- UserFilters `element: 'toggle'`: `default: return null` deleted the ENTIRE
  filter bar for stored toggle configs. The existing-but-unreachable
  ToggleFilters branch is wired; authoring stays untypeable (ADR-0053) while
  stored metadata keeps rendering (spec ADR-0047 §3.4a).
- UserFilters `date-range` / `text`: dead "No options" popovers become a
  from/to date pair (emits >=/<= bounds) and a contains search input.
- useAnimation: preset/easing maps re-keyed to the spec's underscore
  vocabulary (+ rotate/flip via usePageTransition's classes); hyphen dialect
  and `scale-fade` stay accepted for stored configs. The
  `EASING_MAP[easing] || easing` fallthrough no longer emits invalid CSS.
- NotificationContext: displayType materialized (spec default toast, legacy
  `modal` presents as alert) and the unions now match NotificationTypeSchema
  / NotificationPositionSchema instead of claiming to.
- useNavigationOverlay: the spec `size` buckets resolve to viewport-clamped
  widths off app-shell too (explicit `width` still wins; `auto` stays
  host-derived).
- Both ThemeProviders: `mode: 'auto'` follows the OS instead of adding a
  dead `auto` class that locked the light theme; `system` kept as the
  pre-spec spelling.
- AdvancedChart: the single-value families (gauge/solid-gauge/metric/kpi/
  bullet) render the measure as a number, table/pivot name their owning
  component, unknown types are named — never the bar SHELL with null series
  marks that was indistinguishable from an empty dataset (reachable via
  ChartRenderer's `schema.chartType ?? spec.chartType` bypass).
- Timeline: the spec `scale` key is read at last (legacy `timeScale` kept);
  hour/quarter/year generate real gantt header buckets instead of a blank
  axis.
- Toaster: position (all six spec values) and `limit` reach sonner instead
  of being discarded by a bare `<SonnerToaster />`.
- useSpecGesture: the DECLARED `config.type` drives recognition —
  pan/drag/rotate/double_tap no longer collapse to tap; useGesture gains
  real double-tap (two taps, not one) and two-touch pinch/rotate deltas.
- ReportViewer: `aggregation: 'distinct'` computes a distinct count instead
  of a blank summary cell.
- FieldEditWidget: inline resolution goes through the form's alias table, so
  `json` gets the code editor, `tree` the lookup picker, and composite/
  record/repeater/video/audio/autonumber resolve to their documented
  exclusions; ObjectGrid's editability gate consults the same contract, so
  a `composite` cell is read-only instead of a value-corrupting text box.
- FilterBuilder: $startsWith/$endsWith/$null/$exists become authorable
  (startsWith/endsWith/isNull/isNotNull/exists/notExists) and round-trip
  through condToMongo/kvToCondition — every FieldOperatorsSchema token is
  now reachable from the UI.

The export-menu row (PDF silently downloading nothing) was fixed
concurrently by #2999, which this branch rebases onto and defers to.

Each fix lands with a spec-parity guard per the #2897 template; `fields`,
`mobile`, `plugin-charts` and `providers` gain the `@objectstack/spec`
devDependency that makes those guards possible (no second zod peer variant
materialized — verified in the lockfile).

Refs #2942, #2901

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to 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