Skip to content

fix(plugin-tree): tree cells draw the list cell faces, so a boolean no longer shows as true (objectui#11686) - #11721

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11686-tree-cell-display-faces
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11686-tree-cell-display-faces

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #11686

Clause-②: no — tree cells start drawing through the faces list cells already use; no export, prop, type member or i18n key is added to anything a package entry reaches.

What was wrong

Setup → Business Units, Org Chart tab. That tab is sys_business_unit's org_chart list view (type: 'tree'), which ListView's case 'tree' branch hands to object-tree, so it is drawn by ObjectTree in this package. Its cell formatter formatCellValue handled two families (fields carrying options, and expanded references) and ended in return String(value); for everything else. So the boolean active column printed true, and dates, numbers and currency printed their stored form.

What changed

  • packages/plugin-tree/src/ObjectTree.tsx: new renderTreeCell. A cell whose column has a DECLARED type now draws through getCellRenderer(resolveCellRendererType(field)) from @object-ui/fields. That is the two-step the grid (resolveGridCellRendering), the kanban card, the gallery card and the report viewer use. The face receives the column definition plus four values:

    • name: the column key, which BooleanCellRenderer and the date faces read;
    • type: the renderer key, as the grid's fieldMeta.type carries it;
    • options: translated through translateOptions, the same call the grid makes;
    • label: the label the tree's header prints.

    A column with no declared type (one the object does not define, or a schema-less mount) keeps formatCellValue's string. The label column and the other columns both go through renderTreeCell. The record overlay's plain-reading fallback is unchanged.

  • packages/plugin-tree/package.json: declares @object-ui/fields as workspace:*, as plugin-kanban, plugin-grid and plugin-report do. There is no cycle, because fields depends on nothing that depends on plugin-tree. plugin-tree already reached fields at runtime: it imports @object-ui/plugin-detail statically, and plugin-detail's built entry imports @object-ui/fields statically. So this edge adds no module to any bundle closure.

  • pnpm-lock.yaml: only the matching three-line importer entry.

  • .changeset/11686-tree-cell-display-faces.md: a patch for @object-ui/plugin-tree.

Behaviour a reviewer will see beyond the card:

  • A lookup cell now draws LookupCellRenderer. It links to the referenced record when the host publishes a record href; that link stops propagation, so the row click does not also fire. An id that was not expanded is resolved by name on demand.
  • A user cell now draws the avatar face.
  • The label cell gains the text face's hover title. An empty label draws the shared empty-value affordance: the same dash, now with an accessible name.

Measured before editing (dispatch hypotheses H1–H4)

  • H1 holds. The formatter ends in return String(value);, and three call sites print its result: the label column, the other columns, and the overlay fallback. The Org Chart is drawn by this package (path above).
  • H2. The lookup is getCellRenderer + resolveCellRendererType, published by @object-ui/fields, which plugin-tree did not declare.
  • H3. The string is used only for display. Nothing in the tree reads it for search, client sort or an accessible name, so returning an element changes nothing else.
  • H4. The list's boolean face draws a read-only checkbox for true, with no words. For false on a status-named field (active, enabled and so on) it draws a badge reading "FIELD-LABEL — Off". The label half is localized here, because the header's translated label is handed to the face. The word Off, and the completion indicator's Completed / Not completed accessible names, are hard-coded English in BooleanCellRenderer itself; see Acceptance notes.

Tests

New ObjectTree.cellFaces-11686.test.tsx. It mounts the tree the way ListView does (live dataSource plus inline data) with sys_business_unit columns, and pins:

  • boolean true draws the checkbox (aria-checked="true"), not true. Boolean false on active draws the face's badge, not false.
  • date, currency and select cells are byte-equal (innerHTML) to the list face rendered on its own for the same field and value.
  • a column the object does not define keeps its plain string (the declared bound).
  • the card's pin. Under a zh provider with fields.sys_business_unit.active set to 启用, the Active column draws the checkbox for true. For false it draws a badge naming 启用 (not Active), the same word as the header.

The six objectui#6014 pins in ObjectTree.fieldFormatting.test.tsx (lookup name, $expand, option label, case-insensitive match, fieldOptions i18n, plain value) pass unchanged through the faces.

Reverse check (ablation), run from committed state with ablation-replace.mjs in wrap mode:

  • Mutation: renderTreeCell always returns the old string. Anchor hits went 1 → 0; blob 9a76f60b → b395115b.
  • Result: 4 failed / 7 passed.
    • Each of the four face pins went red at its own assertion: expected 'true' not to contain 'true', expected 'false' not to contain 'false', expected '2024-03-05' not to be '2024-03-05', and the zh pin's true.
    • The untyped-bound pin and the six objectui#6014 pins stayed green.
  • Restore proven: blob back to 9a76f60b == HEAD, and git diff HEAD is empty.
  • An earlier run turned all five new pins red at a shared wait-for-a-checkbox helper, so it showed nothing per pin. That helper now waits on a signal the old code also produced (commit 506a6e5), and the result above is from the rerun.

Gates — local head 142121e

command exit verdict line
pnpm --filter '@object-ui/plugin-tree^...' build (closure, 13 projects) 0 lock VERDICT command-exit 0
pnpm --filter @object-ui/plugin-tree build 0 lock VERDICT command-exit 0; dts built, fields external
pnpm --filter @object-ui/plugin-tree test 0 Test Files 24 passed (24), Tests 178 passed (178), at 142121e
pnpm --filter @object-ui/plugin-tree type-check (echoes tsc --noEmit && tsc -p tsconfig.test.json; the test config lists the new file) 0 lock VERDICT command-exit 0
vitest run of ObjectTree.cellFaces-11686.test.tsx + package type-check, on the bytes committed as 142121e 0 Tests 5 passed (5)
pnpm check:control-bytes 0 check-control-bytes: OK
pnpm check:new-line-citations 0 VERDICT new-cross-file-line-citations: 0 new citation(s)
pnpm check:changeset-claims 0 No pending changeset names a file this change touches.
pnpm check:pending-changeset-literals 0 No test source names a pending changeset.
pnpm check:phantom-deps 0 Every in-scope import is declared by the package that publishes it.
pnpm check:unused-deps 0 Every gated declaration has a consumer in the package that declares it.
pnpm check:lockfile-integrity 0 VERDICT clean
pnpm check:lockfile-dedupe (added: lockfile touched) 0 VERDICT deduped
pnpm check:test-path-roots (added: new test file) 0 check-test-path-roots: OK
node scripts/check-changeset-presence.mjs (added: changeset) 0 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-no-major.mjs (added: changeset) 0 No changeset declares a major bump.
node scripts/check-governed-queue-guard.mjs --test (the five paths) 0 NOT GOVERNED

Full package suite at 142121e: measured under the lock on the fourth attempt. Result: Test Files 24 passed (24), Tests 178 passed (178), plus type-check green. The first three attempts timed out in the queue (exit 99) behind another holder. (Paragraph corrected by the domain:ui seat 1 PM at review; the dev's report carries the measurement.)

Lint narrowed to the two touched source files: eslint --no-inline-config --format json read 2 files, with 0 errors. ObjectTree.tsx has 24 warnings, the same count as its base. The new test file has 0. Invariance: the flat config enables no type-aware linting (empty parserOptions, no project and no projectService), no import/* rules, and no custom object-ui/* rule reads the filesystem or type services. So this diff cannot move any untouched file's verdict. The repo-wide pnpm lint stays with CI.

NOT MEASURED locally:

  • check:eager-closure: it needs a full console vite build. See the closure argument above.
  • The live console run.

Acceptance notes (not changed here)

  • Boolean face words. In BooleanCellRenderer (packages/fields/src/index.tsx), the badge word Off and the completion indicator's Completed / Not completed accessible names are hard-coded English on every list surface, and now the tree too. They are the same family as the read-only Yes / No words carded in objectui#11689, but sit in a file that card does not list. They are named for that card's scope, not changed here (this claim adds no i18n key).
  • Grid label divergence. The grid hands its cell face the authored fieldDef.label while its header resolves the translated label. So under a non-en pack the grid's Off badge names the column in the authored language, while the tree's (after this PR) names it like its header. Same family as above.
  • UMD global names. plugin-tree's vite.config.ts names no UMD global for @object-ui/fields (new here), @object-ui/plugin-detail, @object-ui/permissions or react/jsx-runtime. The build warns MISSING_GLOBAL_NAME and guesses a name for each. ESM output is unaffected.

Implemented by an os-dev agent dispatched by the domain:ui seat 1 PM; session https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z.


Generated by Claude Code

claude added 3 commits October 6, 2026 14:07
The tree's cell formatter knew option labels and referenced records and
sent every other type to String(value), so the Org Chart's boolean Active
column printed `true`, and dates, numbers and currency printed raw.

Every cell whose column has a declared type now draws through the face
the list surfaces resolve, getCellRenderer(resolveCellRendererType(field)),
with the column key as `name`, the renderer key as `type`, translated
options, and the header's label. Untyped columns keep the old string.

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
…roduce (objectui#11686)

The pin file's helper waited for the boolean face's checkbox before reading
any cell, so on a tree without the faces every test failed at that wait
rather than at its own assertion. It now waits for the select column's
option label, which the tree printed before this change as well.

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
…jectui#11686)

Replaces the three `as any` casts with `DataSource` and the face's own
`CellRendererProps['field']`, and drops an unused parameter.

Claude-Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3323.9 KB 3330.4 KB
Main entry chunk (gzip) 153.6 KB 350 KB
Entry file index-BhBCwm-I.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
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) 574.74KB 137.95KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 11.71KB
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.75KB
mobile (index.js) 1.99KB 0.87KB
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) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.25KB 65.05KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 15:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit a58626c Oct 6, 2026
47 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11686-tree-cell-display-faces branch October 6, 2026 15:25
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.

tree / Org Chart: cells render raw values (a boolean shows as true)

2 participants