Skip to content

fix(core): $expand user/tree reference columns in list & grid views (not just lookup/master_detail) - #2032

Merged
os-zhuang merged 1 commit into
mainfrom
claude/hardcore-kalam-01594a
Jun 27, 2026
Merged

fix(core): $expand user/tree reference columns in list & grid views (not just lookup/master_detail)#2032
os-zhuang merged 1 commit into
mainfrom
claude/hardcore-kalam-01594a

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

The bug

In list / grid (and kanban / calendar / gallery / …) views, reference columns of type user render a bare id (or a "—" placeholder) instead of the related record's name. Record detail views render the same fields correctly.

Example: a grid showing a user-type "technician"/"owner" column shows VYVspo4Eb9vx9AwqV4ThlA4WNt8CeJ80 instead of Dev Admin.

Root cause

List/grid views derive their $expand set from buildExpandFields() in @object-ui/core (packages/core/src/utils/expand-fields.ts). It recognised onlylookup and master_detail field types:

fieldDef.type==='lookup'||fieldDef.type==='master_detail'

So for a user (or tree) reference column the view never requested $expand → the API returned the raw foreign-key id → the cell had no related record to read a display name from → it rendered the raw id / .

The objectql backend already resolves user through the same expand path as lookup/master_detail (it carries the same reference + id storage). So the gap was purely on the request side, in objectui.

lookup / master_detail columns were already fine (the previous fix added scoped $expand for them) — which is why the bug looked type-specific.

The fix

Extend the expandable-type set to the full reference-bearing set — lookup / master_detail / tree / user — behind a single exported constant + predicate, and route the existing collection logic through it:

exportconstEXPANDABLE_FIELD_TYPES=newSet(['lookup','master_detail','tree','user']);exportfunctionisExpandableFieldType(fieldDef): boolean{/* type ∈ EXPANDABLE_FIELD_TYPES */}
  • user is the field type the backend can expand but the frontend wasn't requesting — this is the end-to-end fix.
  • tree matches the form layer's DATA_SOURCE_FIELD_TYPES (lookup/master_detail/tree); it's a reference too, so it belongs in the set. Requesting it is harmless and forward-compatible (see follow-up below).

Every $expand caller (list, grid, kanban, calendar, gallery, timeline, gantt, tree, map, detail) routes through buildExpandFields, so this one change fixes them uniformly.

Why only the visible reference columns (unchanged, but the key design point)

buildExpandFields(fields, columns) already intersects with the view's columns, and that's preserved. A list is N rows; expanding a relation per row has a cost, and a list typically shows only 2–3 reference columns. So the default is:

$expand = view columns ∩ reference-typed fields; empty set → omit $expand (original behavior).

Wide objects never pay to expand relations no cell will show. Detail view keeps expanding all reference fields (no columns arg), as before.

This is a pure-function change with the type set extracted as the single source of truth (EXPANDABLE_FIELD_TYPES / isExpandableFieldType), unit-tested for: mixed columns pick out only reference types; non-reference columns never enter $expand; the visible-column intersection; and empty → no $expand.

Verification (framework examples/app-showcase)

Stood up the app-showcase backend + the objectui console against it, on the showcase_field_zoo object (which has one field of every type) via a small grid view exposing the relational columns. Seeded a record with a real user reference.

Column (type)BeforeAfter
f_lookup (lookup)NorthwindNorthwind
f_master_detail (master_detail)Website RelaunchWebsite Relaunch
f_user (user)VYVspo4Eb9vx9… (raw id)Dev Admin
f_owner (user)VYVspo4Eb9vx9… (raw id)Dev Admin
f_tree (tree) (see follow-up)

