|
6 | 6 | * LICENSE file in the root directory of this source tree. |
7 | 7 | */ |
8 | 8 |
|
| 9 | +/** |
| 10 | + * The currency config a field declares, in the three modes of |
| 11 | + * `@objectstack/spec`'s `CurrencyConfigSchema` (ADR-0144 D2 adds `record`). |
| 12 | + */ |
| 13 | +export interface FieldCurrencyConfig { |
| 14 | + currencyMode?: 'dynamic' | 'fixed' | 'record'; |
| 15 | + defaultCurrency?: string; |
| 16 | + /** `record` mode only: the sibling field holding each record's ISO 4217 code. */ |
| 17 | + currencyField?: string; |
| 18 | +} |
| 19 | + |
| 20 | +/** |
| 21 | + * The sibling code field a `record`-mode amount points at (ADR-0144 D2), or |
| 22 | + * `undefined` for a `dynamic` / `fixed` field and for anything that is not a |
| 23 | + * currency config at all. The one reading every surface that needs the |
| 24 | + * pointer itself — the form's code select, the grid footer's grouping test, |
| 25 | + * the filter builder — shares, so none of them re-derives the mode test. |
| 26 | + */ |
| 27 | +export function recordCurrencyField( |
| 28 | + field: { currencyConfig?: FieldCurrencyConfig } | null | undefined, |
| 29 | +): string | undefined { |
| 30 | + const config = field?.currencyConfig; |
| 31 | + if (config?.currencyMode !== 'record') return undefined; |
| 32 | + return typeof config.currencyField === 'string' && config.currencyField !== '' |
| 33 | + ? config.currencyField |
| 34 | + : undefined; |
| 35 | +} |
| 36 | + |
9 | 37 | /** |
10 | 38 | * Resolve the display currency code for a currency-typed field, in precedence |
11 | 39 | * order: |
12 | 40 | * |
| 41 | + * 0. a `record`-mode field (ADR-0144 D2) reads ONLY the record in scope: the |
| 42 | + * value of the sibling field `currencyConfig.currencyField` names. No |
| 43 | + * record, or a record whose code is blank, resolves to `undefined` — never |
| 44 | + * to steps 1–4. The record's code field is the only place that amount's |
| 45 | + * currency lives, so the tenant default would name a currency the amount |
| 46 | + * is not in: an amount of 100 EUR shown as `¥100` is worse than `100`; |
13 | 47 | * 1. the field's explicit `currency` — a per-node display code. The spec |
14 | 48 | * declares it on presentation surfaces (the metric widget's `currency`, a |
15 | 49 | * dataset measure's `currency`) and looser grid/column configs carry it; |
|
27 | 61 | * |
28 | 62 | * `currencyMode: 'dynamic'` — and a `currencyConfig` with no `currencyMode` at |
29 | 63 | * all, which `@objectstack/spec` defaults to `'dynamic'` — skips step 2 |
30 | | - * (objectui#10422). The spec describes that mode as "user selectable", and its |
31 | | - * `FieldSchema` guidance says a field without a fixed currency "uses the tenant |
32 | | - * default at runtime". No value carries a user-selected code today (a currency |
33 | | - * value is a bare number), so a dynamic field reads the tenant default, never |
34 | | - * its `defaultCurrency`. That matters beyond authored dynamic configs: the |
35 | | - * spec parses an EMPTY `currencyConfig` to |
36 | | - * `{ currencyMode: 'dynamic', defaultCurrency: 'CNY' }`, which must not render |
37 | | - * as yuan. If a value ever carries its own selected code, that code is the one |
38 | | - * to win, ahead of the tenant default. |
| 64 | + * (objectui#10422): a dynamic field reads the tenant default, never its |
| 65 | + * `defaultCurrency`. That matters beyond authored dynamic configs: until |
| 66 | + * @objectstack/spec dropped the `'CNY'` default (#27157), the spec parsed an |
| 67 | + * EMPTY `currencyConfig` to `{ currencyMode: 'dynamic', defaultCurrency: 'CNY' }`, |
| 68 | + * and rows stored or built by that parse still carry it until a rehydration |
| 69 | + * strips it; it must not render as yuan. The spec now refuses a `fixed` |
| 70 | + * config without a code at authoring and save, but a stored row saved that |
| 71 | + * way before is kept as authored and still loads with no code; step 2 skips |
| 72 | + * it and the tenant default shows, as it always did. A per-record currency is |
| 73 | + * step 0's, and only a `record`-mode field has one. |
39 | 74 | * |
40 | 75 | * This is the single resolution every field / measure / cell renderer shares - |
41 | 76 | * ending the per-renderer drift where some read only `currency`, others only |
42 | 77 | * `defaultCurrency`, and others `currencyConfig`. It lives in `@object-ui/i18n` |
43 | 78 | * (alongside `useLocalization`) because the tenant default is a localization |
44 | 79 | * concern; `@object-ui/fields` re-exports it for backward compatibility. |
| 80 | + * |
| 81 | + * `record` is the row / form values the amount sits in. A caller that holds no |
| 82 | + * record (a column-level header, a footer over many rows) passes none, and a |
| 83 | + * `record`-mode field then resolves to no currency, which is the honest answer |
| 84 | + * for a set of amounts in different currencies. |
45 | 85 | */ |
46 | 86 | export function resolveFieldCurrency( |
47 | 87 | field: |
48 | 88 | | { |
49 | 89 | currency?: string; |
50 | 90 | defaultCurrency?: string; |
51 | | - currencyConfig?: { currencyMode?: 'dynamic' | 'fixed'; defaultCurrency?: string }; |
| 91 | + currencyConfig?: FieldCurrencyConfig; |
52 | 92 | } |
53 | 93 | | null |
54 | 94 | | undefined, |
55 | 95 | tenantDefault?: string, |
| 96 | + record?: Readonly<Record<string, unknown>> | null, |
56 | 97 | ): string | undefined { |
57 | 98 | const config = field?.currencyConfig; |
| 99 | + if (config?.currencyMode === 'record') { |
| 100 | + const codeField = recordCurrencyField(field); |
| 101 | + const code = codeField !== undefined ? record?.[codeField] : undefined; |
| 102 | + return typeof code === 'string' && code !== '' ? code : undefined; |
| 103 | + } |
58 | 104 | return ( |
59 | 105 | field?.currency || |
60 | 106 | (config?.currencyMode === 'fixed' ? config.defaultCurrency : undefined) || |
|
0 commit comments