Repository navigation
fix(fields): a computed grid currency cell stores and shows its currency's ISO minor unit, not scale ?? 2 (objectui#10355) - #10405
Conversation
…ncy's ISO minor unit, not `scale ?? 2` (objectui#10355) `computeRow` rounded a computed `currency` column's stored value to `scale ?? 2`, reading the `scale` ruling B retired from the currency type and inventing a literal 2 for every currency: JPY was stored with cents it does not have and KWD lost its third digit. `displayText` and the editable cell's adornment both fell back to a literal `¥` without resolving any currency. The grid now resolves its currency through `resolveFieldCurrency` (the tenant default; a grid column declares no field-level currency key), rounds a computed currency cell to that currency's ISO 4217 minor unit via `currencyFractionDigits`, stores it unrounded when no currency resolves, and formats every currency face with the resolved currency and width. An authored `prefix` still replaces the symbol. `number` columns keep their `scale` and the objectui#10071 width cap unchanged. Also corrects the `PercentCellRenderer` comment: after objectui#10221 it is the grid footer's percent arm, not its currency arm, that spells `?? 0`. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…inor unit (objectui#10355) JPY 3 x 1234.5 stores a whole number, KWD 3 x 1.2345 keeps three places, USD keeps two, a stale `scale` on a currency column is not read, no resolved currency stores the value as computed, and a `number` column stays unrounded. The display pins read the resolved currency on the computed cell, the list face and the edit adornment, with an authored `prefix` replacing the symbol but not the width. Two existing pins move with the contract: the objectui#9909 list-mode patterns no longer expect the retired literal yen default, and the objectui#10071 currency control passes a currency instead of relying on the removed two-digit default. Adds the `minor` changeset for `@object-ui/fields`. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…ld face (objectui#10355) The display ablation left two pins green: the yen's own symbol is the retired default, and the old face printed three places for 3.704 anyway. The JPY pin now also renders a row stored with cents before the repair, and the prefix pin pads a dinar amount to three places. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…ves (objectui#10355) The read-only table prints numeric cells raw, so "every currency cell" overclaimed; the comment now names the three sites that read it. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
|
changeset-claim-re-read
|
✅ 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
|
…'s width; the minor unit replaces only the default (objectui#10355) The spec's `InlineGridColumnSchema.scale` declares `scale` on an inline grid column for a computed "numeric/currency result", and the installed spec accepts it on a currency column. Ruling B retires `FieldSchema.scale` from the currency field type, a different schema, so the grid may not narrow the inline key away at the consumer. One helper, `currencyWidth`, now decides a currency column's width for both the stored computed value and the display: the authored `scale`, else the resolved currency's ISO 4217 minor unit, else none. The old default of 2 stays gone. The display clamps an authored width above the engine ceiling through `renderableFractionScale`, as the stored value already did (objectui#10071). The "scale is not read" pin becomes "an authored scale wins over the minor unit"; a display-width pin and a display-ceiling pin are added. The changeset prose follows. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ 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
|
… non-percent reader (objectui#10355) This PR's display cap added a third caller of `renderableFractionScale` beside the number cell and `computeRow`: `currencyText` in `GridField`, under the face label `grid currency cell`. The "Also the NUMBER faces" paragraph said two. Comment only; the rest of the paragraph is unchanged. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ 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
|
Fixes #10355
Clause-②: no
What changed
GridField(@object-ui/fields) now resolves the currency of itscurrencycolumns throughresolveFieldCurrency, and gives a currency column ONE fraction width (currencyWidth) for both the stored value of a computed currency cell and its display: the column's authoredscalewhen it has one, otherwise the resolved currency's ISO 4217 minor unit (currencyFractionDigits), otherwise none. The minor unit replaces only the old default of2.computeRow, the stored value. A computedcurrencycell is rounded to its authoredscalewhen present. Without one it is rounded to the resolved currency's minor unit: whole yen for JPY, two places for USD, three for KWD. It no longer invents a2. With neither ascalenor a resolved currency, the value is stored as computed.numbercolumns still readscale, unrounded when it is absent. The objectui#10071 width cap (renderableFractionScale) stays on the same line for both.computeRowtakes the tenant currency as a new optional third argument.displayText, the computed cell and the list form-factor. A currency amount is formatted in the resolved currency at the samecurrencyWidth:¥3,704,KWD 3.704,$1,234.57, or¥1,234.57for a JPY column authored withscale: 2. An authored width above the engine's ceiling is clamped and reported (renderableFractionScale, face labelgrid currency cell, objectui#10071), sinceIntlrefuses it at render. With no currency, the amount is a plain locale number with no symbol. The literal¥default is gone.c.prefix || '¥'default. It now showsc.prefix, else the resolved currency's symbol (currencySymbol, the channelCurrencyFielduses), else nothing. Left alone, the input would have shown¥next to a$display. The four conditions all hold: same defect class, a mechanical fix with a pinned shape, the file is this claim's, and the gate family is the same.prefixstill replaces the symbol on both faces, at the same width.percent-scale.ts's 「Also the NUMBER faces」 paragraph now counts three non-percent readers of a declaredscale; the grid currency cell is the new one.PercentCellRenderercomment inpackages/fields/src/index.tsxnow says the grid footer's percent arm spells?? 0. After objectui#10221 the currency arm reads noscale. One word changed.Why an authored
scalestill wins on a currency column (seat respin)The first head (
69988b32e) stopped readingscaleon a currency column, following the triage's reading of ruling B. The seat reversed that. The spec'sInlineGridColumnSchema.scale(objectstackpackages/spec/src/data/field.zod.ts) still DECLARES 「Decimal places to round a computed numeric/currency result to」, and installed spec 17.4.0 accepts it on a currency inline column. Ruling B retiresFieldSchema.scaleon the currency FIELD type, a different schema. A declared key is implemented, or retired in the spec; ⛔ it is never narrowed away by its consumer. The spec half (retire inlinescaleon currency, and the stale 「(default '¥')」prefixdescribe) is objectstack-ai/objectstack#20045.Reading ruling 乙 on writes (the dispatch's tension)
Ruling 乙 (objectstack-ai/objectstack#19910, comment 5805782503), item 1, verbatim: "delete
scaleon a currency field; the currency's ISO 4217 minor unit decides its display, and its write allowance stays unconstrained (today's contract)."That "write allowance" is what the server record validator accepts. Ruling B item 4 describes it: "a currency write with more decimals than any declared precision is still ACCEPTED when nothing declares a width". Ruling 乙 says nothing about a value the client computes, so it does not forbid rounding a computed amount to the minor unit. Triage items 2 and 4 are therefore implemented as written. There is house precedent under the same ruling:
CurrencyField(objectui#10276) already rounds typed input to the currency's minor unit on blur.How the currency is resolved
The call is
resolveFieldCurrency(undefined, tenantCurrency), and the field-level legs get nothing on purpose. No grid column declarescurrency,currencyConfigordefaultCurrency: notGridColumn, notGridColumnDefinition(@object-ui/types), and the spec's strictInlineGridColumnSchemarefuses them.deriveColumnsandhydrateColumnsalso copy none of them from the child field. Reading them off the column would add a renderer read that no authored metadata can reach. So a column's currency is the tenant default (useLocalization().currency), still read through the one resolver.Tests
Head
b229d7355(the respin is8fb8bc0c8;b229d7355adds only thepercent-scale.tscomment).pnpm --filter @object-ui/fields type-check(dependency closure built first withturbo run build --filter='@object-ui/fields^...'):tsc --noEmit && tsc -p tsconfig.test.json, 0error TS, exit 0 at8fb8bc0c8.packages/fields/src/widgets/GridField.currencyMinorUnit-10355.test.tsx, 13 tests, all green at8fb8bc0c8:3704; KWD 3 × 1.2345 stores3.704; the USD control stores1234.57; an authoredscalewins over the minor unit (JPYscale: 2→1234.57, USDscale: 0→1235); with no currency the value is stored as computed; thenumbercontrol stays unrounded.LocalizationProvider: a JPY edit stores whole yen and the cell shows no decimals; KWD shows three decimals; USD shows$on the list face and in the edit adornment, never¥; no currency means no symbol on either face; an authoredscaledecides the display width (¥1,235→¥1,234.57); an authoredscale: 105is clamped on the display and does not throw (the warning names 105 and objectui#10071); an authoredprefixreplaces the symbol, not the width.pnpm exec vitest run packages/fields/from the repo root at8fb8bc0c8: 182 files passed, 1 skipped; 3073 tests passed, 7 skipped.GridField.tsxorfields/src/index.tsxas text: 135 passed.¥, and the objectui#10071 currency control passes'USD'instead of relying on the removed two-place default (12 tests, green).Ablations
Both legs ran through
ablation-replace.mjsat8fb8bc0c8: the anchor went 1 to 0, the blob changed, and the restore was proven (blob equals HEAD9bd016332c91,git diff HEADempty).c.scale ?? (c.type === 'currency' ? 2 : undefined)restored incomputeRowexpected 3703.5 to be 3704, KWDexpected 3.7 to be 3.704, no currencyexpected 3.7 to be 3.7035, JPY edit3703.5numbercontrolsscaleread removed fromcurrencyWidthexpected 1235 to be 1234.57, display widthexpected '¥1,235' to be '¥1,234.57', display ceilingexpected '' to contain '105'Gates (head
8fb8bc0c8)check:control-bytesOK ·check:new-line-citations0 new ·check-changeset-presencepasses (1 changeset:minoron@object-ui/fields).check:changeset-claims: exit 0. The 4 pending changesets that name a touched file (6625, 6694, 6837, 7166) were re-read, and all are still true.GridField.tsx(27) andindex.tsx(138) are the same as at base778138e20; the test files have 0.check:test-path-roots,check:vi-mock-inherit,check:doc-types,check:registry-bare-names,check:shell-escape-residue,check:unreferenced-sources,check:esm-specifiers,check:self-import,check:phantom-deps) passed at69988b32e. The respin adds no file, import or export. CI runs the full set.Acceptance notes
InlineGridColumnSchema.scalestill offers decimal places on acurrencyinline column, and itsprefixdescribe promises a¥default the renderer no longer has — ruling B / 乙 left the inline grid mirror behind objectstack#20045).InlineGridColumnSchema(objectstackpackages/spec/src/data/field.zod.ts) is documented as the strict mirror of this renderer'sGridColumn. Itsscaledescribe reads "Decimal places to round a computed numeric/currency result to", and itsprefixdescribe reads "Currency symbol rendered inside acurrencycell (default '¥')". After the respin,scaleon a currency inline column is still read, as declared. What is stale is the¥default, and whether the spec keeps offeringscaleon currency is the spec seat's call. Authored instances: 0. Across objectstackexamples/**,inlineColumnsappears in one file (the showcase invoice), and that file is identity-only. The spec half is finding(spec):InlineGridColumnSchema.scalestill offers decimal places on acurrencyinline column, and itsprefixdescribe promises a¥default the renderer no longer has — ruling B / 乙 left the inline grid mirror behind objectstack#20045.currencyConfig.defaultCurrency) does not reach its grid column: the column shape has no such key, and neither derivation copies it. No master-detail child currency field in the examples declares one (invoice lines, opportunity line items), so the tenant default is correct for all of them today.content/docs/fields/grid.mdxshows a grid columncurrency: 'USD'key that no column type declares; the spec refuses it andGridFieldnever reads it.readonly) prints numeric cells raw (String(value)). The grid's own total cell formats with no currency. Neither goes throughdisplayText, and this card does not name them.GridField.tsx, the tests beside it, the oneindex.tsxsentence, and one changeset. One test outside "beside" also moved:packages/fields/src/__tests__/NumberScaleOutOfRange-10071.test.tsx, acomputeRowpin whose currency control encoded the removed?? 2. Andpackages/fields/src/widgets/percent-scale.ts, one comment sentence this PR's new reader made false, taken as a declared add-on at the seat's order.Implemented by the
os-devagent for thedomain:uiseat 1 claim, sessionhttps://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC.Generated by Claude Code