Confirmed at the API layer too: with $expand=f_user the backend returns f_user: { name: "Dev Admin", … }; without it (today's behavior) it stays a raw id.

Tests / build

  • @object-ui/core unit tests: 4683 passed (incl. the extended expand-fields suite — 23 cases).
  • @object-ui/plugin-list: 131 passed; @object-ui/core & @object-ui/plugin-grid build clean.

Follow-ups (intentionally NOT in this PR)

  • tree display: the frontend now requests$expand for tree, but objectql's expand resolver currently materialises only lookup/master_detail/user, so tree columns still show until the backend expands tree too. That's a framework change.
  • Cross-repo rollout: no .objectui-sha / .framework-sha bumped here. After this merges, it reaches the cloud runtime via a framework .objectui-sha bump (then cloud's own pin) — left to a human to time.

🤖 Generated with Claude Code

…st lookup/master_detail)
List/grid views derive their `$expand` set from `buildExpandFields`, which only
recognised `lookup` and `master_detail` field types. Reference columns of type
`user` (and `tree`) were therefore never requested for expansion, so the cell
received a bare foreign-key id and rendered a raw id / "—" placeholder instead
of the related record's display name — even though the objectql backend
resolves `user` through the same expand path as `lookup`/`master_detail`.
Extend the expandable-type set to the full reference-bearing set
(lookup / master_detail / tree / user) behind a single exported constant
`EXPANDABLE_FIELD_TYPES` + an `isExpandableFieldType()` predicate. Visible-
column scoping is unchanged: only the reference columns a view actually shows
are expanded, so wide objects never pay to expand relations no cell will
display, and an empty set still omits `$expand`.
Every `$expand` caller (list, grid, kanban, calendar, gallery, timeline,
gantt, tree, map, detail) routes through `buildExpandFields`, so this single
change fixes them uniformly.
Verified against framework examples/app-showcase: a grid showing `user`-type
columns flips from a raw id to the related user's name. (`tree` display
additionally needs objectql to materialise tree expansion — a backend follow-up.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercelBot commented Jun 27, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredJun 27, 2026 4:09pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)59.0 KB350 KB
Entry fileindex-9dLnz5ka.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)6.87KB2.47KB
app-shell (runtime-config.js)4.72KB1.69KB
app-shell (types.js)0.01KB0.04KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)17.16KB3.44KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)4.79KB1.88KB
auth (LoginForm.js)9.55KB3.36KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.63KB2.15KB
auth (SocialSignInButtons.js)8.89KB3.61KB
auth (UserMenu.js)3.40KB1.22KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)24.74KB6.00KB
auth (createAuthenticatedFetch.js)3.33KB1.32KB
auth (index.js)1.75KB0.76KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.01KB0.79KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.42KB0.96KB
collaboration (index.js)1.25KB0.53KB
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)422.37KB89.87KB
core (index.js)1.52KB0.55KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)99.69KB24.89KB
fields (index.js)164.60KB39.84KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.46KB0.96KB
i18n (pickLocalized.js)1.31KB0.67KB
i18n (provider.js)5.37KB1.72KB
i18n (useObjectLabel.js)21.15KB4.68KB
i18n (useSafeTranslation.js)2.68KB0.98KB
layout (index.js)36.30KB10.04KB
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)4.42KB1.27KB
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)1.77KB0.77KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)5.09KB1.84KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.46KB1.03KB
permissions (evaluator.js)4.00KB1.23KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.50KB0.70KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.98KB12.65KB
plugin-charts (index.js)45.46KB12.78KB
plugin-chatbot (index.js)159.77KB38.54KB
plugin-dashboard (index.js)107.36KB26.65KB
plugin-designer (index.js)213.42KB42.94KB
plugin-detail (index.js)198.30KB47.91KB
plugin-editor (index.js)2.38KB1.06KB
plugin-form (index.js)93.23KB22.71KB
plugin-gantt (index.js)136.68KB33.88KB
plugin-grid (index.js)121.64KB32.95KB
plugin-kanban (index.js)48.82KB13.13KB
plugin-list (index.js)94.53KB22.32KB
plugin-map (index.js)16.02KB4.98KB
plugin-markdown (index.js)13.58KB4.64KB
plugin-report (index.js)37.83KB9.97KB
plugin-timeline (index.js)25.37KB7.20KB
plugin-tree (index.js)8.21KB2.76KB
plugin-view (index.js)84.45KB20.36KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.55KB0.67KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)18.23KB5.97KB
react (index.js)0.76KB0.42KB
tenant (TenantContext.js)0.31KB0.25KB
tenant (TenantGuard.js)1.04KB0.43KB
tenant (TenantProvider.js)2.76KB0.98KB
tenant (TenantScopedQuery.js)0.77KB0.44KB
tenant (index.js)0.75KB0.38KB
tenant (resolver.js)2.64KB0.76KB
tenant (useTenant.js)0.50KB0.32KB
tenant (useTenantBranding.js)0.62KB0.39KB
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 (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)0.77KB0.41KB
types (disclosure.js)0.20KB0.18KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (index.js)1.54KB0.68KB
types (layout.js)0.20KB0.18KB
types (mobile.js)0.20KB0.18KB
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 (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.26KB1.96KB
types (tenant.js)0.20KB0.18KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)0.75KB0.46KB
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

@os-zhuang
os-zhuang merged commit 196aa53 into mainJun 27, 2026
10 checks passed
@os-zhuang
os-zhuang deleted the claude/hardcore-kalam-01594a branch June 27, 2026 16:22
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.

1 participant

@os-zhuang