Skip to content

fix(fields): draw a text-valued formula as text, not monospace (objectui#12357) - #12383

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12357-formula-text-face
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12357-formula-text-face

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12357
Clause-②: no

What changed

A text-valued formula now draws as text in both faces, whether it declares returnType: 'text' or no returnType at all.

  • Table cell (FormulaCellRenderer, packages/fields/src/index.tsx): the last branch, which printed every result that was not a number, boolean or date in a monospace span (text-gray-700 font-mono text-sm), now returns TextCellRenderer for the same value. The renderer's own block comment already said its text face "mirrors" TextCellRenderer; now it does.
  • Form face (FormulaField, packages/fields/src/widgets/FormulaField.tsx): the text branch now uses the same typedClassName (text-sm plus the caller's className) as the number, boolean and date branches. That matches what TextField's read-only branch draws for a text value (a text-sm span), so the monospace font and text-gray-700 are gone.
  • The choice of face still depends only on whether the result is text. The type rule is unchanged: use the declared returnType; with none declared, a JS number is a number and anything else is text. An undeclared string result therefore gets the text face too. The triage ruling asked for this because code-authored formulas usually arrive without a returnType.

What I measured against the dispatch assumptions

  • A1 (confirmed): on origin/main d78e70b, both faces fell through to font-mono for every result that was not a number, boolean or date. The cell now calls TextCellRenderer. The new pin compares the cell's innerHTML byte for byte with TextCellRenderer's for the same value. It also checks that the form face's class list equals TextField's read-only class list.
  • A2 (done): the new sibling pin formulaFaces.textFace-12357.test.tsx covers a declared returnType: 'text' result, an undeclared string result, and a control: a numeric result, declared or undeclared, still reads 200,000 through the number face (.tabular-nums). The 11748 pin's equality leg (form text equals cell text) already covered declared and undeclared text and still passes.
  • A3 (does not hold as written): an object or array result has no face of its own. Both faces call coerceToSafeValue first. It turns an expanded record into its name (or label, id and so on) and joins an array into text. The text branch then prints that string. So before this change, object and array results already shared the monospace text face, and after it they share the text cell's face. Keeping monospace for them alone would need a new branch that does not exist today, so I left them in the text face. The 11748 row text, an expanded record (reads Ada) still passes in both faces.

Pins that encoded the defect (rewritten, not deleted)

Four assertions used the old monospace span as a stand-in for "drawn as text":

  • 11748 pin: no returnType: a string of digits stays text and returnType 'text' over a number stays text. These now assert there is no .font-mono; the literal text 200000 (not 200,000) still shows the text face was used.
  • 11683 pin (formulaDatetimeCells.copiedText-11683.test.tsx): the same two cases. These now assert there is no .tabular-nums (not the number face) and no .font-mono.

Evidence

  • Ablation at c3954f9, run with ablation-replace.mjs. The anchor hit once, the mutation was confirmed on disk by blob hash, and after the run the restore matched the HEAD blob and git diff HEAD was empty. In this package, vitest reads src directly, so no dist rebuild was involved.
    • Cell leg: restoring the monospace span in FormulaCellRenderer gave Tests 6 failed | 98 passed (104). The failures were the cell legs of the new pin (markup equality and no monospace, both declared and undeclared) and the two flipped 11748 cell assertions.
    • Form leg: restoring the monospace span in FormulaField gave Tests 6 failed | 98 passed (104). The failures were the matching form legs.
  • Targeted tests at 9126ff6: the 12357, 11748 and 11683 pins plus summaryFaces.numberFace-11752 and cellRenderers.fabricatedValue-8490 gave Test Files 5 passed (5), Tests 280 passed (280).
  • Full packages/fields/ run at c3954f9 (--maxWorkers=2): Tests 4 failed | 4062 passed | 7 skipped (4073). The only failures were the four 11683 assertions listed above. 9126ff6 changes only that test file, and that file passes on 9126ff6.
  • pnpm --filter @object-ui/fields type-check exits 0, after building the @object-ui/fields^... closure. That script also runs tsc -p tsconfig.test.json, which includes src/**/*.test.tsx.
  • Lint, limited to the 5 changed fields files: eslint --format json reported 5 files, 0 errors and 146 warnings. All the warnings were already there: index.tsx shows 145 both at the base and at HEAD, and the one in FormulaField.tsx is the existing any in its signature. eslint.config.js has no parserOptions.project or projectService, so linting is not type-aware and this diff cannot change the result for any file it does not touch.
  • Pre-tsc checks: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift and i18n-designer-parity all exit 0. check-changeset-fixed and check-changeset-presence exit 0. check:spec-floors exits 1 with "The scan collapsed: 0 package(s) declare @objectstack/spec"; this also fails on main and is tracked as objectui#12334, which this PR does not touch.

Acceptance notes

  • No published export, prop or accepted set changes. TextCellRenderer was already exported, and the component signatures are the same. Changeset: .changeset/12357-formula-text-face.md, a patch for @object-ui/fields.
  • summary uses the same cell renderer, so a summary roll-up with a non-numeric result also gets the text cell's face now.
  • The currency face (objectui#12111) is untouched.

Generated by Claude Code

…tui#12357)

The formula cell's text face now renders through TextCellRenderer and the
read-only form face takes TextField's read-only classes, for a declared
`returnType: 'text'` and an undeclared string result alike.

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

Two assertions used the old monospace span as the proxy for "drawn as
text"; they now assert the number face is absent and monospace is gone.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 69 chunks) 3052.7 KB 3119.4 KB
Main entry chunk (gzip) 73.8 KB 350 KB
Entry file index-DgDLKkBd.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) 20.90KB 7.67KB
app-shell (runtime-config.js) 22.59KB 7.89KB
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.62KB 0.83KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
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.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
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) 591.65KB 142.68KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.41KB 69.48KB
fields (index.js) 282.89KB 72.11KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 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) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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) 54.94KB 16.05KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 143.63KB 38.79KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 259.78KB 68.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 179.38KB 46.59KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 259.06KB 72.25KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 123.35KB 31.60KB
plugin-map (index.js) 27.14KB 8.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.99KB 23.23KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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.07KB 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.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
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-approval-decision.js) 0.20KB 0.20KB
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.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

Copy link
Copy Markdown
Contributor Author

Test (shard 1/12) cancelled at the job timeout on head 9126ff6; not this PR's, one re-run · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB · 2026-10-14T18:50Z

  • What failed: Test (shard 1/12) (job 116029528393) ended cancelled at the 20-minute job limit (18:29:06Z → 18:49:26Z). The aggregate Test check is red because of it.
  • Why it is not this PR's:
  • No fix to port: no change exists that would make the shard faster.
  • Action: one re-run of the failed jobs on 9126ff6. A second timeout or a real failure is this PR's.

Generated by Claude Code

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.

fields: a text-valued formula is drawn in monospace code font in the table cell and the form face, declared returnType: 'text' or not

2 participants