Repository navigation
fix(plugin-grid,components,fields,plugin-detail,plugin-gantt,console): HotCRM rendering — column label, widget fallback and rating cell, reference rail, gantt year, approval drawer, public form heading (objectui#12110) - #12151
Merged
objectstack-fleet[bot] merged 12 commits intoOct 11, 2026
Merged
Conversation
…(objectui#12110 item 1) `ListColumnSchema.label` is the spec's "Display label override", but the grid handed it to the translated field-label lookup as that lookup's fallback, so any bundle translating the field replaced the override. The authored label is now the header as written; the field-label lookup runs only for a column with no override. Pin: ObjectGrid.authoredColumnLabel-12110 (en and zh, plus the no-override control). Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
… (objectui#12110 item 1) Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
…enderer, and a rating cell draws stars (objectui#12110 item 2)
The spec's FieldSchema.widget promises an unregistered widget "Degrades
to the `type` renderer when unregistered". The form renderer took the
hint whether or not anything could draw it, so a `rating` field with
`widget: 'star_rating'` fell to the default text box. Each hint (form
config, then field metadata) is now taken only when it can render: a
registered field widget, a built-in control, or one of the default arm's
own native email/password inputs and the secret refusal (kept so a
secret-declaring hint never degrades to clear text). Otherwise `type`.
The cascade-clear effect reads the same resolution.
The `rating` cell drew a bare number; it now draws the star row
RatingField's read-only branch draws, out of the field's `max` (5 by
default). Module-private, reached through getCellRenderer('rating').
Two plugin-form tests reached the old fallback through a widget nothing
registers; they now reach the same props spread through the built-in
`input` control, and ObjectForm's comment on that route says so.
Pins: form-unregistered-widget-type-fallback-12110,
objectFormUnregisteredWidget-12110, cellRenderers.ratingCell-12110.
Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
Co-authored-by: Claude <noreply@anthropic.com>
…fetches on the invalidation bus (objectui#12110 items 3 and 4) Item 3: the rail named each related row through a fixed ladder of key names and kept only `fields` of the target schema. The same schema read now keeps the object's declared name field (core's declaredNameField), and a row is named by it, through recordDisplayValueAt, before the ladder. Item 4: the rail latched its fetch on (parentId + entries) and never subscribed to the data-invalidation bus, so `refreshAfter: true` (which posts notifyDataChanged for the parent record) left it stale. It now subscribes, and a change to the parent record, to an entry's object or to everything bumps a nonce the fetch signature carries. A refetch of the scope already on screen keeps its rows up while it reads: refresh in place, no skeleton, no remount (AGENTS.md #8). RecordDetailView is not touched: its existing post is what the rail now hears. Pin: record-reference-rail.nameFieldRefresh-12110. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
…rrent one (objectui#12110 item 5)
The Start / End cells, the row date line, the bar tooltip and the two drag
previews formatted { month, day } only. One helper, formatTaskDay, adds
the year when the date's year differs from the current one (the view
zone's `now`, as the today line reads it): two digits on the compact
numeric faces, in full on the short-month tooltip.
Existing pins with 2020 / 2024 fixtures now read the year; the 2026
fixture of objectGanttInputs-11168 is pinned to a 2026 clock (Date only)
so it cannot change meaning on New Year's Day.
Pin: GanttView.dateYear-12110 (clock pinned).
Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
Co-authored-by: Claude <noreply@anthropic.com>
…s with the declared amount (objectui#12110 item 6)
Approver: `formatIdentity` middle-truncated `position:sales_manager` to
`positi…ager`. A position reference now reads the position's label from
the console's metadata store (`ensureType('position')`, the registry
read Setup's recipient picker makes, ADR-0131 D3), else the prettified
name. Asked only for positions the server left unresolved.
Headline: `decisionAmountEntry` led with the first name-matched key, so
HotCRM's account led with "Child Account Revenue 0". A field declared
`currency` now leads (its first non-zero, else its zero), and a name
match leads only with a non-zero value. The 11677 lead-label pin moves to
a billed invoice: its dogfood invoice's `total: 0` is the zero roll-up
this change keeps out of the headline.
Pin: ApprovalsInboxPage.approverHeadline-12110.
Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
Co-authored-by: Claude <noreply@anthropic.com>
…abel (objectui#12110 item 7, heading half) `loadPublicForm` fell back from the form's title straight to the object's API name. The object's label, served on the payload's `objectSchema`, now comes first; the API name stays the last resort. The objectui#8408 no-widening control pinned the API name in exactly this case; it moves one arm down (no object label either -> API name), and the no-title leg now pins the object's label. The other half of item 7 (editor hints on the public surface) is not in this commit: no signal tells a field widget it renders on the public form, and adding one is a contract change the card does not make. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
…e ref in an effect (objectui#12110 item 6) The console type-check read the pin's hoisted stubs as `never[]` / `null` answers; they now declare the row shape. `usePositionLabels` wrote its store ref during render (react-hooks/refs); it now syncs it in an effect declared ahead of the read. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
…ectui#12110 item 5) ObjectGantt.catalogPlans-11141 derived the End cell's expected text as MONTH/DAY from the authored date; the catalog's plans are dated 2024, which is not the current year, so the cell now names the year too. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
Brings in objectui#12140 (RecordDetailView feed handlers, RelatedList, record-related-list) and objectui#12142 (data-table pager names). No file of this branch is touched by either; RecordDetailView's `refreshAfter` post, which item 4's rail now hears, is unchanged. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
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
Size Limits
|
… rule (objectui#12110 item 5) objectql-record-source-render-identity-6939 freezes the clock in 2026 and renders three 2023/2024 catalog plans, so item 5's year rule now writes `2/1/24 → 2/14/24` where its PRE_REPAIR literal held `2/1 → 2/14`. The three tiles' `visibleText` and `sha256` are re-measured through the file's own measure() at fd4059c; with the year segments removed the text equals the d88e20f reading exactly, and `elements` / `tags` are unchanged. The assertion is not loosened and no reading is dropped. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
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
Size Limits
|
This was referenced Oct 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #12110 (items 1 to 6, and the heading half of item 7)
Clause-②: no
Draft by dispatch: the
domain:uiseat 2 PM (sessionsession_01TYgwmFK1q4KJ6Qq2WRLzsD) dispatched this run on claim comment 6108276574. The claim's branch is this branch. The seat moves this PR; the dev does not mark it ready.What stays open on the card: the second half of item 7, "Hide editor hints on the public surface". It needs a decision the card does not make (see "Stopped" below), so this PR names the card with
Part ofand does not close it.The items
Each item has its own commit and its own pin. Each pin was run against the BASE source (
c9fc10b1ec) first, and every defect leg failed there for the reason the card states, while its controls passed (the readings are under "Measured red on base").1. An authored column label wins over the field label (A-02).
ObjectGridbuiltcol.label || field.label || prettified nameand handed that to the translated field-label lookup as its FALLBACK, so a bundle translating the field replaced the override. The spec callsListColumnSchema.labelthe "Display label override". An authored label is now the header as written; a column without one keeps the translated field label. Pin:ObjectGrid.authoredColumnLabel-12110(en and zh, plus the no-override control).2. An unregistered
widgetdegrades to thetyperenderer;ratingcells draw stars (A-03).FieldSchema.widget's own description, "names a registered field component (resolved asfield:WIDGET) … Degrades to thetyperenderer when unregistered", read in the installed@objectstack/spec17.7.0 and again on objectstackmain(b7cd1af9df).star_ratingis a free string: the spec declares no such widget, so the fix is the fallback, not a registration. There is no alias: each hint is taken or not, by one rule.form.tsx) now takes a widget hint (form config first, then the field metadata) only when it can render: a registered field widget (resolved exactly asrenderFieldComponentresolves it), a built-in control (input,textarea,checkbox,switch,select), or one of the default arm's own native email / password inputs and the secret refusal. The last group is kept on purpose: degrading a secret-declaring hint to its field'stypecould put the value on screen as clear text, the one degrade objectui#5322 rules out. Anything else falls back totype. The cascade-clear effect, which reads "which control is this row", now reads the same resolution, so the two readers cannot disagree.ratingcells: the standard cell table mappedratingtoNumberCellRenderer. A new module-privateRatingCellRendererdraws the star rowRatingField's read-only branch draws, out of the field'smax(5 when none is declared,Field.rating's own default), with the value and the scale as accessible text. It is reached throughgetCellRenderer('rating'), so the package's export surface does not grow.form-unregistered-widget-type-fallback-12110(the renderer, with probes),objectFormUnregisteredWidget-12110(the real door:ObjectFormwith the fields package registered),cellRenderers.ratingCell-12110.objectFormNumericStep-9574,sectionEntryOverrides-10475) reached the old fallback throughwidget: 'nothing-registers-this'to see the producer'sstepspread onto aninput. That route is exactly what the spec rules out. They now reach the same spread through the built-ininputcontrol (widget: 'input'), andObjectForm's comment on that route says so.3. The reference rail names a row by the target's
nameField(B-13). The same schema read that servedfieldsnow keeps the object's declared name field (declaredNameFieldfrom@object-ui/core), andpickDisplayNamereads it throughrecordDisplayValueAtbefore the fixed ladder: the same two halvesgetRecordDisplayNamereads its declared rung with.4.
refreshAfter: truerefreshes the reference rail (B-14). The rail now subscribes to the data-invalidation bus (subscribeDataChanges+dataChangeMatchesfrom@object-ui/react). A change to the parent record (whatRecordDetailView'snotifyRecordChangedposts after arefreshAfter: trueaction), to an entry's own object, or to everything bumps a nonce the fetch signature carries. A refetch of the scope already on screen keeps its rows and counts up while it reads: no skeleton, no remount (AGENTS.md #8). The pin checks the row's DOM node survives the refresh.RecordDetailView.tsxis not touched: its existing post is what the rail now hears.Pin for 3 and 4:
record-reference-rail.nameFieldRefresh-12110. The item 4 legs read through the unchanged ladder, so they stand on their own.5. Gantt dates name their year (B-20). The six faces the card lists (Start / End cells, the row date line, the bar tooltip, the summary drag chip, the bar's drag overlay) now go through one helper. It adds the year when the date's year is not the current one ("now" in the view's zone, as the today line reads it): two digits on the compact numeric faces, because the Start / End cells are narrow, and in full on the short-month tooltip. Existing pins with 2020 and 2024 fixtures now read the year.
objectGanttInputs-11168uses 2026 fixtures, so its date legs are pinned to a 2026 clock (Date only); otherwise they would change meaning on New Year's Day. Pin:GanttView.dateYear-12110(clock pinned).Round 1 (
be73faf):examples/schema-catalog'sobjectql-record-source-render-identity-6939freezes the clock in 2026 over three 2023/2024 catalog plans, so item 5 moved its text. ItsPRE_REPAIRvisibleTextandsha256were re-measured through the file's ownmeasure(): with the year segments removed the text equals the old reading, andelements/tagsare unchanged. Against the pre-item-5GanttView.tsxthe new literal fails atvisibleTexton all three tiles; atbe73fafit passes.6. Approval drawer: the approver and the headline (C-18).
position:NAMEapprover the server left unresolved readpositi…ager. It now reads the position's label from the console's metadata store:useMetadata().ensureType('position'), thepositionregistry (ADR-0131 D3), the same read Setup's sharing-rule recipient picker makes since objectui#7611 / feat(app-shell,fields,console): Setup's positions and permission sets read the registry, through the metadata-admin pages' environment scope (part of objectui#7611) #12089. The store fetches it once per session and shares it. That is the source used; no new fetch path. The registry is asked only when a position approver is unresolved, and the prettified machine name stands in when the registry has no such item. The raw reference stays in the chip'stitle.decisionAmountEntryled with the first key whose NAME matched an amount pattern, zero or not. HotCRM's account declaresannual_revenueascurrencyandchild_account_revenueas asummaryroll-up (read in hotcrm'saccount.object.ts), and the drawer led with "Child Account Revenue 0". A field the object declares ascurrencynow leads (its first non-zero value, else its zero). A name match leads only with a non-zero value.summaryFaces-11677lead-label leg used the dogfood invoice, whosetotal: 0is asummaryroll-up, so it is exactly the zero this item keeps out of the headline. That leg now reads a billed invoice (total: 1180); its purpose (the lead takes the metadata label) is unchanged.RecordFieldFace, which this PR does not touch, and neitherpayloadSummary.ApprovalsInboxPage.approverHeadline-12110.7 (heading half). The public form is headed by its object's label (A-09).
loadPublicFormfell back from the form's title straight to the API name. The object's label, served on the payload'sobjectSchema, now comes before it, and the API name stays the last resort. The objectui#8408 no-widening control pinned the API name in exactly this case; it moves one arm down (no object label either: API name), and the no-title leg now pins the object's label.Stopped: the editor hints on the public surface (item 7, second half)
Measured on this branch's base: a markdown field on the anonymous
/f/SLUGpage shows "Format: markdown" (a throwaway render test of the realFormPagein public mode, deleted after the reading). The hints come fromRichTextField's editable branch, which renders them on every host.RichTextFieldcannot tell it is on the public form.FormPageknows itsmode, but nothing passes it to a widget: the widget contract (FieldWidgetComponentProps) has no such member, and no context carries it. Hiding the hints only there needs a new contract member or a new export, and the card does not make that call. The options and a recommendation are in the dispatch report on the card.Measured red on base
One run of every pin against the base source, before any fix:
["#","Account Name","Industry"](en) and["#","客户名称","行业"](zh), where the override "Company" belongs;rating-probe(the text box rendered);ObjectForm: zero stars; the cell: zero stars;CS-0001not found (the row read its subject);HEADafterwards, blob hash checked equal toHEAD): both item 4 legs red (the new note never appeared after the bus post), the item 3 leg red, the two controls green;3/2where3/2/25belongs; the tooltipMar 2 → Mar 6;positi…ager; leadchild_account_revenue;ats_inquirywhereInquirybelongs.Tests and gates
The branch merged
mainat158c75d(objectui#12140 and #12142; no file of this branch is touched by either). Head:fd4059c.After the merge, at
fd4059c:turbo run type-checkforplugin-grid,components,fields,plugin-form,plugin-detailandplugin-gantt(green; the console's task was cut by the foreground cap behind the dependency rebuild), then@object-ui/consolealone: 36 of 36 tasks green. Each package'stsconfig.test.jsonincludessrc/**/*.test.tsxby glob (not counted with--listFiles).main's neighbours (pagerButtonNames-12124,affordanceGrantMap-12082) and the column-identity ratchet: 9 files, 82 tests passed.packages/plugin-detail/(4 shards): 255 files passed, 1 skipped file.packages/components/(6 shards): 375 passed, 1 skipped file.scripts/__tests__/(3 shards): 180 passed, 2 skipped files.node scripts/check-changeset-presence.mjs,check:changeset-claims,check:pending-changeset-literals,check:new-line-citations,check:control-bytes,check:test-path-roots, the threecheck:vi-mock-*,check:phantom-deps,check:doc-json,check:doc-fences: all exit 0.Before the merge, at
50c7747(packages the merge did not change under this branch's files):packages/plugin-grid/202 files passed;packages/plugin-gantt/99 passed, 1 skipped file;packages/fields/245 passed, 1 skipped file;packages/plugin-form/175 passed;apps/console/181 passed.Lint: objectui's package lint is
eslint .with inline config honoured. This branch ran eslint on its 22 changed TypeScript files only: 0 errors. On changed lines the warnings are: pre-existing signatures (anyparameters kept as they were), one pre-existingexhaustive-depson the rail's fetch effect, and four newno-explicit-anywarnings in two new test files (a probe's props, and theas anycasts the neighbouringObjectFormtests use);ApprovalsInboxPage.tsxreads 19 warnings at head and 19 at base. The config enables no type-aware linting, so this diff cannot move a verdict on an unchanged file; the fulleslint .run is CI's.Eager closure: NOT MEASURED as the console's eager closure (that needs two console builds). Proxy, base against head, the packages' built
dist/index.js:fields+776 bytes raw, +170 gzip;components+218 raw, +79 gzip. TheStaricon itself comes fromlucide-react, outside the package dist;RatingFieldalready imported it, lazily.Acceptance notes (observations, not filed)
ObjectForm's numericformField.stepreaches the DOM now only through awidget: 'input'hint (or a host with no field widgets registered): the registered number, currency and percent widgets derive their own step, and an unregistered hint no longer reaches the old fallback. A dormant producer, not a defect. Carrier: none.{ month: 'short', day: 'numeric' }, and shift mode has no year tier above it. Same family as item 5, not measured at a public door. Carrier: none.sys_positionrows (useApproverDirectory, the spec'sAPPROVER_VALUE_BINDINGSsource), and the inbox now reads thepositionregistry. A registry-only position reads as its prettified name on the record page and as its label here. Not measured as a defect. Carrier: whoever next touchesapproverIdentity.ts.plugin-form'sObjectForm.tsx(a comment the item 2 fix made false) and twoplugin-formtests that relied on the retired route;ApprovalsInboxPage.tsxbeyondformatIdentity/decisionAmountEntry(a smallusePositionLabelshook, and the label map threaded throughapproverDisplay/approverChips);form.tsx's cascade-clear reader. Docs touched (pre-authorised):plugin-grid.mdx,plugin-form.mdx,plugin-gantt.mdx,guide/public-forms.md.Generated by Claude Code