Repository navigation
fix(plugin-form): the master-detail form's Subtotal / Tax / Total show the amount field's currency, not a hard-coded ¥ (objectui#11132) - #11142
Conversation
…t's resolved currency The Subtotal / Tax / Total stack prefixed a literal yen sign whatever the amount field's or the tenant's currency. It now resolves the currency through resolveFieldCurrency on each entry's amount field (the child field definition the resolve effect already loads), falling back to the tenant default, and formats with Intl's currency style in the display locale: the sign sits where the locale puts it and the width is the currency's minor unit. With no currency known, or entries that disagree, the lines are plain numbers. Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
…the 9909 pin off the yen sign The objectui#9909 display-locale pin now declares a USD tenant and asserts the de/en grouping plus the resolved sign where the locale puts it. A new pin covers a USD tenant on all three lines for every resolve branch, a fixed field currency winning over the tenant's, a dynamic config deferring to the tenant, the JPY width (whole yen), the de-DE sign placement, no currency at all, and two entries in different currencies. Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
…ency Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec Co-authored-by: Claude <noreply@anthropic.com>
✅ 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
|
Contract reviewServed-tier: Inputs: card #11132 (body; triage Gate verdicts on the head ( ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named:
② Semver level
Changeset
③ Boundary flagsEvery dev flag and both a. Reach correction (PR Acceptance note 2) — confirmed at the head; the fix stands. Read at objectstack b. Open question 1 (entries resolving different currencies show plain numbers, no sign) — accepted within the triage direction; no ruling needed. "The field's currency, else the tenant's, never a constant": with two fields naming two currencies there is no single field currency, and a plain number is c. Open question 2 (a fully configured entry shows the tenant sign even when its amount field fixes a currency) — a pre-existing branch gap shared with d. Out-of-scope finding (the hydrate branch derives neither e. Second note ( f. Deviations 1 to 5 in the report — accepted. (1) g. The gates the dev reports (plugin-form vitest 146 files, 1738 passed; type-check; narrowed eslint; the seven check scripts) agree with the head's check-runs, which are the verdicts here. Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #11132
Clause-②: no
Why no (carried from the claim, comment 5897361654): a display sign follows the field's resolved currency. No schema, prop, type or accepted set moves.
Dispatched implementation,
domain:uiseat 2, sessionhttps://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec. Draft until the seat lands it. Direction: triage comment 5896646901.What changed
MasterDetailForm's document totals stack (Subtotal / Tax / Total, test idsmd-subtotal,md-tax,md-grand-total) built every line as a literal¥followed by the display-locale digits at two places, whatever the amount field's or the tenant's currency.resolveFieldCurrency(@object-ui/i18n) on the entry's amount field definition, with the tenant default (useLocalization().currency, ADR-0053) as the resolver's last step. No constant, no second resolver.DetailEntryrecord asamountFieldDef; the authoredMasterDetailDetailConfigis untouched.formatDisplayNumber(n, { locale, currency }):Intl's currency style in the display locale. The sign sits where the locale puts it, and the width is the currency's ISO 4217 minor unit, which isIntl's default forstyle: 'currency'.CurrencyField's no-currency width), no sign.Intlrefuses. The code beside the plain number, digits still in the display locale, instead of a thrownRangeErrortaking the form down.Measured: which formatter the currency faces use
Two formatters exist in the family, both reading
resolveFieldCurrency:formatCurrency(@object-ui/fieldsentry):CurrencyCellRenderer, the grid summary footer (useColumnSummary) and the metric tile. A whole amount drops its fraction ($1,234).formatDisplayNumberwithcurrencyand the currency's minor unit on both bounds:CurrencyField(formatAmount) andGridField's currency cells (currencyText). Those grid cells are the line items directly above this stack.The stack takes the second. It agrees with the line cells above it by construction and keeps the stack's own always-show-the-fraction form. With the first, a whole subtotal would read
$1,000under line cells reading$1,000.00.currencyFractionDigitsis not on the@object-ui/fieldspackage entry (itsindex.tsxre-exports onlyresolveFieldCurrencyandformatCurrencyfromcurrency.ts), so the stack does not pass a width.Intl's default forstyle: 'currency'is the same minor unit that helper reads:resolvedOptions()for JPY answers 0 / 0, and 1234.5 formats as$1,234.50(en, USD),1.234,50 $(de-DE, USD),¥1,235(en, JPY),KWD 1,234.500(en, KWD).What a reader sees change
¥1,234.50$1,234.50¥1.234,501.234,50 $¥1,234.50¥1,235(width 2 becomes 0)¥1,234.50€1,234.50¥1,234.501,234.50A CNY tenant reads
CN¥1,234.50in an English session and¥1,234.50in zh-CN: that isIntl's symbol, as on every other currency face.Pins
formFaces.displayLocale-9909.test.tsx: the master-detail surface now declares a USD tenant and asserts the de/en grouping plus the resolved sign where the locale puts it (1.234,50 $,$1,234.50). The de/en difference and the no-machine-locale tripwire still run on it.MasterDetailForm.totalsCurrency-11132.test.tsx(10 cases, one line of 1234.5 under a 10% tax rate, all three lines asserted together):$1,234.50/$123.45/$1,357.95, once per resolve branch (fully configured, hydrated identity-only columns, derived);€on all three lines (hydrated and derived branches);currencyMode: 'dynamic'with adefaultCurrencyof EUR: the tenant's$, so the stack reads the field through the resolver'scurrencyModerule (objectui#10422);¥1,235/¥123/¥1,358, the width change pinned;1.234,50 $/123,45 $/1.357,95 $;1,234.50/123.45/1,357.95;Reverse verification (one-shot, not kept). With the tests committed,
MasterDetailForm.tsxwas replaced by its blob at the branch pointdb11afd49(on-disk hash equal to that blob; the¥literal count went 0 to 1,formatTotalsAmount3 to 0), and both pin files were run: 12 failed, 10 passed of 22. The 12 are all 10 new cases plus the two rewritten 9909 master-detail rows, each failing on the stack text (for example¥1,234.50received where$1,234.50was expected). Restored withgit checkout HEAD, proven by the on-disk hash equal to the HEAD blob and an emptygit diff HEAD.Acceptance notes
examples/app-showcaseinvoice as the producer that switches the stack on. A render probe (not committed) ofMasterDetailFormwith the subform the console builds from that relationship (relationshipField: 'invoice', identity-onlyinlineColumns, noinlineAmountField) and atax_rateheader rendered nomd-totals: the hydrate branch of the resolve effect never derives anamountField. WithamountField: 'amount'added, the stack renders. So the stack is reached where an entry carries an amount field (the spec'sinlineAmountField, or derived when noinlineColumnsare declared) under atax_rateheader. No example combines those two today: only the showcase invoice hastax_rate, and the CRM opportunity's line items declareinlineAmountFieldunder a parent without one.sortFieldderivation too. In the probe, a line created through the showcase-shaped subform carried noposition, while the derived-branch control stampedposition: 0.GridFieldresolves a currency column against the tenant default only (itscolumnCurrencyhands the resolver no field, by design). So under a fixed-EUR amount field in a USD tenant, the line cells read$while the stack now reads€, as the ruling asks. Before this change they read$and¥. Owner: none.taxRateField,Subtotalandtax_ratehave no hits inpackages/plugin-form/README.mdor incontent/docsoutside an unrelated formula example. Nothing to update.Gates (run at
86935655d, the head of this PR)pnpm exec vitest run --maxWorkers=2 packages/plugin-form/: exit 0,Test Files 146 passed (146),Tests 1738 passed | 1 skipped (1739).pnpm --filter @object-ui/plugin-form type-check: exit 0, after building the dependency closure (pnpm --filter '@object-ui/plugin-form^...' run build, exit 0).tsc -p tsconfig.test.json --listFileslists both test files andMasterDetailForm.tsx.pnpm exec eslinton the three changed code files (one source, two tests): exit 0,--format jsoncounts 3 files, 0 errors, 23 warnings. The 23 are allno-explicit-anyinMasterDetailForm.tsx, and the branch-point blob of that file gives the same 23 (via--stdin). No line this PR adds containsany.eslint.config.jssets noparserOptions.projectorprojectService: type-aware linting is off, so this diff cannot move a verdict on any untouched file. The repo-widepnpm lintis CI's.node scripts/check-changeset-presence.mjs: exit 0 (1 changeset, 1 released package).node scripts/check-control-bytes.mjs: exit 0.node scripts/check-new-cross-file-line-citations.mjs: exit 0,0 new citation(s).node scripts/check-changeset-claims.mjs,node scripts/check-pending-changeset-literals.mjs,node scripts/check-test-path-roots.mjs: exit 0.node scripts/check-governed-queue-guard.mjs --testover the four changed paths: NOT GOVERNED.Generated by Claude Code