Skip to content

fix(app-shell): format currency, number and percent values in the record history Changes entry - #27638

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-q6ulq0
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-q6ulq0

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

维护者速读

  • 改了什么:记录详情 Activity › Timeline 的 "Changes" 条目里,货币、数字、百分比字段不再显示原始存储值(如 4321000),改为与记录页同样的格式(如 $4,321,000.00)。
  • 为什么改:历史条目与记录头部对同一个值显示两种样子,用户会以为数据有问题;格式化统一走字段渲染器已有的格式化函数,不另写一套。
  • 风险与回滚:只影响历史面板的显示文本,不改存储、不改记录页;回滚即 revert 本 PR。record 模式货币因单次格式化拿不到行上的币种字段,显示为不带符号的数字。
  • 先看:ui/packages/app-shell/src/utils/auditHistoryDisplay.ts 的 formatNumericAmount;ui/packages/fields/src/index.tsx 新增的 formatNumberFieldValue 导出。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/utils/__tests__/auditHistoryDisplay.numericAmounts-27486.test.ts,预期 12 passed。
  • 需要你做的:无,席位落地。

Fixes #27486

Clause-②: no
Metadata-protocol: none

What changes

  • auditHistoryDisplay.ts formatScalar() gains an arm for currency, number and percent fields. A finite stored value (a JS number, or a decimal column's numeric string) is formatted by the calls the record page's cells make, by reference:
    • currency: formatCurrency(value, resolveFieldCurrency(def, ctx.tenantCurrency), ctx.locale), the pair CurrencyCellRenderer uses.
    • number: formatNumberFieldValue(value, field, ctx.locale), the call NumberCellRenderer and NumberField's read-only branch make (declared scale, useGrouping).
    • percent: formatPercent(value, percentCellScale(def), resolveFieldScale(...), ctx.locale), the pair PercentCellRenderer uses (declared storage and width).
      A value that is not a number keeps the existing String(value) face.
  • AuditFieldDef declares the members those formatters read (scale, max, useGrouping, currencyConfig), so the arm needs no cast.
  • AuditValueFormatContext gains an optional tenantCurrency, the last step of resolveFieldCurrency's precedence.
  • @object-ui/fields: formatNumberFieldValue is exported from the package entry (it was module-internal), and its locale parameter is optional like formatCurrency's and formatPercent's. No rendering change there.
  • ui/scripts/__tests__/percent-storage-callers-11475.test.ts: the repo-wide pin over every formatPercent caller gains the new call's row (storage percentCellScale(def), field in hand). The pin went red on the first gate run because the population on disk grew by this file, which is its job.
  • Changeset: patch for @object-ui/app-shell and @object-ui/fields.

Currency modes, measured against resolveFieldCurrency

  • fixed: the declared code. The card's showcase_account.annual_revenue is fixed USD, so its repro now reads $4,321,000.00.
  • dynamic: the tenant default when the caller passes tenantCurrency; without it, the formatter's no-code face (a plain grouped number at width 2), never a guessed symbol.
  • record (ADR-0144 D2): the code lives in a sibling field of the row. formatAuditValue is called once per side with no row, so the resolver answers no code, and the amount reads as a plain grouped number. The tenant default is never substituted (the resolver's own rule). No hard-coded code, no second formatter.

Tests

ui/packages/app-shell/src/utils/__tests__/auditHistoryDisplay.numericAmounts-27486.test.ts, 12 cases: currency (fixed USD both sides, JPY width, de-DE locale, numeric string, dynamic with and without a tenant default, record mode), number (declared scale, scale-0 year ungrouped, de-DE, non-numeric value), percent (fraction storage, whole storage via max, de-DE). Each states the record page's text as a literal and checks the shared formatter returns the same.

Acceptance notes

  • Caller wiring, out of this card's file surface. RecordDetailView's History effect builds the format context as { t, locale: displayLocale, lookupLabels }. It does not pass tenantCurrency, so a dynamic-mode currency field still reads as a plain number in history while the record page shows the tenant symbol. The one-line fix (tenantCurrency from useLocalization().currency) is in RecordDetailView.tsx, which this card's order puts out of bounds (the record-page chain). Listed for the seat in the report.
  • Record-mode code. A per-side code would need each side's payload (old_value / new_value) handed to the formatter; that is a caller change too. Not attempted.
  • File surface. The percent-caller pin row above is outside the order's named surface (auditHistoryDisplay.ts and its tests, the @object-ui/fields export, the changeset); it is the registration that pin requires of any new formatPercent caller, and is declared as a deviation in the report.
  • The scale-0 year case and the non-numeric case also pass with the new arm removed (both equal String(value)); they pin that the arm keeps those faces, not that it fires.

Verification

Local runs, head fded0c5
  • pnpm --dir ui exec vitest run packages/app-shell/src/utils/__tests__/: Test Files 28 passed, 1 skipped; Tests 433 passed, 8 skipped.
  • pnpm --filter @object-ui/app-shell run type-check (both tsconfigs) and pnpm --filter @object-ui/fields run type-check: exit 0.
  • Ablation (scripts/ablation-replace.mjs): the arm's return formatNumericAmount(...) disabled, the new file reads 10 failed, 2 passed; restored, blob equals HEAD and git diff HEAD is empty.
  • Derived gate list (dispatch-gates --commands, 34 commands, check-sdui-manifest included) run on fded0c5, each exit 0, including pnpm --dir ui exec vitest run scripts/ (Test Files 169 passed, 2 skipped; Tests 4902 passed, 2 skipped). dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED.
  • pnpm --dir ui run check:new-line-citations: 0 new citations.

Generated by Claude Code

…ord history Changes entry

Route numeric field types in auditHistoryDisplay formatScalar through the
field renderer's own formatters (formatCurrency + resolveFieldCurrency,
formatNumberFieldValue, formatPercent + percentCellScale), so the History
tab reads a stored amount the way the record page does.

Claude-Session: https://claude.ai/code/session_01Utp6CXcn7RbCYknff87Ke1
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
Contract-review-claim: session_01Utp6CbDAucNyQ4UtzCJKyh · head 62d62ff · 2026-10-29T15:50Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 62d62ff21758229180d8b08d6b1224b3c1d3b5f7
Local-runs: none

Face: declaration only (Clause-②: yes (widening) on the amended claim 6501425591 and the changeset). The committed tree touches no packages/spec/src/**, no governed path and no narrowing line. Reviewed 2026-10-29T15:51Z from the diff, the card and the head's check-runs. The head is fded0c5f plus one commit (the changeset's Clause-②: line only).

① Derived judgments

  • @object-ui/fields (published) re-exports formatNumberFieldValue from its entry; it was module-internal. Its locale parameter becomes optional (type only; absent, the runtime default answers, as for the already-exported formatCurrency / formatPercent). No behaviour of the function changes. Additive; nothing narrows. The export exists so the History tab calls the record page's own formatter (triage 6496229595: 「No second currency formatter」).
  • @object-ui/app-shell is private: true: AuditFieldDef / AuditValueFormatContext gaining optional members (scale, max, useGrouping, currencyConfig, tenantCurrency) is no published surface.
  • Currency modes: fixed reads its declared code; dynamic the tenant default when the caller passes tenantCurrency, else a plain number; record a plain number (its code is a sibling field the per-value call is not handed) — never a guessed code. No epic [Epic] multi-currency (shape C′): record currency, dated exchange rates, write-time home-currency projection — 17 children, R1/R2/R3 release pairing (ruling 6465431966) #8345 file is touched.
  • ui/scripts/__tests__/percent-storage-callers-11475.test.ts gains one ledger row for the new formatPercent caller (the pin goes red on any unlisted caller); a ledger row, not a rule change.

② Semver level

patch for @object-ui/app-shell and @object-ui/fields, matching Metadata-protocol: none (AGENTS.md Post-Task Checklist step 3). The PR body's Clause-②: no line is stale: the relay refuses a body patch whose first line is not a signature, and the project keeps the attribution block as lines 1–2; the claim amendment and the changeset carry the authoritative yes (widening).

③ Boundary flags

  • The dev's open_questions is empty.
  • Finding (the record page builds the History context without tenantCurrency, so a dynamic currency still reads as a plain number there) is filed by the seat at ACCEPT.
  • PR author is objectstack-fleet[bot] (relay write).

Implemented-by: claude/project-thread-q6ulq0
Reviewed-by: session_01Utp6CbDAucNyQ4UtzCJKyh

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

席位核对

ACCEPT · head 62d62ff21758229180d8b08d6b1224b3c1d3b5f7 · 2026-10-29T16:05Z

  • Shape: draft → main, Fixes #27486 on its own line, no other card number beside a closing keyword. Metadata-protocol: none. Clause-②: yes (widening) per claim amendment 6501425591 and the changeset: @object-ui/fields newly exports formatNumberFieldValue (its locale parameter is now optional). The body's Clause-②: no line is stale and stays, because the relay cannot patch a body whose first lines are the attribution block. The contract review is on record on this head: TIER_DEFAULT PASS 6501514158.
  • Change, read: formatScalar in auditHistoryDisplay.ts formats currency, number and percent by reference to the record page's cell formatters (formatCurrency + resolveFieldCurrency, formatNumberFieldValue, formatPercent + percentCellScale/resolveFieldScale). A non-numeric value keeps String(value). AuditValueFormatContext.tenantCurrency is optional.
  • Accepted deviations: the repo-wide formatPercent caller pin (percent-storage-callers-11475.test.ts) gains the new call's row. Record-mode currency renders as an unsymbolled grouped number, because a single-value format has no row to read the sibling code field from. That is named in the body, and no symbol is guessed.
  • Filed: record history: a dynamic-mode currency change still reads as a plain number, because the record page builds the History format context without the tenant currency #27640 (RecordDetailView's format context does not pass tenantCurrency, so dynamic-mode currency in the panel falls back to the no-code face). Blocked-by: #27486.
  • Gates (check-runs on the head): 44 success, 11 skipped, 0 failure. Lint & Repo Gates, TypeScript Type Check and Check Changeset are each success. check-governed-merges --pr 27638: NOT governed, 0 of 6 paths hit the register, 230 changed lines.
  • Landing: non-governed, all checks green and mergeable_state clean, so the seat lands it with pr_ready + automerge_enable.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 16:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit d220b9b Oct 29, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-q6ulq0 branch October 29, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant