@asteby/metacore-runtime-react 18.3.0 → 18.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d7c792d: Render one_to_many relations as a rich table (headers + currency/image/date/badge cells)
8
+
9
+ `OneToManyRelation` now renders the child list as a real metadata-driven table
10
+ using the same metacore-ui `<Table>` primitives and the exact
11
+ `makeDefaultGetDynamicColumns` cell factory as `<DynamicTable>`, instead of a
12
+ bare flex grid of unlabeled values. Line items now get column headers, money in
13
+ the org currency right-aligned (e.g. `100,00 MXN`), FK thumbnails + labels,
14
+ dates in the org timezone, status/option badges and creator names — matching
15
+ the main dynamic table. The inline edit (DynamicForm dialog) and delete
16
+ (AlertDialog) actions are preserved as a trailing actions column.
17
+
18
+ The org `timeZone`/`currency` contexts were extracted from
19
+ `dialogs/dynamic-record` into a shared `org-runtime-context` module so the
20
+ relation table can consume them without a circular import. `ManyToManyRelation`
21
+ is unchanged.
22
+
23
+ ## 18.4.0
24
+
25
+ ### Minor Changes
26
+
27
+ - c7fb1ad: Org-currency-aware money formatting in dynamic tables + the record dialog.
28
+
29
+ `<DynamicTable>` and `<DynamicRecordDialog>` now accept an optional `currency`
30
+ prop (the org's ISO-4217 code, e.g. `MXN`, threaded from org config like
31
+ `timeZone`). Money columns (`type:'number'` + `cellStyle:'currency'`) without an
32
+ explicit per-column currency now fall back to the org currency instead of
33
+ hardcoded `USD` — `resolveCurrency(col, orgCurrency)`. The record dialog, which
34
+ previously showed raw numbers, now formats money fields as a currency string in
35
+ the view renderer: a field is treated as money when the backend stamps
36
+ `cellStyle:'currency'`, or — as a robustness fallback mirroring the backend's
37
+ `inferDisplayCellStyle` — when it's numeric and its key matches the money
38
+ heuristic (`price`/`amount`/`total`/`cost`/`subtotal`/`balance`/`paid`, as the
39
+ whole key or a `_<m>`/`<m>_` affix). Editable inputs stay numeric.
40
+
3
41
  ## 18.3.0
4
42
 
5
43
  ### Minor Changes
@@ -45,6 +45,19 @@ export interface FieldDef {
45
45
  * its SQL type. Unknown values fall through to the `type`-based default.
46
46
  */
47
47
  widget?: string;
48
+ /**
49
+ * Declarative display hint the backend stamps on the column/modal field
50
+ * (mirrors the table column's `cellStyle`). `'currency'` makes the view
51
+ * renderer format the numeric value in the org currency. Optional —
52
+ * absent, a money-key heuristic still detects obvious money fields.
53
+ */
54
+ cellStyle?: string;
55
+ /**
56
+ * Per-field style overrides served alongside `cellStyle` (e.g.
57
+ * `{ currency: 'MXN' }`). When it carries an explicit `currency` it wins
58
+ * over the org fallback.
59
+ */
60
+ styleConfig?: Record<string, any>;
48
61
  }
49
62
  export interface DynamicRecordDialogProps {
50
63
  open: boolean;
@@ -118,9 +131,16 @@ export interface DynamicRecordDialogProps {
118
131
  * regardless of the viewer's browser timezone. Pure `date` values pin to UTC.
119
132
  */
120
133
  timeZone?: string;
134
+ /**
135
+ * Org ISO-4217 currency code (e.g. `MXN`) used as the fallback for money
136
+ * fields (`cellStyle:'currency'` or the money-key heuristic) that lack an
137
+ * explicit per-field currency. Optional — defaults to 'USD'.
138
+ */
139
+ currency?: string;
121
140
  }
122
- export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, }: DynamicRecordDialogProps): import("react").JSX.Element;
123
- export declare function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, }: {
141
+ export declare function isMoneyField(field: FieldDef, value: any): boolean;
142
+ export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, }: DynamicRecordDialogProps): import("react").JSX.Element;
143
+ export declare function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, currency: currencyProp, }: {
124
144
  field: FieldDef;
125
145
  value: any;
126
146
  record: any;
@@ -128,5 +148,7 @@ export declare function ViewValue({ field, value: rawValue, record, getImageUrl:
128
148
  getImageUrl?: GetImageUrl;
129
149
  /** Optional override; when omitted falls back to the nearest provider. */
130
150
  timeZone?: string;
151
+ /** Optional override; when omitted falls back to the nearest provider. */
152
+ currency?: string;
131
153
  }): import("react").JSX.Element;
132
154
  //# sourceMappingURL=dynamic-record.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AA6C1C,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAK1F,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;IACpH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACpC;AAiCD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;2DAEuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAqID,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAUjE;AAED,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,aAAa,EACb,WAA8B,EAC9B,QAAQ,EACR,QAAQ,GACX,EAAE,wBAAwB,+BAuW1B;AAgGD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,KAAK,EAAE,QAAQ,EACf,MAAM,EACN,WAAW,EAAE,eAAe,EAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,GACzB,EAAE;IACC,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,EAAE,GAAG,CAAA;IACX,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,+BA0JA"}
@@ -28,6 +28,7 @@ import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
28
28
  import { humanizeToken } from '../dynamic-columns-helpers';
29
29
  import { formatDateCell } from '../dynamic-columns';
30
30
  import { ImageUrlContext, identityImageUrl } from '../image-url-context';
31
+ import { TimeZoneContext, CurrencyContext } from '../org-runtime-context';
31
32
  // localizedModelName resolves the (possibly addon-i18n) model name: prefer the
32
33
  // translated titleKey, fall back to the backend-provided raw title.
33
34
  function localizedModelName(meta, t) {
@@ -157,8 +158,28 @@ const MODE_CONFIG = {
157
158
  // Context threading host runtime values to nested field components (uploads,
158
159
  // image leads, tz-aware dates) without prop-drilling through every renderer.
159
160
  const ModelContext = createContext('');
160
- const TimeZoneContext = createContext(undefined);
161
- export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, }) {
161
+ // Money-key heuristic mirroring the backend's `inferDisplayCellStyle`: lets the
162
+ // dialog format obvious money fields as currency even when the backend hasn't
163
+ // stamped `cellStyle:'currency'` yet. Case-insensitive; matches a key that
164
+ // equals one of these, or ends with `_<m>`, or starts with `<m>_`.
165
+ const MONEY_KEY_HEURISTIC = ['price', 'amount', 'total', 'cost', 'subtotal', 'balance', 'paid'];
166
+ // isMoneyField decides whether a field should render as currency. The explicit
167
+ // `cellStyle:'currency'` stamp always wins; otherwise a numeric value whose key
168
+ // matches the money heuristic qualifies (robustness fallback).
169
+ export function isMoneyField(field, value) {
170
+ if (field.cellStyle === 'currency')
171
+ return true;
172
+ if (value === null || value === undefined || value === '')
173
+ return false;
174
+ const num = typeof value === 'number' ? value : Number(value);
175
+ if (isNaN(num))
176
+ return false;
177
+ const key = String(field.key || '').toLowerCase();
178
+ if (!key)
179
+ return false;
180
+ return MONEY_KEY_HEURISTIC.some(m => key === m || key.endsWith(`_${m}`) || key.startsWith(`${m}_`));
181
+ }
182
+ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, }) {
162
183
  const api = useApi();
163
184
  const { t } = useTranslation();
164
185
  const [modalMeta, setModalMeta] = useState(schema ? schema : null);
@@ -387,10 +408,10 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
387
408
  return false;
388
409
  return true;
389
410
  }) ?? [];
390
- return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsxs(TimeZoneContext.Provider, { value: timeZone, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
391
- const isFullWidth = field.type === 'textarea';
392
- return (_jsx("div", { className: isFullWidth ? 'sm:col-span-2' : '', children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
393
- }), record?.external_url && (_jsx("div", { className: "sm:col-span-2", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit' }) }))] }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel }), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }));
411
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
412
+ const isFullWidth = field.type === 'textarea';
413
+ return (_jsx("div", { className: isFullWidth ? 'sm:col-span-2' : '', children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
414
+ }), record?.external_url && (_jsx("div", { className: "sm:col-span-2", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit' }) }))] }) }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel }), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }));
394
415
  }
395
416
  function LoadingSkeleton() {
396
417
  return (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-9 w-full" })] }, i))) }));
@@ -439,11 +460,14 @@ function RelationViewValue({ field, value, record }) {
439
460
  };
440
461
  return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(OptionLead, { option: lead, size: 24 }), _jsx("span", { className: "text-sm", children: label ?? '—' })] }));
441
462
  }
442
- export function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, }) {
463
+ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, currency: currencyProp, }) {
464
+ const { i18n } = useTranslation();
443
465
  const ctxImageUrl = useContext(ImageUrlContext);
444
466
  const ctxTimeZone = useContext(TimeZoneContext);
467
+ const ctxCurrency = useContext(CurrencyContext);
445
468
  const getImageUrl = getImageUrlProp ?? ctxImageUrl;
446
469
  const timeZone = timeZoneProp ?? ctxTimeZone;
470
+ const currency = currencyProp ?? ctxCurrency;
447
471
  // created_by / avatar resolver sibling → name (+ avatar) instead of "—".
448
472
  if (field.type === 'avatar' || field.key === 'created_by' || field.key === 'created_by_id') {
449
473
  const user = createdBySibling(rawValue, record);
@@ -481,6 +505,23 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
481
505
  if (field.type === 'url' && value) {
482
506
  return (_jsx("a", { href: value, target: "_blank", rel: "noreferrer", className: "text-sm text-primary hover:underline truncate", children: value }));
483
507
  }
508
+ // Money → org-currency string. Detected by the backend `cellStyle:'currency'`
509
+ // stamp or a numeric value whose key matches the money heuristic (fallback
510
+ // mirroring the table cell + backend `inferDisplayCellStyle`).
511
+ if (isMoneyField(field, value)) {
512
+ const num = typeof value === 'number' ? value : Number(value);
513
+ if (!isNaN(num)) {
514
+ const resolvedCurrency = field.styleConfig?.currency || currency || 'USD';
515
+ const localeTag = i18n.language || 'es';
516
+ const formatted = new Intl.NumberFormat(localeTag, {
517
+ style: 'currency',
518
+ currency: resolvedCurrency,
519
+ minimumFractionDigits: 2,
520
+ maximumFractionDigits: 2,
521
+ }).format(num);
522
+ return _jsx("p", { className: "text-sm py-1 tabular-nums", children: formatted });
523
+ }
524
+ }
484
525
  // Date/datetime/timestamp → tz-aware format. `date` pins to UTC (calendar
485
526
  // day); instants render in the org timezone with a full-precision tooltip.
486
527
  if (field.type === 'date' || field.type === 'datetime' || field.type === 'timestamp') {
@@ -16,7 +16,7 @@ export interface ColumnFilterConfig {
16
16
  searchEndpoint?: string;
17
17
  }
18
18
  /** Signature for the host-provided `getDynamicColumns` factory. */
19
- export type GetDynamicColumns = (metadata: TableMetadata, handleAction: (action: string, row: any) => void, t: (key: string, options?: any) => string, language: string, columnFilterConfigs: Map<string, ColumnFilterConfig>, timeZone?: string) => ColumnDef<any>[];
19
+ export type GetDynamicColumns = (metadata: TableMetadata, handleAction: (action: string, row: any) => void, t: (key: string, options?: any) => string, language: string, columnFilterConfigs: Map<string, ColumnFilterConfig>, timeZone?: string, currency?: string) => ColumnDef<any>[];
20
20
  /** Signature for the host-provided `DynamicIcon` renderer. */
21
21
  export type DynamicIconComponent = React.ComponentType<{
22
22
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns-shim.d.ts","sourceRoot":"","sources":["../src/dynamic-columns-shim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;IAClF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAChD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,EACzC,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACpD,QAAQ,CAAC,EAAE,MAAM,KAChB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;AAErB,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA"}
1
+ {"version":3,"file":"dynamic-columns-shim.d.ts","sourceRoot":"","sources":["../src/dynamic-columns-shim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;IAClF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAChD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,EACzC,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACpD,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,KAChB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;AAErB,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA"}
@@ -16,6 +16,12 @@ export interface DynamicColumnsHelpers {
16
16
  */
17
17
  apiBaseUrl?: string;
18
18
  }
19
+ /**
20
+ * Resolves the active currency for a column: the column's explicit currency
21
+ * style wins, then the org-level fallback (org config, like `timeZone`), then
22
+ * 'USD' as a last resort.
23
+ */
24
+ export declare const resolveCurrency: (col: ColumnDefinition, orgCurrency?: string) => string;
19
25
  /**
20
26
  * State-machine gate for per-row actions.
21
27
  *
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAgC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAgGD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AA0ED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAumBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAgC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAoErD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AA0ED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAwmBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -44,8 +44,12 @@ const styleCfg = (col, ...keys) => {
44
44
  return undefined;
45
45
  };
46
46
  const EmptyCell = () => _jsx("span", { className: "text-muted-foreground", children: "-" });
47
- /** Resolves the active org currency, defaulting to USD when no override. */
48
- const resolveCurrency = (col) => styleCfg(col, 'currency') || 'USD';
47
+ /**
48
+ * Resolves the active currency for a column: the column's explicit currency
49
+ * style wins, then the org-level fallback (org config, like `timeZone`), then
50
+ * 'USD' as a last resort.
51
+ */
52
+ export const resolveCurrency = (col, orgCurrency) => styleCfg(col, 'currency') || orgCurrency || 'USD';
49
53
  const formatNumber = (value, opts, locale) => new Intl.NumberFormat(locale || undefined, opts).format(value);
50
54
  /**
51
55
  * Semantic status → badge color. Used by the `status` cell when no explicit
@@ -351,7 +355,7 @@ const AvatarCell = ({ name, desc, avatarSrc, getImageUrl }) => (_jsxs("div", { c
351
355
  export function makeDefaultGetDynamicColumns(helpers = {}) {
352
356
  const getImageUrl = helpers.getImageUrl ?? defaultGetImageUrl;
353
357
  const apiBaseUrl = helpers.apiBaseUrl ?? '';
354
- return function defaultGetDynamicColumns(metadata, onAction, t, currentLanguage, filterConfigs, timeZone) {
358
+ return function defaultGetDynamicColumns(metadata, onAction, t, currentLanguage, filterConfigs, timeZone, currency) {
355
359
  const dateLocale = currentLanguage === 'en' ? enUS : es;
356
360
  const columns = [
357
361
  {
@@ -552,7 +556,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
552
556
  const decimals = styleCfg(col, 'decimals') ?? 2;
553
557
  return (_jsx("span", { className: "block text-right font-medium tabular-nums", children: formatNumber(num, {
554
558
  style: 'currency',
555
- currency: resolveCurrency(col),
559
+ currency: resolveCurrency(col, currency),
556
560
  minimumFractionDigits: decimals,
557
561
  maximumFractionDigits: decimals,
558
562
  }, currentLanguage) }));
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AA6CA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,GACjB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;CACtB;AAiBD,UAAU,WAAW;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,6BAA8B,SAAQ,WAAW;IAC9D,IAAI,EAAE,aAAa,CAAA;IACnB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,8BAA+B,SAAQ,WAAW;IAC/D,IAAI,EAAE,cAAc,CAAA;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC1B,6BAA6B,GAC7B,8BAA8B,CAAA;AAEpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAK1D"}
1
+ {"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AA+DA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,GACjB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;CACtB;AAiBD,UAAU,WAAW;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,6BAA8B,SAAQ,WAAW;IAC9D,IAAI,EAAE,aAAa,CAAA;IACnB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,8BAA+B,SAAQ,WAAW;IAC/D,IAAI,EAAE,cAAc,CAAA;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC1B,6BAA6B,GAC7B,8BAA8B,CAAA;AAEpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAK1D"}
@@ -5,15 +5,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  // - "many_to_many": multi-select sobre la tabla destino con sync a la pivot.
6
6
  // La RFC completa vive en `packages/runtime-react/docs/relations.md`.
7
7
  import { useCallback, useEffect, useMemo, useState } from 'react';
8
- import { Button, Skeleton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Dialog, DialogContent, DialogHeader, DialogTitle, MultiSelect, } from '@asteby/metacore-ui/primitives';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { flexRender, getCoreRowModel, useReactTable, } from '@tanstack/react-table';
10
+ import { cn } from '@asteby/metacore-ui/lib';
11
+ import { Button, Skeleton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Dialog, DialogContent, DialogHeader, DialogTitle, MultiSelect, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@asteby/metacore-ui/primitives';
9
12
  import { Plus, Trash2, Pencil } from 'lucide-react';
10
13
  import { useApi } from './api-context';
11
14
  import { useMetadataCache } from './metadata-cache';
12
15
  import { DynamicForm } from './dynamic-form';
13
16
  import { useImageUrl } from './image-url-context';
14
- import { OptionThumb } from './dynamic-select-field';
17
+ import { useTimeZone, useCurrency } from './org-runtime-context';
18
+ import { makeDefaultGetDynamicColumns } from './dynamic-columns';
15
19
  import { useOptionsResolver } from './use-options-resolver';
16
- import { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, formatRelationCell, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
20
+ import { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
17
21
  export { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, formatRelationCell, objectLabel, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
18
22
  const DEFAULT_STRINGS = {
19
23
  title: '',
@@ -38,6 +42,9 @@ export function DynamicRelation(props) {
38
42
  function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoint, hiddenColumns = [], canCreate = true, canDelete = true, canEdit = true, strings, className, onChange, }) {
39
43
  const api = useApi();
40
44
  const getImageUrl = useImageUrl();
45
+ const timeZone = useTimeZone();
46
+ const currency = useCurrency();
47
+ const { i18n } = useTranslation();
41
48
  const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache();
42
49
  const cachedMeta = getMetadata(model);
43
50
  const labels = { ...DEFAULT_STRINGS, ...(strings || {}) };
@@ -88,6 +95,43 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
88
95
  const hidden = new Set([foreignKey, ...Object.keys(filters || {}), ...hiddenColumns]);
89
96
  return metadata.columns.filter(c => !hidden.has(c.key) && !c.hidden);
90
97
  }, [metadata, foreignKey, filtersKey, hiddenColumns]);
98
+ // Reuse the EXACT column factory the main `<DynamicTable>` uses so each cell
99
+ // renders identically — money in the org currency right-aligned, FK chips
100
+ // with thumbnails, dates in the org timezone, status/option badges, creator
101
+ // names — instead of a hand-rolled parallel formatting stack. Stable per
102
+ // image-url resolver.
103
+ const buildColumns = useMemo(() => makeDefaultGetDynamicColumns({ getImageUrl }), [getImageUrl]);
104
+ const showActions = canEdit || canDelete;
105
+ const columns = useMemo(() => {
106
+ if (!metadata)
107
+ return [];
108
+ // Feed the factory a metadata view scoped to the visible columns only,
109
+ // with model-level actions stripped — the relation list owns its own
110
+ // inline edit/delete column (appended below), and the factory's
111
+ // select/actions columns don't belong in an embedded child list.
112
+ const scopedMeta = {
113
+ ...metadata,
114
+ columns: visibleColumns,
115
+ actions: [],
116
+ hasActions: false,
117
+ enableCRUDActions: false,
118
+ };
119
+ const base = buildColumns(scopedMeta, () => { }, (key, opts) => opts?.defaultValue ?? key, i18n?.language || 'es', new Map(), timeZone, currency).filter((c) => c.id !== 'select' && c.id !== 'actions');
120
+ if (!showActions)
121
+ return base;
122
+ const actionsCol = {
123
+ id: 'actions',
124
+ header: () => _jsx("span", { className: "sr-only", children: labels.editLabel }),
125
+ size: 80,
126
+ cell: ({ row }) => (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [canEdit && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => { setEditingRow(row.original); setFormOpen(true); }, "aria-label": labels.editLabel, children: _jsx(Pencil, { className: "h-4 w-4" }) })), canDelete && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setRowToDelete(row.original), "aria-label": labels.removeLabel, children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })),
127
+ };
128
+ return [...base, actionsCol];
129
+ }, [metadata, visibleColumns, buildColumns, i18n?.language, timeZone, currency, showActions, canEdit, canDelete, labels.editLabel, labels.removeLabel]);
130
+ const table = useReactTable({
131
+ data: rows,
132
+ columns,
133
+ getCoreRowModel: getCoreRowModel(),
134
+ });
91
135
  const handleSubmit = useCallback(async (values) => {
92
136
  setSubmitting(true);
93
137
  try {
@@ -137,21 +181,10 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
137
181
  setSubmitting(false);
138
182
  }
139
183
  }, [api, dataEndpoint, fetchAll, onChange, rowToDelete]);
140
- return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate) && (_jsxs("div", { className: "flex items-center justify-between pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), canCreate && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => { setEditingRow(null); setFormOpen(true); }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), labels.addLabel] }))] })), loading ? (_jsx("div", { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-10 w-full" }, `rel-skeleton-${i}`))) })) : rows.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsx("div", { className: "border rounded-md divide-y bg-card", children: rows.map((row, idx) => (_jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2", children: [_jsx("div", { className: "flex-1 grid grid-cols-[repeat(auto-fit,minmax(0,1fr))] gap-2 text-sm", children: visibleColumns.map(col => {
141
- const cell = formatRelationCell(row, col);
142
- // FK column whose backend-resolved sibling
143
- // carries an image render a thumbnail + label
144
- // instead of plain text (e.g. a line item's
145
- // product photo). The sibling is the column key
146
- // with the trailing `_id` stripped.
147
- const isFk = !!col.ref || col.key.endsWith('_id');
148
- const sibling = isFk ? row[col.key.replace(/_id$/, '')] : undefined;
149
- if (sibling && typeof sibling === 'object' && sibling.image) {
150
- const label = sibling.label ?? sibling.name ?? cell;
151
- return (_jsxs("span", { className: "flex min-w-0 items-center gap-2", title: String(label), children: [_jsx(OptionThumb, { image: getImageUrl(sibling.image), size: 20 }), _jsx("span", { className: "truncate", children: label })] }, col.key));
152
- }
153
- return (_jsx("span", { className: "truncate", title: cell, children: cell }, col.key));
154
- }) }), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [canEdit && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => { setEditingRow(row); setFormOpen(true); }, "aria-label": labels.editLabel, children: _jsx(Pencil, { className: "h-4 w-4" }) })), canDelete && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setRowToDelete(row), "aria-label": labels.removeLabel, children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })] }, relationRowKey(row, idx, foreignKey)))) })), _jsx(Dialog, { open: formOpen, onOpenChange: (open) => { setFormOpen(open); if (!open)
184
+ return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate) && (_jsxs("div", { className: "flex items-center justify-between pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), canCreate && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => { setEditingRow(null); setFormOpen(true); }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), labels.addLabel] }))] })), loading ? (_jsx("div", { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-10 w-full" }, `rel-skeleton-${i}`))) })) : rows.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsx("div", { className: "overflow-x-auto border rounded-md bg-card", children: _jsxs(Table, { noWrapper: true, className: "w-full", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { className: "border-b-0 hover:bg-transparent", children: headerGroup.headers.map((header) => {
185
+ const isActions = header.id === 'actions';
186
+ return (_jsx(TableHead, { colSpan: header.colSpan, style: header.column.columnDef.size ? { width: header.column.columnDef.size } : undefined, className: cn('bg-card border-b h-10', isActions && 'text-right'), children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
187
+ }) }, headerGroup.id))) }), _jsx(TableBody, { children: table.getRowModel().rows.map((row, idx) => (_jsx(TableRow, { children: row.getVisibleCells().map((cell) => (_jsx(TableCell, { style: cell.column.columnDef.size ? { width: cell.column.columnDef.size } : undefined, className: "py-2", children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, relationRowKey(row.original, idx, foreignKey)))) })] }) })), _jsx(Dialog, { open: formOpen, onOpenChange: (open) => { setFormOpen(open); if (!open)
155
188
  setEditingRow(null); }, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: editingRow ? labels.editLabel : labels.addLabel }) }), _jsx(DynamicForm, { fields: formFields, initialValues: editingRow || undefined, onSubmit: handleSubmit, onCancel: () => { setFormOpen(false); setEditingRow(null); }, submitLabel: labels.saveLabel, cancelLabel: labels.cancelLabel, disabled: submitting })] }) }), _jsx(AlertDialog, { open: !!rowToDelete, onOpenChange: (open) => !open && setRowToDelete(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: labels.confirmRemoveTitle }), _jsx(AlertDialogDescription, { children: labels.confirmRemoveDescription })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: submitting, children: labels.cancelLabel }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); handleDelete(); }, className: "bg-red-600 hover:bg-red-700", disabled: submitting, children: labels.removeLabel })] })] }) })] }));
156
189
  }
157
190
  function ManyToManyRelation({ kind, through, references, foreignKey, referencesKey, parentId, filters, pivotEndpoint, referencesEndpoint, displayKey, canCreate = true, canDelete = true, strings, className, onChange, }) {
@@ -23,7 +23,13 @@ interface DynamicTableProps {
23
23
  * Optional — omitting it preserves the legacy browser-local formatting.
24
24
  */
25
25
  timeZone?: string;
26
+ /**
27
+ * ISO 4217 currency code (e.g. the org's `MXN`) used as the fallback for
28
+ * money cells (`type:'number'` + `cellStyle:'currency'`) that don't carry
29
+ * an explicit per-column currency. Optional — defaults to 'USD'.
30
+ */
31
+ currency?: string;
26
32
  }
27
- export declare function DynamicTable({ model, endpoint, enableUrlSync, hiddenColumns, onAction, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, timeZone, }: DynamicTableProps): import("react").JSX.Element;
33
+ export declare function DynamicTable({ model, endpoint, enableUrlSync, hiddenColumns, onAction, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, timeZone, currency, }: DynamicTableProps): import("react").JSX.Element;
28
34
  export {};
29
35
  //# sourceMappingURL=dynamic-table.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAUnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,GACX,EAAE,iBAAiB,+BAgyBnB"}
1
+ {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAUnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,GACX,EAAE,iBAAiB,+BAgyBnB"}
@@ -31,7 +31,7 @@ import { getSearchableColumnKeys } from './column-visibility';
31
31
  import { DynamicRecordDialog } from './dialogs/dynamic-record';
32
32
  import { ExportDialog } from './dialogs/export';
33
33
  import { ImportDialog } from './dialogs/import';
34
- export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColumns = [], onAction, refreshTrigger, defaultFilters, extraColumns = [], getDynamicColumns = defaultGetDynamicColumns, timeZone, }) {
34
+ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColumns = [], onAction, refreshTrigger, defaultFilters, extraColumns = [], getDynamicColumns = defaultGetDynamicColumns, timeZone, currency, }) {
35
35
  const { t, i18n } = useTranslation();
36
36
  const api = useApi();
37
37
  const currentBranch = useCurrentBranch();
@@ -555,12 +555,12 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
555
555
  const rowMetadata = metadata.actions?.some((a) => a.placement === 'table' || a.placement === 'create')
556
556
  ? { ...metadata, actions: metadata.actions.filter((a) => !a.placement || a.placement === 'row') }
557
557
  : metadata;
558
- const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone);
558
+ const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone, currency);
559
559
  const filteredBase = baseColumns.filter((col) => !hiddenColumns.includes(col.id));
560
560
  const actionsCol = filteredBase.find((c) => c.id === 'actions');
561
561
  const otherCols = filteredBase.filter((c) => c.id !== 'actions');
562
562
  return [...otherCols, ...extraColumns, ...(actionsCol ? [actionsCol] : [])];
563
- }, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone]);
563
+ }, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone, currency]);
564
564
  const filters = useMemo(() => [], []);
565
565
  const table = useReactTable({
566
566
  data,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * IANA timezone (e.g. the org's `America/Mexico_City`) used to render
3
+ * datetime/timestamp cells. `undefined` outside a provider → renderers fall
4
+ * back to the viewer's browser zone (legacy behaviour).
5
+ */
6
+ export declare const TimeZoneContext: import("react").Context<string | undefined>;
7
+ /** Reads the nearest org timezone (undefined outside a provider). */
8
+ export declare const useTimeZone: () => string | undefined;
9
+ /**
10
+ * Org ISO-4217 currency (org config, like the timezone) used as the fallback
11
+ * for money cells/fields that don't carry an explicit per-column currency.
12
+ * `undefined` outside a provider → renderers fall back to 'USD'.
13
+ */
14
+ export declare const CurrencyContext: import("react").Context<string | undefined>;
15
+ /** Reads the nearest org currency (undefined outside a provider). */
16
+ export declare const useCurrency: () => string | undefined;
17
+ //# sourceMappingURL=org-runtime-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-runtime-context.d.ts","sourceRoot":"","sources":["../src/org-runtime-context.ts"],"names":[],"mappings":"AASA;;;;GAIG;AACH,eAAO,MAAM,eAAe,6CAA+C,CAAA;AAE3E,qEAAqE;AACrE,eAAO,MAAM,WAAW,0BAAoC,CAAA;AAE5D;;;;GAIG;AACH,eAAO,MAAM,eAAe,6CAA+C,CAAA;AAE3E,qEAAqE;AACrE,eAAO,MAAM,WAAW,0BAAoC,CAAA"}
@@ -0,0 +1,24 @@
1
+ // Org runtime contexts — thread the org's display config (timezone, currency)
2
+ // to nested field/cell/relation renderers without prop-drilling. Lives in its
3
+ // own module (mirroring `image-url-context`) so any renderer can consume them
4
+ // without importing from `dialogs/dynamic-record`. That dialog imports
5
+ // `dynamic-relations` → `dynamic-relation`, so the relation table cannot import
6
+ // these contexts back from the dialog without a circular import — hence this
7
+ // standalone module is the single source of truth.
8
+ import { createContext, useContext } from 'react';
9
+ /**
10
+ * IANA timezone (e.g. the org's `America/Mexico_City`) used to render
11
+ * datetime/timestamp cells. `undefined` outside a provider → renderers fall
12
+ * back to the viewer's browser zone (legacy behaviour).
13
+ */
14
+ export const TimeZoneContext = createContext(undefined);
15
+ /** Reads the nearest org timezone (undefined outside a provider). */
16
+ export const useTimeZone = () => useContext(TimeZoneContext);
17
+ /**
18
+ * Org ISO-4217 currency (org config, like the timezone) used as the fallback
19
+ * for money cells/fields that don't carry an explicit per-column currency.
20
+ * `undefined` outside a provider → renderers fall back to 'USD'.
21
+ */
22
+ export const CurrencyContext = createContext(undefined);
23
+ /** Reads the nearest org currency (undefined outside a provider). */
24
+ export const useCurrency = () => useContext(CurrencyContext);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.3.0",
3
+ "version": "18.5.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,62 @@
1
+ // Locks the org-currency fallback contract for money rendering: the table cell
2
+ // (`resolveCurrency`) and the record dialog (`isMoneyField`). Pure logic, no DOM.
3
+ import { describe, it, expect } from 'vitest'
4
+ import { resolveCurrency } from '../dynamic-columns'
5
+ import { isMoneyField } from '../dialogs/dynamic-record'
6
+ import type { ColumnDefinition } from '../types'
7
+ import type { FieldDef } from '../dialogs/dynamic-record'
8
+
9
+ const col = (over: Partial<ColumnDefinition>): ColumnDefinition => ({
10
+ key: 'total',
11
+ label: 'Total',
12
+ type: 'number',
13
+ sortable: true,
14
+ filterable: true,
15
+ ...over,
16
+ })
17
+
18
+ const field = (over: Partial<FieldDef>): FieldDef => ({
19
+ key: 'total',
20
+ label: 'Total',
21
+ type: 'number',
22
+ ...over,
23
+ })
24
+
25
+ describe('resolveCurrency', () => {
26
+ it("defaults to 'USD' when neither the column nor the org provide one", () => {
27
+ expect(resolveCurrency(col({}))).toBe('USD')
28
+ })
29
+
30
+ it('falls back to the org currency when the column has no explicit currency', () => {
31
+ expect(resolveCurrency(col({}), 'MXN')).toBe('MXN')
32
+ })
33
+
34
+ it('prefers the explicit per-column currency over the org fallback', () => {
35
+ expect(resolveCurrency(col({ styleConfig: { currency: 'EUR' } }), 'MXN')).toBe('EUR')
36
+ })
37
+ })
38
+
39
+ describe('isMoneyField', () => {
40
+ it("recognizes the backend cellStyle:'currency' stamp regardless of key", () => {
41
+ expect(isMoneyField(field({ key: 'foo', cellStyle: 'currency' }), 100)).toBe(true)
42
+ })
43
+
44
+ it('detects numeric money keys via the heuristic (no stamp needed)', () => {
45
+ expect(isMoneyField(field({ key: 'total' }), 100)).toBe(true)
46
+ expect(isMoneyField(field({ key: 'sub_total' }), 100)).toBe(true)
47
+ expect(isMoneyField(field({ key: 'tax_amount' }), 16)).toBe(true)
48
+ expect(isMoneyField(field({ key: 'unit_price' }), 5)).toBe(true)
49
+ expect(isMoneyField(field({ key: 'balance' }), 0)).toBe(true)
50
+ })
51
+
52
+ it('accepts numeric strings the backend serializes', () => {
53
+ expect(isMoneyField(field({ key: 'total' }), '100')).toBe(true)
54
+ })
55
+
56
+ it('ignores non-money keys and non-numeric values', () => {
57
+ expect(isMoneyField(field({ key: 'name' }), 'Acme')).toBe(false)
58
+ expect(isMoneyField(field({ key: 'quantity' }), 3)).toBe(false)
59
+ expect(isMoneyField(field({ key: 'total' }), 'n/a')).toBe(false)
60
+ expect(isMoneyField(field({ key: 'total' }), null)).toBe(false)
61
+ })
62
+ })
@@ -57,6 +57,7 @@ import { humanizeToken } from '../dynamic-columns-helpers'
57
57
  import { formatDateCell } from '../dynamic-columns'
58
58
  import type { ActionFieldDef, RelationMeta } from '../types'
59
59
  import { ImageUrlContext, identityImageUrl, type GetImageUrl } from '../image-url-context'
60
+ import { TimeZoneContext, CurrencyContext } from '../org-runtime-context'
60
61
 
61
62
  // Re-export the resolver type so `index.ts`'s
62
63
  // `export type { … GetImageUrl } from './dialogs/dynamic-record'` keeps working.
@@ -107,6 +108,19 @@ export interface FieldDef {
107
108
  * its SQL type. Unknown values fall through to the `type`-based default.
108
109
  */
109
110
  widget?: string
111
+ /**
112
+ * Declarative display hint the backend stamps on the column/modal field
113
+ * (mirrors the table column's `cellStyle`). `'currency'` makes the view
114
+ * renderer format the numeric value in the org currency. Optional —
115
+ * absent, a money-key heuristic still detects obvious money fields.
116
+ */
117
+ cellStyle?: string
118
+ /**
119
+ * Per-field style overrides served alongside `cellStyle` (e.g.
120
+ * `{ currency: 'MXN' }`). When it carries an explicit `currency` it wins
121
+ * over the org fallback.
122
+ */
123
+ styleConfig?: Record<string, any>
110
124
  }
111
125
 
112
126
  // Permissive shape: the wire payload may omit some fields (e.g. `title` is
@@ -208,6 +222,12 @@ export interface DynamicRecordDialogProps {
208
222
  * regardless of the viewer's browser timezone. Pure `date` values pin to UTC.
209
223
  */
210
224
  timeZone?: string
225
+ /**
226
+ * Org ISO-4217 currency code (e.g. `MXN`) used as the fallback for money
227
+ * fields (`cellStyle:'currency'` or the money-key heuristic) that lack an
228
+ * explicit per-field currency. Optional — defaults to 'USD'.
229
+ */
230
+ currency?: string
211
231
  }
212
232
 
213
233
  function resolvePath(obj: any, path: string): any {
@@ -331,7 +351,27 @@ const MODE_CONFIG = {
331
351
  // Context threading host runtime values to nested field components (uploads,
332
352
  // image leads, tz-aware dates) without prop-drilling through every renderer.
333
353
  const ModelContext = createContext('')
334
- const TimeZoneContext = createContext<string | undefined>(undefined)
354
+
355
+ // Money-key heuristic mirroring the backend's `inferDisplayCellStyle`: lets the
356
+ // dialog format obvious money fields as currency even when the backend hasn't
357
+ // stamped `cellStyle:'currency'` yet. Case-insensitive; matches a key that
358
+ // equals one of these, or ends with `_<m>`, or starts with `<m>_`.
359
+ const MONEY_KEY_HEURISTIC = ['price', 'amount', 'total', 'cost', 'subtotal', 'balance', 'paid']
360
+
361
+ // isMoneyField decides whether a field should render as currency. The explicit
362
+ // `cellStyle:'currency'` stamp always wins; otherwise a numeric value whose key
363
+ // matches the money heuristic qualifies (robustness fallback).
364
+ export function isMoneyField(field: FieldDef, value: any): boolean {
365
+ if (field.cellStyle === 'currency') return true
366
+ if (value === null || value === undefined || value === '') return false
367
+ const num = typeof value === 'number' ? value : Number(value)
368
+ if (isNaN(num)) return false
369
+ const key = String(field.key || '').toLowerCase()
370
+ if (!key) return false
371
+ return MONEY_KEY_HEURISTIC.some(
372
+ m => key === m || key.endsWith(`_${m}`) || key.startsWith(`${m}_`),
373
+ )
374
+ }
335
375
 
336
376
  export function DynamicRecordDialog({
337
377
  open,
@@ -351,6 +391,7 @@ export function DynamicRecordDialog({
351
391
  initialRecord,
352
392
  getImageUrl = identityImageUrl,
353
393
  timeZone,
394
+ currency,
354
395
  }: DynamicRecordDialogProps) {
355
396
  const api = useApi()
356
397
  const { t } = useTranslation()
@@ -603,6 +644,7 @@ export function DynamicRecordDialog({
603
644
  <ModelContext.Provider value={model}>
604
645
  <ImageUrlContext.Provider value={getImageUrl}>
605
646
  <TimeZoneContext.Provider value={timeZone}>
647
+ <CurrencyContext.Provider value={currency}>
606
648
  <form
607
649
  id="dynamic-record-form"
608
650
  onSubmit={handleSubmit}
@@ -656,6 +698,7 @@ export function DynamicRecordDialog({
656
698
  />
657
699
  </div>
658
700
  )}
701
+ </CurrencyContext.Provider>
659
702
  </TimeZoneContext.Provider>
660
703
  </ImageUrlContext.Provider>
661
704
  </ModelContext.Provider>
@@ -810,6 +853,7 @@ export function ViewValue({
810
853
  record,
811
854
  getImageUrl: getImageUrlProp,
812
855
  timeZone: timeZoneProp,
856
+ currency: currencyProp,
813
857
  }: {
814
858
  field: FieldDef
815
859
  value: any
@@ -818,11 +862,16 @@ export function ViewValue({
818
862
  getImageUrl?: GetImageUrl
819
863
  /** Optional override; when omitted falls back to the nearest provider. */
820
864
  timeZone?: string
865
+ /** Optional override; when omitted falls back to the nearest provider. */
866
+ currency?: string
821
867
  }) {
868
+ const { i18n } = useTranslation()
822
869
  const ctxImageUrl = useContext(ImageUrlContext)
823
870
  const ctxTimeZone = useContext(TimeZoneContext)
871
+ const ctxCurrency = useContext(CurrencyContext)
824
872
  const getImageUrl = getImageUrlProp ?? ctxImageUrl
825
873
  const timeZone = timeZoneProp ?? ctxTimeZone
874
+ const currency = currencyProp ?? ctxCurrency
826
875
 
827
876
  // created_by / avatar resolver sibling → name (+ avatar) instead of "—".
828
877
  if (field.type === 'avatar' || field.key === 'created_by' || field.key === 'created_by_id') {
@@ -904,6 +953,24 @@ export function ViewValue({
904
953
  )
905
954
  }
906
955
 
956
+ // Money → org-currency string. Detected by the backend `cellStyle:'currency'`
957
+ // stamp or a numeric value whose key matches the money heuristic (fallback
958
+ // mirroring the table cell + backend `inferDisplayCellStyle`).
959
+ if (isMoneyField(field, value)) {
960
+ const num = typeof value === 'number' ? value : Number(value)
961
+ if (!isNaN(num)) {
962
+ const resolvedCurrency = field.styleConfig?.currency || currency || 'USD'
963
+ const localeTag = i18n.language || 'es'
964
+ const formatted = new Intl.NumberFormat(localeTag, {
965
+ style: 'currency',
966
+ currency: resolvedCurrency,
967
+ minimumFractionDigits: 2,
968
+ maximumFractionDigits: 2,
969
+ }).format(num)
970
+ return <p className="text-sm py-1 tabular-nums">{formatted}</p>
971
+ }
972
+ }
973
+
907
974
  // Date/datetime/timestamp → tz-aware format. `date` pins to UTC (calendar
908
975
  // day); instants render in the org timezone with a full-precision tooltip.
909
976
  if (field.type === 'date' || field.type === 'datetime' || field.type === 'timestamp') {
@@ -31,6 +31,7 @@ export type GetDynamicColumns = (
31
31
  language: string,
32
32
  columnFilterConfigs: Map<string, ColumnFilterConfig>,
33
33
  timeZone?: string,
34
+ currency?: string,
34
35
  ) => ColumnDef<any>[]
35
36
 
36
37
  /** Signature for the host-provided `DynamicIcon` renderer. */
@@ -93,9 +93,13 @@ const styleCfg = (
93
93
 
94
94
  const EmptyCell = () => <span className="text-muted-foreground">-</span>
95
95
 
96
- /** Resolves the active org currency, defaulting to USD when no override. */
97
- const resolveCurrency = (col: ColumnDefinition): string =>
98
- styleCfg(col, 'currency') || 'USD'
96
+ /**
97
+ * Resolves the active currency for a column: the column's explicit currency
98
+ * style wins, then the org-level fallback (org config, like `timeZone`), then
99
+ * 'USD' as a last resort.
100
+ */
101
+ export const resolveCurrency = (col: ColumnDefinition, orgCurrency?: string): string =>
102
+ styleCfg(col, 'currency') || orgCurrency || 'USD'
99
103
 
100
104
  const formatNumber = (
101
105
  value: number,
@@ -559,6 +563,7 @@ export function makeDefaultGetDynamicColumns(
559
563
  currentLanguage?: string,
560
564
  filterConfigs?: Map<string, ColumnFilterConfig>,
561
565
  timeZone?: string,
566
+ currency?: string,
562
567
  ): ColumnDef<any>[] {
563
568
  const dateLocale = currentLanguage === 'en' ? enUS : es
564
569
  const columns: ColumnDef<any>[] = [
@@ -842,7 +847,7 @@ export function makeDefaultGetDynamicColumns(
842
847
  num,
843
848
  {
844
849
  style: 'currency',
845
- currency: resolveCurrency(col),
850
+ currency: resolveCurrency(col, currency),
846
851
  minimumFractionDigits: decimals,
847
852
  maximumFractionDigits: decimals,
848
853
  },
@@ -4,6 +4,18 @@
4
4
  // - "many_to_many": multi-select sobre la tabla destino con sync a la pivot.
5
5
  // La RFC completa vive en `packages/runtime-react/docs/relations.md`.
6
6
  import { useCallback, useEffect, useMemo, useState } from 'react'
7
+ import { useTranslation } from 'react-i18next'
8
+ import {
9
+ type ColumnDef,
10
+ type Row,
11
+ type Cell,
12
+ type HeaderGroup,
13
+ type Header,
14
+ flexRender,
15
+ getCoreRowModel,
16
+ useReactTable,
17
+ } from '@tanstack/react-table'
18
+ import { cn } from '@asteby/metacore-ui/lib'
7
19
  import {
8
20
  Button,
9
21
  Skeleton,
@@ -20,13 +32,20 @@ import {
20
32
  DialogHeader,
21
33
  DialogTitle,
22
34
  MultiSelect,
35
+ Table,
36
+ TableBody,
37
+ TableCell,
38
+ TableHead,
39
+ TableHeader,
40
+ TableRow,
23
41
  } from '@asteby/metacore-ui/primitives'
24
42
  import { Plus, Trash2, Pencil } from 'lucide-react'
25
43
  import { useApi } from './api-context'
26
44
  import { useMetadataCache } from './metadata-cache'
27
45
  import { DynamicForm } from './dynamic-form'
28
46
  import { useImageUrl } from './image-url-context'
29
- import { OptionThumb } from './dynamic-select-field'
47
+ import { useTimeZone, useCurrency } from './org-runtime-context'
48
+ import { makeDefaultGetDynamicColumns } from './dynamic-columns'
30
49
  import { useOptionsResolver } from './use-options-resolver'
31
50
  import type { ApiResponse, TableMetadata } from './types'
32
51
  import {
@@ -37,7 +56,6 @@ import {
37
56
  deriveRelationFormFields,
38
57
  diffSelection,
39
58
  extractSelectedTargetIds,
40
- formatRelationCell,
41
59
  pickOptionLabel,
42
60
  relationRowKey,
43
61
  type DynamicRelationKind,
@@ -172,6 +190,9 @@ function OneToManyRelation({
172
190
  }: DynamicRelationOneToManyProps) {
173
191
  const api = useApi()
174
192
  const getImageUrl = useImageUrl()
193
+ const timeZone = useTimeZone()
194
+ const currency = useCurrency()
195
+ const { i18n } = useTranslation()
175
196
  const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache()
176
197
  const cachedMeta = getMetadata(model)
177
198
  const labels = { ...DEFAULT_STRINGS, ...(strings || {}) }
@@ -228,6 +249,81 @@ function OneToManyRelation({
228
249
  return metadata.columns.filter(c => !hidden.has(c.key) && !c.hidden)
229
250
  }, [metadata, foreignKey, filtersKey, hiddenColumns])
230
251
 
252
+ // Reuse the EXACT column factory the main `<DynamicTable>` uses so each cell
253
+ // renders identically — money in the org currency right-aligned, FK chips
254
+ // with thumbnails, dates in the org timezone, status/option badges, creator
255
+ // names — instead of a hand-rolled parallel formatting stack. Stable per
256
+ // image-url resolver.
257
+ const buildColumns = useMemo(
258
+ () => makeDefaultGetDynamicColumns({ getImageUrl }),
259
+ [getImageUrl],
260
+ )
261
+
262
+ const showActions = canEdit || canDelete
263
+
264
+ const columns = useMemo<ColumnDef<any>[]>(() => {
265
+ if (!metadata) return []
266
+ // Feed the factory a metadata view scoped to the visible columns only,
267
+ // with model-level actions stripped — the relation list owns its own
268
+ // inline edit/delete column (appended below), and the factory's
269
+ // select/actions columns don't belong in an embedded child list.
270
+ const scopedMeta = {
271
+ ...metadata,
272
+ columns: visibleColumns,
273
+ actions: [],
274
+ hasActions: false,
275
+ enableCRUDActions: false,
276
+ } as TableMetadata
277
+ const base = buildColumns(
278
+ scopedMeta,
279
+ () => {},
280
+ (key: string, opts?: any) => opts?.defaultValue ?? key,
281
+ i18n?.language || 'es',
282
+ new Map(),
283
+ timeZone,
284
+ currency,
285
+ ).filter((c) => c.id !== 'select' && c.id !== 'actions')
286
+
287
+ if (!showActions) return base
288
+
289
+ const actionsCol: ColumnDef<any> = {
290
+ id: 'actions',
291
+ header: () => <span className="sr-only">{labels.editLabel}</span>,
292
+ size: 80,
293
+ cell: ({ row }: { row: Row<any> }) => (
294
+ <div className="flex items-center justify-end gap-1">
295
+ {canEdit && (
296
+ <Button
297
+ size="sm"
298
+ variant="ghost"
299
+ onClick={() => { setEditingRow(row.original); setFormOpen(true) }}
300
+ aria-label={labels.editLabel}
301
+ >
302
+ <Pencil className="h-4 w-4" />
303
+ </Button>
304
+ )}
305
+ {canDelete && (
306
+ <Button
307
+ size="sm"
308
+ variant="ghost"
309
+ onClick={() => setRowToDelete(row.original)}
310
+ aria-label={labels.removeLabel}
311
+ >
312
+ <Trash2 className="h-4 w-4" />
313
+ </Button>
314
+ )}
315
+ </div>
316
+ ),
317
+ }
318
+ return [...base, actionsCol]
319
+ }, [metadata, visibleColumns, buildColumns, i18n?.language, timeZone, currency, showActions, canEdit, canDelete, labels.editLabel, labels.removeLabel])
320
+
321
+ const table = useReactTable({
322
+ data: rows,
323
+ columns,
324
+ getCoreRowModel: getCoreRowModel(),
325
+ })
326
+
231
327
  const handleSubmit = useCallback(async (values: Record<string, any>) => {
232
328
  setSubmitting(true)
233
329
  try {
@@ -299,62 +395,46 @@ function OneToManyRelation({
299
395
  {labels.emptyState}
300
396
  </div>
301
397
  ) : (
302
- <div className="border rounded-md divide-y bg-card">
303
- {rows.map((row, idx) => (
304
- <div
305
- key={relationRowKey(row, idx, foreignKey)}
306
- className="flex items-center justify-between gap-3 px-3 py-2"
307
- >
308
- <div className="flex-1 grid grid-cols-[repeat(auto-fit,minmax(0,1fr))] gap-2 text-sm">
309
- {visibleColumns.map(col => {
310
- const cell = formatRelationCell(row, col)
311
- // FK column whose backend-resolved sibling
312
- // carries an image → render a thumbnail + label
313
- // instead of plain text (e.g. a line item's
314
- // product photo). The sibling is the column key
315
- // with the trailing `_id` stripped.
316
- const isFk = !!col.ref || col.key.endsWith('_id')
317
- const sibling = isFk ? (row as any)[col.key.replace(/_id$/, '')] : undefined
318
- if (sibling && typeof sibling === 'object' && sibling.image) {
319
- const label = sibling.label ?? sibling.name ?? cell
398
+ // Real metadata-driven table — same metacore-ui primitives and
399
+ // cell renderers as `<DynamicTable>` so headers, money/currency,
400
+ // FK thumbnails, dates and badges all match the main table.
401
+ <div className="overflow-x-auto border rounded-md bg-card">
402
+ <Table noWrapper className="w-full">
403
+ <TableHeader>
404
+ {table.getHeaderGroups().map((headerGroup: HeaderGroup<any>) => (
405
+ <TableRow key={headerGroup.id} className="border-b-0 hover:bg-transparent">
406
+ {headerGroup.headers.map((header: Header<any, unknown>) => {
407
+ const isActions = header.id === 'actions'
320
408
  return (
321
- <span key={col.key} className="flex min-w-0 items-center gap-2" title={String(label)}>
322
- <OptionThumb image={getImageUrl(sibling.image)} size={20} />
323
- <span className="truncate">{label}</span>
324
- </span>
409
+ <TableHead
410
+ key={header.id}
411
+ colSpan={header.colSpan}
412
+ style={header.column.columnDef.size ? { width: header.column.columnDef.size } : undefined}
413
+ className={cn('bg-card border-b h-10', isActions && 'text-right')}
414
+ >
415
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
416
+ </TableHead>
325
417
  )
326
- }
327
- return (
328
- <span key={col.key} className="truncate" title={cell}>
329
- {cell}
330
- </span>
331
- )
332
- })}
333
- </div>
334
- <div className="flex items-center gap-1 shrink-0">
335
- {canEdit && (
336
- <Button
337
- size="sm"
338
- variant="ghost"
339
- onClick={() => { setEditingRow(row); setFormOpen(true) }}
340
- aria-label={labels.editLabel}
341
- >
342
- <Pencil className="h-4 w-4" />
343
- </Button>
344
- )}
345
- {canDelete && (
346
- <Button
347
- size="sm"
348
- variant="ghost"
349
- onClick={() => setRowToDelete(row)}
350
- aria-label={labels.removeLabel}
351
- >
352
- <Trash2 className="h-4 w-4" />
353
- </Button>
354
- )}
355
- </div>
356
- </div>
357
- ))}
418
+ })}
419
+ </TableRow>
420
+ ))}
421
+ </TableHeader>
422
+ <TableBody>
423
+ {table.getRowModel().rows.map((row: Row<any>, idx: number) => (
424
+ <TableRow key={relationRowKey(row.original, idx, foreignKey)}>
425
+ {row.getVisibleCells().map((cell: Cell<any, unknown>) => (
426
+ <TableCell
427
+ key={cell.id}
428
+ style={cell.column.columnDef.size ? { width: cell.column.columnDef.size } : undefined}
429
+ className="py-2"
430
+ >
431
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
432
+ </TableCell>
433
+ ))}
434
+ </TableRow>
435
+ ))}
436
+ </TableBody>
437
+ </Table>
358
438
  </div>
359
439
  )}
360
440
 
@@ -97,6 +97,12 @@ interface DynamicTableProps {
97
97
  * Optional — omitting it preserves the legacy browser-local formatting.
98
98
  */
99
99
  timeZone?: string
100
+ /**
101
+ * ISO 4217 currency code (e.g. the org's `MXN`) used as the fallback for
102
+ * money cells (`type:'number'` + `cellStyle:'currency'`) that don't carry
103
+ * an explicit per-column currency. Optional — defaults to 'USD'.
104
+ */
105
+ currency?: string
100
106
  }
101
107
 
102
108
  export function DynamicTable({
@@ -110,6 +116,7 @@ export function DynamicTable({
110
116
  extraColumns = [],
111
117
  getDynamicColumns = defaultGetDynamicColumns,
112
118
  timeZone,
119
+ currency,
113
120
  }: DynamicTableProps) {
114
121
  const { t, i18n } = useTranslation()
115
122
  const api = useApi()
@@ -598,12 +605,12 @@ export function DynamicTable({
598
605
  const rowMetadata = metadata.actions?.some((a) => a.placement === 'table' || a.placement === 'create')
599
606
  ? { ...metadata, actions: metadata.actions.filter((a) => !a.placement || a.placement === 'row') }
600
607
  : metadata
601
- const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone)
608
+ const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone, currency)
602
609
  const filteredBase = baseColumns.filter((col: ColumnDef<any>) => !hiddenColumns.includes(col.id as string))
603
610
  const actionsCol = filteredBase.find((c: ColumnDef<any>) => c.id === 'actions')
604
611
  const otherCols = filteredBase.filter((c: ColumnDef<any>) => c.id !== 'actions')
605
612
  return [...otherCols, ...extraColumns, ...(actionsCol ? [actionsCol] : [])]
606
- }, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone])
613
+ }, [metadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone, currency])
607
614
 
608
615
  const filters = useMemo(() => [], [])
609
616
 
@@ -0,0 +1,28 @@
1
+ // Org runtime contexts — thread the org's display config (timezone, currency)
2
+ // to nested field/cell/relation renderers without prop-drilling. Lives in its
3
+ // own module (mirroring `image-url-context`) so any renderer can consume them
4
+ // without importing from `dialogs/dynamic-record`. That dialog imports
5
+ // `dynamic-relations` → `dynamic-relation`, so the relation table cannot import
6
+ // these contexts back from the dialog without a circular import — hence this
7
+ // standalone module is the single source of truth.
8
+ import { createContext, useContext } from 'react'
9
+
10
+ /**
11
+ * IANA timezone (e.g. the org's `America/Mexico_City`) used to render
12
+ * datetime/timestamp cells. `undefined` outside a provider → renderers fall
13
+ * back to the viewer's browser zone (legacy behaviour).
14
+ */
15
+ export const TimeZoneContext = createContext<string | undefined>(undefined)
16
+
17
+ /** Reads the nearest org timezone (undefined outside a provider). */
18
+ export const useTimeZone = () => useContext(TimeZoneContext)
19
+
20
+ /**
21
+ * Org ISO-4217 currency (org config, like the timezone) used as the fallback
22
+ * for money cells/fields that don't carry an explicit per-column currency.
23
+ * `undefined` outside a provider → renderers fall back to 'USD'.
24
+ */
25
+ export const CurrencyContext = createContext<string | undefined>(undefined)
26
+
27
+ /** Reads the nearest org currency (undefined outside a provider). */
28
+ export const useCurrency = () => useContext(CurrencyContext)