Skip to content

fix(plugin-tree): format tree-grid cells like the flat table - #6097

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-6014-tree-grid-field-formatting
Aug 24, 2026
Merged

fix(plugin-tree): format tree-grid cells like the flat table#6097
yinlianghui merged 1 commit into
mainfrom
claude/issue-6014-tree-grid-field-formatting

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6014

Verified at 773c6463e (the branch head; every gate result below was produced on this tree).

What was wrong — and it was not the formatter

The card's own control pointed at the fetch, not at the cell renderer.

ObjectTree treated "the host passed inline data" as "I do not need the object schema" and skipped getObjectSchema. But its record-fetch branch prefers a live object dataSource over any inline data. On the one mount shape ListView actually uses — objectName + a dataSource + its own pre-fetched data — the tree therefore issued its own query with buildExpandFields(undefined)[] → no $expand at all, and had no field definitions to format cells from.

Both reported symptoms fall out of that single gap. That is why the flat tab was unaffected (it renders from ListView's own expanded fetch) and why the tree's existing tests could not see it (they pass inline data with no dataSource — a path that never runs the tree's own fetch).

Measured with a dataSource that expands only what it is asked to expand:

mount shapegetObjectSchemafindrendered row
dataSource + inline data (the repro)never called1x, $expand: nullAcme │ u1 │ department
dataSource onlycalled2x, 2nd $expand: [parent_id, manager_user_id]Acme │ Zhang San │ department
inline only, no dataSource (existing tests)n/anoneAcme │ Zhang San │ department

Row 2 is the counter-probe: same component, same formatter, schema present → the lookup already resolved. formatValue was never the lookup defect. Row 2 also shows the select half surviving a correct expand — it is a genuinely separate mechanism, and the only half a formatter change fixes.

The fix

Three changes, all in packages/plugin-tree/src/ObjectTree.tsx:

  1. Fetch the object schema whenever the dataSource can serve one, not only when no host passed inline data. The guard inside the fetch already no-ops without a dataSource, so the pure inline/static path is untouched.
  2. Hold the record fetch until that schema has settled — settled, not necessarily successful, so a rejected or inapplicable schema fetch can never block the tree. This also removes the wasted first query visible as find 1-of-2 in row 2 above, whose lookup columns came back as bare ids and were painted for a moment before the real query landed.
  3. Route cells through a field-aware formatter that delegates both decisions rather than re-deciding them.

Which seam, measured rather than assumed

The dispatch asked for "the same chain as the flat table" and warned there may not be one reachable chain. Measured:

  • ObjectGrid.tsx:3125 renderFieldValue is the record-detail drawer panel, not the flat table's cells. The flat table's column cells go through resolveCellRendererType + getCellRenderer (@object-ui/fields), with a fieldMeta built from the object schema — and, for the select half, fieldMeta.options = translateOptions(objectName, field, options).
  • translateOptions comes from useSafeFieldLabel()which this component already calls for its column headers. No new dependency, no new export; the tree and the flat tab now read one fieldOptions.* i18n key.
  • For the reference half, getRecordDisplayName (@object-ui/core) is the unified display-name resolver (ADR-0079), and the family is judged by isExpandableFieldTypethe same predicate that decided what to put in $expand a few lines up, so "what we expanded" and "what we unwrap as a reference" cannot drift apart.
  • localizeFieldOptions / buildDimensionLabelMap (core/utils/chart-series.ts) are ruled out — not because they are chart-shaped (their mechanism is generic) but because they implement a different matching contract from the one the flat tab uses: no case-insensitive fallback, null-when-no-op, and extra authored-label keys. Adopting them would make tree cells disagree with flat cells in exactly the mixed-case seed-data cases SelectCellRenderer's own comment calls out.

What I did not do: take on @object-ui/fields and render cells through getCellRenderer. That is the fuller convergence, but it turns every tree cell into the grid's chrome (badges, links, per-cell lookup-resolution hooks) — a redesign of the tree-grid rather than a patch-level bug fix. Flagging it rather than deciding it unilaterally.

Verification

Every new assertion was red before the fix for a value reason, not a mount error:

AssertionError: expected 'u1' to be 'Zhang San'
AssertionError: expected 'department' to be 'Business Department'
AssertionError: expected 'department' to be '部门'

The select fixture's labels are deliberately not derivable from their values: humanizeLabel('department') returns 'Department', so an assertion expecting Department would have passed with option matching completely broken. The fixture uses Business Department, and the test pins the fallback out explicitly.

Ablation (two legs, each mutation proven on disk by grepping the injected and the removed text, restored under trap … EXIT INT TERM, git diff HEAD --stat empty after each). No rebuild leg applies: the tests import ./ObjectTree as a relative source path and vitest.config.mts aliases @object-ui/corepackages/core/src, so nothing resolves through dist/.

legmutationredgreen
Aselect/option branch disabledthe 3 select teststhe 3 lookup/plain tests
BbuildExpandFields(undefined) (schema still fetched)the 2 lookup testsall 4 select/plain tests

Each leg reds exactly the tests bound to the half it ablates — so neither half's green is phantom, and the two mechanisms are independent as claimed.

Gates

gateexita red would have meant
pnpm --filter @object-ui/plugin-tree type-check0a type error in the package (it caught one real red mid-work — TS2550 on Array.prototype.at, since removed). Log shows the real tsc invocation, so not a zero-match silent green.
pnpm exec vitest run packages/plugin-tree/src (root form)0 — 4 files, 17/17a behavioural regression in the tree
pnpm exec eslint (plain, no --no-inline-config) on both changed files0 — 0 errors, 30 warningsa lint error. Warnings are no-explicit-any (this file is any-typed throughout) plus two pre-existing hook rules on untouched lines; pnpm lint sets no --max-warnings.
check:phantom-deps · check:self-import · check:esm-specifiers · check:spec-symbols · check:action-forward-parity · check:designer-field-key-parity · check:icon-record-names · check:i18n-keys · check:i18n-drift · check:control-bytes0 eachthe Type Check job's own step list — derived from the job steps, not from top-level script names
check:changeset-presence · check:published-dist · check:eager-closure · check-lint-coverage · check-type-check-coverage0 eachthe other workflows that trigger on packages/** / .changeset/**
downstream prefix-form sweep pnpm --filter '...@object-ui/plugin-tree' type-check2 — see below

The dependent set is exactly two projects: packages/plugin-tree (Done, green) and apps/console. Every apps/console error is the unbuilt-closure class, confirmed mechanically rather than asserted: all 105 TS2307 + 36 TS2882 name modules whose dist/index.d.ts is MISSING (the caret form '@object-ui/plugin-tree^...' builds the package's dependencies, not the package itself nor console's other deps), the list includes packages entirely unrelated to this diff (@object-ui/auth, @object-ui/app-shell, @object-ui/permissions, @object-ui/providers) and 14 sibling plugins untouched here, and zero console errors name any file this PR changes. CI installs and builds properly, so it type-checks console for real.

(The lock wrapper reported 141 for that run — SIGPIPE from a head in my own reporting pipeline, not the gate. The gate's exit was captured before the pipe: 2.)

Scope

packages/core and packages/components were flagged as contended — neither is touched. No new exports, no new package dependencies, no changes to the flat-table renderers. Changeset is patch (runtime bug fix; no public type or export changes).

One visible consequence beyond the report, called out in the changeset: an expanded record that comes back with no name-ish field now reads as ADR-0079's Record # + the record id floor — the string every other surface shows for it — rather than as the bare id.


Generated by Claude Code

…i#6014)
The 「组织架构」 tree tab rendered a lookup column as a bare record id and a
select column as its raw stored value, while the flat-table tab on the same
page rendered the display name and the translated label over the same records.
The defect was in the fetch, not the formatter. ObjectTree treated "the host
passed inline `data`" as "I do not need the object schema" and skipped
getObjectSchema, but its record-fetch branch prefers a live object dataSource
over any inline data. On ListView's real mount shape (objectName + dataSource +
its own pre-fetched data) the tree therefore ran its own query with
buildExpandFields(undefined) -> [] -> no $expand, and had no field definitions
to format cells from. Both symptoms fall out of that one gap.
- Fetch the object schema whenever the dataSource can serve one.
- Hold the record fetch until that schema has SETTLED (not necessarily
succeeded), which also drops a wasted first query whose lookup columns came
back as bare ids and were painted before the real query landed.
- Route cells through a field-aware formatter that delegates both decisions:
option labels via the same `translateOptions` seam ObjectGrid uses, expanded
references via getRecordDisplayName (ADR-0079), family judged by
isExpandableFieldType -- the same predicate that built $expand.
No new exports, no new package dependencies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3220.9 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-DQMULeeS.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)505.15KB114.53KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)166.86KB46.08KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.04KB61.85KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
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)2.74KB1.41KB
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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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 24, 2026 16:52
@yinlianghui
yinlianghui added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit b00fcbaAug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-6014-tree-grid-field-formatting branch August 24, 2026 17:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

树形表格(业务单元「组织架构」视图)不做字段格式化:lookup 列显示裸 ID、select 列显示原始值

2 participants

@yinlianghui@claude