Repository navigation
Commit ea9d17f
Fixes #10422
Clause-②: yes
A currency field in `dynamic` mode now shows the tenant's currency on
every face, not its `currencyConfig.defaultCurrency`.
`resolveFieldCurrency` in `@object-ui/i18n` reads
`currencyConfig.defaultCurrency` only when `currencyMode` is `'fixed'`.
Its parameter type gains `currencyConfig.currencyMode?: 'dynamic' |
'fixed'`, the spec's enum. No other export changes.
In a USD tenant, a field declaring `currencyConfig: { currencyMode:
'dynamic', defaultCurrency: 'EUR' }` read `€3,456`; it now reads
`$3,456`. An empty `currencyConfig`, which the spec parses to a dynamic
CNY config, read `CN¥`; it now reads `$`. A `fixed` field renders as
before.
## The contract, quoted from the installed spec
`@objectstack/spec` 17.4.0, the version in objectui's lockfile,
`CurrencyConfigSchema`:
- `currencyMode: z.enum(['dynamic',
'fixed']).default('dynamic').describe('Currency mode: dynamic (user
selectable) or fixed (single currency)')`
- `defaultCurrency:
z.string().length(3).default('CNY').describe('Default or fixed currency
code (ISO 4217, e.g., USD, CNY, EUR)')`
`FieldSchema` guidance for the key `currency`, which it refuses:
"`currency` is not a field key — a fixed currency is declared as
`currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'JPY' }`. A
field without one uses the tenant default at runtime."
Measured by parsing through that installed spec from this worktree:
| input | result |
|:--|:--|
| `CurrencyConfigSchema.parse({})` |
`{"precision":2,"currencyMode":"dynamic","defaultCurrency":"CNY"}` |
| `CurrencyConfigSchema.parse({ defaultCurrency: 'EUR' })` |
`{"precision":2,"currencyMode":"dynamic","defaultCurrency":"EUR"}`, so
an absent mode is `dynamic` |
| `CurrencyConfigSchema.parse({ currencyMode: 'fixed', defaultCurrency:
'JPY' })` | `{"currencyMode":"fixed","defaultCurrency":"JPY"}` |
| `FieldSchema` with a top-level `defaultCurrency: 'GBP'` | refused,
`unrecognized_keys` |
| `FieldSchema` with a top-level `currency: 'GBP'` | refused,
`unrecognized_keys`, with the guidance above |
## The precedence (tenant USD)
| step | the field carries | the resolver returns | why |
|:--|:--|:--|:--|
| 1 | an explicit `currency` | that code | Not a `FieldSchema` key, but
the spec declares `currency` on the metric widget and on dataset
measures, and looser column configs carry it. Kept first: nothing in the
spec says otherwise. |
| 2 | `currencyConfig` with `currencyMode: 'fixed'` | its
`defaultCurrency` | The spec's one fixed-currency declaration. |
| skipped | `currencyConfig` with `currencyMode: 'dynamic'` | falls
through | The `FieldSchema` guidance: the tenant default. ⛔ Not
`defaultCurrency`. |
| skipped | `currencyConfig` with no `currencyMode` | falls through |
The spec defaults the mode to `dynamic` (measured above). |
| skipped | the spec-parsed empty config (`dynamic`, `CNY`) | falls
through | Never `CN¥`. |
| 3 | a legacy top-level `defaultCurrency` | that code | A pre-spec
spelling: `6c3f018a8` added it as the "canonical top-level" key, which
`FieldSchema` now refuses as unrecognized. Kept in its place. |
| 4 | none of the above | the tenant default | `localization.currency`,
ADR-0053 |
| 5 | no tenant default either | `undefined` | The amount shows as a
plain number. |
Triage point 1 says a value carrying its own user-selected currency
should win. Nothing carries one today: the spec deprecates
`CurrencyValueSchema`, and a currency value is a bare number. So there
is nothing to implement. The docblock says where such a code would rank:
ahead of the tenant default.
## Consumers: what each call site passes
| face | call | what it passes | does `currencyMode` reach the resolver?
|
|:--|:--|:--|:--|
| list cell, `CurrencyCellRenderer` (`@object-ui/fields`) |
`resolveFieldCurrency(field as any, tenantCurrency)` | the cell's field
bag as handed | yes. The whole def on auto-generated grid columns and
the detail panel. The three configured grid paths copy `currencyConfig`
verbatim since objectui#10354. |
| field widget, `CurrencyField` | `resolveFieldCurrency(currencyField,
tenantCurrency)` | the whole `field` prop | yes |
| metric tile, `ObjectMetricWidget` |
`resolveFieldCurrency(valueFieldDef, tenantCurrency)` | the object
schema's def for `aggregate.field` | yes |
| detail summary chip, `DetailView` | `resolveFieldCurrency({
...objField, ...sectionField }, tenantCurrency)` | the object def and
the section field, merged | yes. A section-level `currencyConfig`
replaces the object's as a whole. |
| gantt tooltip, `ObjectGantt` | `resolveFieldCurrency(def as any,
tenantCurrency)` | the object schema's def | yes |
| grid summary footer, `useColumnSummary` |
`resolveFieldCurrency(column, tenantDefault)` | hints carrying
`currencyConfig: meta?.currencyConfig` verbatim | yes |
| `GridField` computed currency column |
`resolveFieldCurrency(undefined, tenantCurrency)` | nothing, by design
(objectui#10355) | not applicable, unchanged |
| `ObjectGrid` mobile card amount | `resolveFieldCurrency(amountCol as
any, tenantCurrency)` | a table column that carries no currency key |
not applicable. See Out of scope. |
No consumer needed a change. Every consumer that reads a
`currencyConfig` passes it through whole.
## Pins
- **At the resolver**
(`packages/i18n/src/__tests__/resolveFieldCurrency.test.ts`): the truth
table under a USD tenant. It covers fixed, dynamic, parsed-empty (parsed
through the installed spec, and as a literal), no mode (the spec's
default is read from the spec itself), dynamic with no tenant default,
fixed with no code, an explicit `currency` over both modes, the legacy
`defaultCurrency` next to each mode, and none. A control row feeds the
showcase configs in objectstack at `246314df`, all `fixed` USD
(`account`, `field-zoo`, `semantic-zoo`), under a JPY tenant: they read
USD.
- **One agreement row per face:** list cell and field widget
(`packages/fields/src/__tests__/currencyMode.faces-10422.test.tsx`),
metric tile (`ObjectMetricWidget.currencyMode-10422.test.tsx`), detail
chip (`summaryChip.currencyMode-10422.test.tsx`), gantt tooltip
(`ObjectGantt.currencyMode-10422.test.tsx`), and grid summary
(`useColumnSummary.currencyMode-10422.test.tsx`). Each row asserts two
things. A dynamic field, and a spec-parsed empty config, read exactly
what a field with no `currencyConfig` reads on that face. That reference
is itself pinned to the tenant `$` as absolute bytes. A fixed JPY field
keeps `¥`. The grid's configured cells are held to the whole-def cell by
`gridFixedCurrency-10354.test.tsx`, which stays green. Its `dyn` row now
reads `$3,456` on both sides.
- **Tests the fix made false:**
`packages/fields/src/__tests__/resolveFieldCurrency.test.ts` and
`CurrencyField.minorUnits.test.tsx` used a mode-less `currencyConfig` to
stand for a fixed one. They now say `currencyMode: 'fixed'`. The
re-export test also gains a dynamic row.
## Ablation (on committed code, `34795f4b4`)
The mutation was the mode read, removed with `ablation-replace.mjs`. The
replacement text: `(config?.currencyMode === 'fixed' ?
config.defaultCurrency : undefined)` became `config?.defaultCurrency`.
- On disk: anchor `x1 -> x0`, replacement `x0 -> x1`, blob `f192e57d0f84
-> af1b29163c2f`. Inside the run, `grep -c` found the mode read 0 times
and the ablated line once.
- The resolver resolves through the vitest alias to `packages/i18n/src`,
so no build or `dist` step applies.
- Result over the 8 pin files: `Tests 19 failed | 43 passed (62)`. Every
dynamic and parsed-empty row went red, on the resolver and on all six
faces, as did the resolver's no-mode, dynamic-with-no-tenant and
legacy-with-dynamic rows. Every fixed and control row stayed green.
- Predicted versus observed: I predicted the legacy row GREEN and
observed it RED. Its second assertion pairs the legacy key with a
dynamic config, whose EUR the base read first. The pin's header records
this.
- Restore: blob after restore `f192e57d0f84` equals the blob at HEAD,
and `git diff HEAD` is empty.
**Reverse type check** (at `8380484d6`, after the closure build). I
wrote `currencyMode: 'fixd'` into the dynamic row of
`packages/fields/src/__tests__/resolveFieldCurrency.test.ts`. That file
imports the resolver through `@object-ui/fields` from
`@object-ui/i18n`'s rebuilt `dist` (`paths: {}`). `tsc -p
tsconfig.test.json` then fails with `TS2820: Type '"fixd"' is not
assignable to type '"fixed" | "dynamic" | undefined'. Did you mean
'"fixed"'?`, and `--listFilesOnly` lists both test files. Restored by
blob equality and an empty `git diff HEAD`.
## Local verification
Final head: `593f65aa3`. It differs from `8380484d6` by one line: a
regex in `currencyMode.faces-10422.test.tsx` now spells U+00A0 as an
escape instead of a raw byte. The same regex matches the same text.
- **At `593f65aa3`:**
- the 8 pin files, plus `gridFixedCurrency-10354`, plus the two tests
that read `currency.ts` as text (`machineLocaleCensus-9909`,
`CurrencyField.symbol`): all passed;
- `pnpm --filter @object-ui/fields type-check`: exit 0;
- `pnpm check:control-bytes`: OK;
- `pnpm check:new-line-citations`: 0 new;
- `node scripts/check-changeset-presence.mjs`: 9 source files of 6
released packages, 1 changeset declared;
- `pnpm check:changeset-claims`: no pending changeset names a touched
file, and the two bodies this PR adds or edits are self-consistent;
- `check-changeset-no-major`: exit 0;
- `check-changeset-overwrite` (report-only): it reports the intended
objectui#10354 body edit;
- `check:phantom-deps`, `check:vi-mock-specifiers`,
`check:vi-mock-inherit`, `check:vi-mock-override-shape`,
`check:test-path-roots`, `check:pending-changeset-literals`: all exit 0.
- **At `8380484d6`,** from the repo root: `vitest run packages/i18n/
packages/fields/` gave 257 files passed and 1 skipped, 4280 tests passed
and 7 skipped. `packages/plugin-dashboard/ packages/plugin-detail/
packages/plugin-gantt/` gave 406 passed and 1 skipped, 3857 tests passed
and 8 skipped. `packages/plugin-grid/` ran in two shards: 77 + 77 files,
687 + 786 tests passed.
- **Type-check at `8380484d6`,** after `turbo run build` over the
dependency closure of fields, plugin-dashboard, plugin-detail,
plugin-gantt and plugin-grid (14 tasks, i18n included): `type-check`
exited 0 for `@object-ui/i18n`, `fields`, `plugin-dashboard`,
`plugin-detail`, `plugin-gantt` and `plugin-grid`.
- **ESLint** over the 9 touched `.ts`/`.tsx` files: 0 errors. The 4
modified files keep their base counts exactly: `no-explicit-any`
warnings 3, 1, 1 and 0. The 5 new test files add 10 `no-explicit-any`
warnings, the idiom of the suites they copy (the `GanttView` stub, `as
any` data sources).
## The objectui#10354 changeset (bounded in-place fix)
Its "Behaviour change" paragraph said a `dynamic` field shows
`defaultCurrency` on the grid, "because `resolveFieldCurrency` does not
read `currencyMode`". That changeset is still pending, so this PR would
publish it false at the next release. The paragraph now says a field
whose `currencyConfig` is in `fixed` mode shows its code, and a
`dynamic` field still shows the tenant currency. The front matter is
byte-identical to the base (`od -c`).
The four conditions for an in-place fix:
1. It is the same defect: the sentence describes the resolver this PR
replaces.
2. The replacement text follows mechanically from the pinned truth
table.
3. No open claim holds the file: objectui#10354 is merged.
4. It falls in the same gate family: changesets.
This file is outside the claim's declared file surface. The report says
so.
## Acceptance notes
- Several comments restate the precedence without the `fixed` condition:
the `CurrencyCellRenderer` comment in `packages/fields/src/index.tsx`,
`CurrencyField.tsx`, the `columnCurrency` docblock in `GridField.tsx`,
and a `buildDefaultPageSchema.ts` comment. They are now incomplete, not
wrong about any key. Left alone, because they are outside this claim's
file surface. Carrier: none named.
- No objectui doc describes currency resolution, so no guide or README
page needed an update.
## Out of scope (reported to the seat, not fixed here)
- The `ObjectGrid` mobile card amount and the dashboard
`ObjectDataTable` cell (`buildFieldMeta` in `recordFields.tsx`) never
hand the resolver a `currencyConfig`. Throwaway probes, deleted
afterwards, measured both under a USD tenant with a fixed-JPY `1234`:
the desktop grid read `¥1,234`, the mobile card read `$1.2K`, and the
dashboard table cell read `$1,234`.
- objectstack's `skills/objectstack-data/rules/field-types.md` states
the resolution chain without the mode, and describes `dynamic` as a
per-record `{ value, currency }`. The spec says a currency value is a
bare number.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 808f339 commit ea9d17f
11 files changed
Lines changed: 710 additions & 18 deletions
File tree
- .changeset
- packages
- fields/src/__tests__
- i18n/src
- __tests__
- plugin-dashboard/src/__tests__
- plugin-detail/src/__tests__
- plugin-gantt/src
- plugin-grid/src/__tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
90 | 92 | | |
91 | 93 | | |
92 | | - | |
| 94 | + | |
93 | 95 | | |
94 | 96 | | |
95 | 97 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
7 | 7 | | |
8 | | - | |
9 | | - | |
| 8 | + | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
16 | 21 | | |
17 | 22 | | |
18 | 23 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
| |||
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
12 | | - | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
16 | | - | |
17 | | - | |
| 17 | + | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | | - | |
13 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
14 | 25 | | |
15 | 26 | | |
16 | 27 | | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
17 | 40 | | |
18 | 41 | | |
19 | 42 | | |
| |||
22 | 45 | | |
23 | 46 | | |
24 | 47 | | |
25 | | - | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
26 | 53 | | |
27 | 54 | | |
28 | 55 | | |
29 | 56 | | |
| 57 | + | |
30 | 58 | | |
31 | 59 | | |
32 | | - | |
| 60 | + | |
33 | 61 | | |
34 | 62 | | |
35 | 63 | | |
| |||
0 commit comments