@asteby/metacore-runtime-react 18.19.0 → 18.20.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 +11 -0
- package/dist/collection-cell.d.ts +21 -4
- package/dist/collection-cell.d.ts.map +1 -1
- package/dist/collection-cell.js +109 -14
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/collection-cell.test.tsx +80 -17
- package/src/collection-cell.tsx +150 -14
- package/src/dynamic-columns.tsx +7 -1
- package/src/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6ec7baf: CollectionCell is now locale-aware: jsonb/array popover headers and the item
|
|
8
|
+
count noun render in the org's language. Resolution per key: host `t(rawKey)`
|
|
9
|
+
override → built-in es/en dictionary of common data/commerce keys (product_id,
|
|
10
|
+
quantity, price, total, name, sku, …) → snake→Title prettify fallback. Count
|
|
11
|
+
noun localizes (es: ítem/ítems). Locale + translator are threaded from the
|
|
12
|
+
dynamic columns factory; defaults to English when absent.
|
|
13
|
+
|
|
3
14
|
## 18.19.0
|
|
4
15
|
|
|
5
16
|
### Minor Changes
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
2
|
+
/** Host i18n translator (react-i18next `t`), as threaded into the columns factory. */
|
|
3
|
+
export type Translate = (key: string, options?: any) => string;
|
|
4
|
+
/**
|
|
5
|
+
* Localized key label for a popover column header. Resolution order:
|
|
6
|
+
* (a) host i18n `t(rawKey)` if it resolves to something ≠ rawKey;
|
|
7
|
+
* (b) the built-in generic es/en data/commerce dictionary;
|
|
8
|
+
* (c) snake_case → Title Case prettify (`product_id` → "Product ID").
|
|
9
|
+
* `locale` defaults to 'en' when absent.
|
|
10
|
+
*/
|
|
11
|
+
export declare function prettifyKey(key: string, locale?: string, t?: Translate): string;
|
|
12
|
+
/**
|
|
13
|
+
* Localized, pluralized count noun. Prefers the host i18n `t` (react-i18next
|
|
14
|
+
* count plural via `defaultValue`); otherwise the built-in es/en noun map.
|
|
15
|
+
*/
|
|
16
|
+
export declare function countLabel(count: number, locale?: string, t?: Translate): string;
|
|
4
17
|
/**
|
|
5
18
|
* Render a single scalar (or near-scalar) value for compact display.
|
|
6
19
|
* - uuid-like or very long (32+ char) strings → first 8 chars + "…"
|
|
@@ -13,10 +26,14 @@ export interface CollectionCellProps {
|
|
|
13
26
|
value: unknown;
|
|
14
27
|
/** Max items previewed inline for scalar arrays. */
|
|
15
28
|
maxInline?: number;
|
|
29
|
+
/** Org/UI language tag (e.g. `es`, `en-US`). Defaults to `'en'`. */
|
|
30
|
+
locale?: string;
|
|
31
|
+
/** Host i18n translator; takes precedence over the built-in dictionary. */
|
|
32
|
+
t?: Translate;
|
|
16
33
|
}
|
|
17
34
|
/**
|
|
18
35
|
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
19
|
-
* compact, dark-mode friendly. Never throws on unexpected shapes.
|
|
36
|
+
* compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
|
|
20
37
|
*/
|
|
21
|
-
export declare function CollectionCell({ value, maxInline }: CollectionCellProps): React.JSX.Element;
|
|
38
|
+
export declare function CollectionCell({ value, maxInline, locale, t, }: CollectionCellProps): React.JSX.Element;
|
|
22
39
|
//# sourceMappingURL=collection-cell.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-cell.d.ts","sourceRoot":"","sources":["../src/collection-cell.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoB9B,
|
|
1
|
+
{"version":3,"file":"collection-cell.d.ts","sourceRoot":"","sources":["../src/collection-cell.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoB9B,sFAAsF;AACtF,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,CAAA;AAiE9D;;;;;;GAMG;AACH,wBAAgB,WAAW,CACvB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,EACf,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,CAWR;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACtB,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,EACf,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,CAcR;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAYnD;AAyJD,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,OAAO,CAAA;IACd,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2EAA2E;IAC3E,CAAC,CAAC,EAAE,SAAS,CAAA;CAChB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAa,EACb,MAAM,EACN,CAAC,GACJ,EAAE,mBAAmB,qBAgFrB"}
|
package/dist/collection-cell.js
CHANGED
|
@@ -3,11 +3,106 @@ import { List } from 'lucide-react';
|
|
|
3
3
|
import { Badge, Popover, PopoverContent, PopoverTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, } from '@asteby/metacore-ui';
|
|
4
4
|
import { humanizeToken } from './dynamic-columns-helpers';
|
|
5
5
|
const UUID_LIKE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/** Normalize an org/UI language tag to a base language code (`es-MX` → `es`). */
|
|
7
|
+
function baseLang(locale) {
|
|
8
|
+
return (locale || 'en').toLowerCase().split('-')[0];
|
|
9
|
+
}
|
|
10
|
+
// Built-in generic data/commerce vocabulary. Localizes the common jsonb keys
|
|
11
|
+
// (line-item rows, config blobs) to the org language out of the box. This is
|
|
12
|
+
// intentionally GENERIC — no domain-specific narrative — and a host i18n bundle
|
|
13
|
+
// can override any key via the `t` resolver (which takes precedence). Keys not
|
|
14
|
+
// found here fall back to snake→Title prettify, so unknown shapes still read.
|
|
15
|
+
const KEY_DICTIONARY = {
|
|
16
|
+
es: {
|
|
17
|
+
product_id: 'Producto',
|
|
18
|
+
product: 'Producto',
|
|
19
|
+
quantity: 'Cantidad',
|
|
20
|
+
qty: 'Cantidad',
|
|
21
|
+
unit_cost: 'Costo unitario',
|
|
22
|
+
cost: 'Costo',
|
|
23
|
+
price: 'Precio',
|
|
24
|
+
total: 'Total',
|
|
25
|
+
subtotal: 'Subtotal',
|
|
26
|
+
amount: 'Importe',
|
|
27
|
+
name: 'Nombre',
|
|
28
|
+
sku: 'SKU',
|
|
29
|
+
code: 'Código',
|
|
30
|
+
date: 'Fecha',
|
|
31
|
+
notes: 'Notas',
|
|
32
|
+
reason: 'Motivo',
|
|
33
|
+
delta: 'Variación',
|
|
34
|
+
warehouse: 'Almacén',
|
|
35
|
+
description: 'Descripción',
|
|
36
|
+
id: 'ID',
|
|
37
|
+
},
|
|
38
|
+
en: {
|
|
39
|
+
product_id: 'Product',
|
|
40
|
+
product: 'Product',
|
|
41
|
+
quantity: 'Quantity',
|
|
42
|
+
qty: 'Quantity',
|
|
43
|
+
unit_cost: 'Unit Cost',
|
|
44
|
+
cost: 'Cost',
|
|
45
|
+
price: 'Price',
|
|
46
|
+
total: 'Total',
|
|
47
|
+
subtotal: 'Subtotal',
|
|
48
|
+
amount: 'Amount',
|
|
49
|
+
name: 'Name',
|
|
50
|
+
sku: 'SKU',
|
|
51
|
+
code: 'Code',
|
|
52
|
+
date: 'Date',
|
|
53
|
+
notes: 'Notes',
|
|
54
|
+
reason: 'Reason',
|
|
55
|
+
delta: 'Delta',
|
|
56
|
+
warehouse: 'Warehouse',
|
|
57
|
+
description: 'Description',
|
|
58
|
+
id: 'ID',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
/** Localized count noun for the array-of-objects badge (`1 ítem` / `2 ítems`). */
|
|
62
|
+
const ITEM_NOUN = {
|
|
63
|
+
es: { one: 'ítem', other: 'ítems' },
|
|
64
|
+
en: { one: 'item', other: 'items' },
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Localized key label for a popover column header. Resolution order:
|
|
68
|
+
* (a) host i18n `t(rawKey)` if it resolves to something ≠ rawKey;
|
|
69
|
+
* (b) the built-in generic es/en data/commerce dictionary;
|
|
70
|
+
* (c) snake_case → Title Case prettify (`product_id` → "Product ID").
|
|
71
|
+
* `locale` defaults to 'en' when absent.
|
|
72
|
+
*/
|
|
73
|
+
export function prettifyKey(key, locale, t) {
|
|
74
|
+
if (t) {
|
|
75
|
+
const translated = t(key);
|
|
76
|
+
if (translated && translated !== key)
|
|
77
|
+
return translated;
|
|
78
|
+
}
|
|
79
|
+
const lang = baseLang(locale);
|
|
80
|
+
const dict = KEY_DICTIONARY[lang] ?? KEY_DICTIONARY.en;
|
|
81
|
+
const hit = dict[key.toLowerCase()];
|
|
82
|
+
if (hit)
|
|
83
|
+
return hit;
|
|
8
84
|
const pretty = humanizeToken(key);
|
|
9
85
|
return pretty || key;
|
|
10
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Localized, pluralized count noun. Prefers the host i18n `t` (react-i18next
|
|
89
|
+
* count plural via `defaultValue`); otherwise the built-in es/en noun map.
|
|
90
|
+
*/
|
|
91
|
+
export function countLabel(count, locale, t) {
|
|
92
|
+
const lang = baseLang(locale);
|
|
93
|
+
const noun = ITEM_NOUN[lang] ?? ITEM_NOUN.en;
|
|
94
|
+
const fallback = `${count} ${count === 1 ? noun.one : noun.other}`;
|
|
95
|
+
if (t) {
|
|
96
|
+
const translated = t('runtime.collectionCell.itemCount', {
|
|
97
|
+
count,
|
|
98
|
+
defaultValue: fallback,
|
|
99
|
+
});
|
|
100
|
+
if (translated && translated !== 'runtime.collectionCell.itemCount') {
|
|
101
|
+
return translated;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return fallback;
|
|
105
|
+
}
|
|
11
106
|
/**
|
|
12
107
|
* Render a single scalar (or near-scalar) value for compact display.
|
|
13
108
|
* - uuid-like or very long (32+ char) strings → first 8 chars + "…"
|
|
@@ -69,18 +164,18 @@ function unionKeys(rows) {
|
|
|
69
164
|
return seen;
|
|
70
165
|
}
|
|
71
166
|
const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0';
|
|
72
|
-
function MiniTable({ rows }) {
|
|
167
|
+
function MiniTable({ rows, locale, t, }) {
|
|
73
168
|
const keys = unionKeys(rows);
|
|
74
169
|
if (keys.length === 0) {
|
|
75
170
|
return _jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "-" });
|
|
76
171
|
}
|
|
77
|
-
return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: keys.map((key) => (_jsx(TableHead, { className: "text-xs whitespace-nowrap", children: prettifyKey(key) }, key))) }) }), _jsx(TableBody, { children: rows.map((row, i) => (_jsx(TableRow, { children: keys.map((key) => (_jsx(TableCell, { className: "text-xs whitespace-nowrap", children: formatScalar(row[key]) }, key))) }, i))) })] }));
|
|
172
|
+
return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: keys.map((key) => (_jsx(TableHead, { className: "text-xs whitespace-nowrap", children: prettifyKey(key, locale, t) }, key))) }) }), _jsx(TableBody, { children: rows.map((row, i) => (_jsx(TableRow, { children: keys.map((key) => (_jsx(TableCell, { className: "text-xs whitespace-nowrap", children: formatScalar(row[key]) }, key))) }, i))) })] }));
|
|
78
173
|
}
|
|
79
174
|
function ScalarList({ values }) {
|
|
80
175
|
return (_jsx("ul", { className: "p-3 space-y-1", children: values.map((v, i) => (_jsx("li", { className: "text-xs text-foreground", children: formatScalar(v) }, i))) }));
|
|
81
176
|
}
|
|
82
|
-
function PairList({ entries }) {
|
|
83
|
-
return (_jsx("ul", { className: "p-3 space-y-1", children: entries.map(([key, v]) => (_jsxs("li", { className: "text-xs", children: [_jsxs("span", { className: "text-muted-foreground", children: [prettifyKey(key), ":"] }), ' ', _jsx("span", { className: "text-foreground", children: formatScalar(v) })] }, key))) }));
|
|
177
|
+
function PairList({ entries, locale, t, }) {
|
|
178
|
+
return (_jsx("ul", { className: "p-3 space-y-1", children: entries.map(([key, v]) => (_jsxs("li", { className: "text-xs", children: [_jsxs("span", { className: "text-muted-foreground", children: [prettifyKey(key, locale, t), ":"] }), ' ', _jsx("span", { className: "text-foreground", children: formatScalar(v) })] }, key))) }));
|
|
84
179
|
}
|
|
85
180
|
/** Compact badge trigger that opens a popover panel. */
|
|
86
181
|
function PopoverShell({ label, title, children, icon = true, }) {
|
|
@@ -88,9 +183,9 @@ function PopoverShell({ label, title, children, icon = true, }) {
|
|
|
88
183
|
}
|
|
89
184
|
/**
|
|
90
185
|
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
91
|
-
* compact, dark-mode friendly. Never throws on unexpected shapes.
|
|
186
|
+
* compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
|
|
92
187
|
*/
|
|
93
|
-
export function CollectionCell({ value, maxInline = 3 }) {
|
|
188
|
+
export function CollectionCell({ value, maxInline = 3, locale, t, }) {
|
|
94
189
|
const parsed = parseValue(value);
|
|
95
190
|
// Empty-ish → muted dash.
|
|
96
191
|
if (parsed === null ||
|
|
@@ -111,13 +206,13 @@ export function CollectionCell({ value, maxInline = 3 }) {
|
|
|
111
206
|
if (allObjects) {
|
|
112
207
|
const rows = parsed;
|
|
113
208
|
const count = rows.length;
|
|
114
|
-
const label = count
|
|
209
|
+
const label = countLabel(count, locale, t);
|
|
115
210
|
const title = rows
|
|
116
211
|
.map((row) => Object.entries(row)
|
|
117
|
-
.map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
|
|
212
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
118
213
|
.join(', '))
|
|
119
214
|
.join(' | ');
|
|
120
|
-
return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows }) }));
|
|
215
|
+
return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows, locale: locale, t: t }) }));
|
|
121
216
|
}
|
|
122
217
|
// Array of scalars (or mixed): preview first N joined, "+N" overflow.
|
|
123
218
|
const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ');
|
|
@@ -130,12 +225,12 @@ export function CollectionCell({ value, maxInline = 3 }) {
|
|
|
130
225
|
const entries = Object.entries(parsed);
|
|
131
226
|
const inline = entries
|
|
132
227
|
.slice(0, maxInline)
|
|
133
|
-
.map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
|
|
228
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
134
229
|
.join(', ');
|
|
135
230
|
const overflow = entries.length - maxInline;
|
|
136
231
|
const label = overflow > 0 ? `${inline} +${overflow}` : inline;
|
|
137
232
|
const title = entries
|
|
138
|
-
.map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
|
|
233
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
139
234
|
.join(', ');
|
|
140
|
-
return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(PairList, { entries: entries }) }));
|
|
235
|
+
return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(PairList, { entries: entries, locale: locale, t: t }) }));
|
|
141
236
|
}
|
|
@@ -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;AAiC9C,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;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;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;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,
|
|
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;AAiC9C,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;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;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;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA6nBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -736,7 +736,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
736
736
|
}
|
|
737
737
|
default: {
|
|
738
738
|
if (typeof value === 'object' && value !== null) {
|
|
739
|
-
return _jsx(CollectionCell, { value: value });
|
|
739
|
+
return (_jsx(CollectionCell, { value: value, locale: currentLanguage, t: t }));
|
|
740
740
|
}
|
|
741
741
|
if (col.key === 'description' ||
|
|
742
742
|
col.key === 'features' ||
|
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export * from './dynamic-icon';
|
|
|
22
22
|
export type { ColumnFilterConfig, FilterOption as DynamicColumnFilterOption, GetDynamicColumns, DynamicIconComponent, } from './dynamic-columns-shim';
|
|
23
23
|
export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, relationKeyFor, resolveRelationLabel, type DynamicColumnsHelpers, } from './dynamic-columns';
|
|
24
24
|
export { humanizeToken } from './dynamic-columns-helpers';
|
|
25
|
-
export { CollectionCell, formatScalar, prettifyKey, type CollectionCellProps, } from './collection-cell';
|
|
25
|
+
export { CollectionCell, formatScalar, prettifyKey, countLabel, type CollectionCellProps, type Translate as CollectionCellTranslate, } from './collection-cell';
|
|
26
26
|
export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
|
|
27
27
|
export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
|
|
28
28
|
export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,kBAAkB,EAClB,eAAe,EACf,KAAK,uBAAuB,EAC5B,KAAK,eAAe,GACvB,MAAM,wBAAwB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,WAAW,EAChB,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,OAAO,EACH,mBAAmB,EACnB,MAAM,EACN,oBAAoB,EACpB,OAAO,EACP,sBAAsB,EACtB,eAAe,EACf,iBAAiB,EACjB,KAAK,KAAK,EACV,KAAK,wBAAwB,GAChC,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,aAAa,EACb,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,OAAO,EACZ,KAAK,SAAS,GACjB,MAAM,uBAAuB,CAAA;AAC9B,cAAc,uBAAuB,CAAA;AACrC,cAAc,wBAAwB,CAAA;AACtC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,GACtC,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACH,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC9B,MAAM,yBAAyB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,cAAc,EACd,oBAAoB,EACpB,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,EACH,cAAc,EACd,YAAY,EACZ,WAAW,EACX,KAAK,mBAAmB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederation
|
|
|
26
26
|
export * from './dynamic-icon';
|
|
27
27
|
export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, relationKeyFor, resolveRelationLabel, } from './dynamic-columns';
|
|
28
28
|
export { humanizeToken } from './dynamic-columns-helpers';
|
|
29
|
-
export { CollectionCell, formatScalar, prettifyKey, } from './collection-cell';
|
|
29
|
+
export { CollectionCell, formatScalar, prettifyKey, countLabel, } from './collection-cell';
|
|
30
30
|
export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
|
|
31
31
|
export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
|
|
32
32
|
export { CreateRecordDialog } from './dialogs/create-record-dialog';
|
package/package.json
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// - plain object → inline key: value pairs
|
|
7
7
|
// - null / empty → "-"
|
|
8
8
|
// - JSON-string value is defensively parsed
|
|
9
|
+
// - locale-aware: count noun + header keys render in the org language; the
|
|
10
|
+
// host `t` overrides; unknown keys fall back to snake→Title prettify.
|
|
9
11
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
10
12
|
import { cleanup, render, screen } from '@testing-library/react'
|
|
11
13
|
|
|
@@ -14,6 +16,7 @@ afterEach(cleanup)
|
|
|
14
16
|
|
|
15
17
|
import {
|
|
16
18
|
CollectionCell,
|
|
19
|
+
countLabel,
|
|
17
20
|
formatScalar,
|
|
18
21
|
prettifyKey,
|
|
19
22
|
} from '../collection-cell'
|
|
@@ -41,14 +44,50 @@ describe('formatScalar', () => {
|
|
|
41
44
|
})
|
|
42
45
|
|
|
43
46
|
describe('prettifyKey', () => {
|
|
44
|
-
it('
|
|
45
|
-
expect(prettifyKey('product_id')).toBe('
|
|
46
|
-
expect(prettifyKey('quantity')).toBe('
|
|
47
|
+
it('localizes common data/commerce keys to Spanish', () => {
|
|
48
|
+
expect(prettifyKey('product_id', 'es')).toBe('Producto')
|
|
49
|
+
expect(prettifyKey('quantity', 'es')).toBe('Cantidad')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('localizes common keys to English (default locale)', () => {
|
|
53
|
+
expect(prettifyKey('product_id')).toBe('Product')
|
|
54
|
+
expect(prettifyKey('product_id', 'en')).toBe('Product')
|
|
55
|
+
expect(prettifyKey('quantity', 'en')).toBe('Quantity')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('accepts a regional tag and normalizes to base language', () => {
|
|
59
|
+
expect(prettifyKey('quantity', 'es-MX')).toBe('Cantidad')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('prefers a host `t` translation over the built-in dictionary', () => {
|
|
63
|
+
const t = (k: string) => (k === 'quantity' ? 'Piezas' : k)
|
|
64
|
+
expect(prettifyKey('quantity', 'es', t)).toBe('Piezas')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('falls back to snake→Title prettify for unknown keys', () => {
|
|
68
|
+
expect(prettifyKey('shelf_position', 'es')).toBe('Shelf Position')
|
|
69
|
+
expect(prettifyKey('warehouse_bin', 'en')).toBe('Warehouse Bin')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('countLabel', () => {
|
|
74
|
+
it('pluralizes the count noun per locale', () => {
|
|
75
|
+
expect(countLabel(1, 'es')).toBe('1 ítem')
|
|
76
|
+
expect(countLabel(2, 'es')).toBe('2 ítems')
|
|
77
|
+
expect(countLabel(1, 'en')).toBe('1 item')
|
|
78
|
+
expect(countLabel(3, 'en')).toBe('3 items')
|
|
79
|
+
expect(countLabel(1)).toBe('1 item') // default → en
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('prefers a host `t` count plural', () => {
|
|
83
|
+
const t = (_k: string, o?: any) =>
|
|
84
|
+
o?.count === 1 ? '1 renglón' : `${o?.count} renglones`
|
|
85
|
+
expect(countLabel(2, 'es', t)).toBe('2 renglones')
|
|
47
86
|
})
|
|
48
87
|
})
|
|
49
88
|
|
|
50
89
|
describe('CollectionCell', () => {
|
|
51
|
-
it('renders
|
|
90
|
+
it('renders an English count badge by default for an array of objects', () => {
|
|
52
91
|
render(
|
|
53
92
|
<CollectionCell
|
|
54
93
|
value={[
|
|
@@ -57,32 +96,55 @@ describe('CollectionCell', () => {
|
|
|
57
96
|
]}
|
|
58
97
|
/>
|
|
59
98
|
)
|
|
60
|
-
//
|
|
61
|
-
expect(screen.getByText('2
|
|
62
|
-
// The trigger's title carries the
|
|
63
|
-
const badge = screen.getByText('2
|
|
99
|
+
// Default locale → English plural.
|
|
100
|
+
expect(screen.getByText('2 items')).toBeTruthy()
|
|
101
|
+
// The trigger's title carries the localized rows for the no-JS fallback.
|
|
102
|
+
const badge = screen.getByText('2 items').closest('[title]')
|
|
64
103
|
expect(badge).toBeTruthy()
|
|
65
104
|
const title = badge!.getAttribute('title') ?? ''
|
|
66
105
|
expect(title).toContain('Quantity: 2')
|
|
67
106
|
expect(title).toContain('Quantity: 5')
|
|
68
|
-
expect(title).toContain('Product
|
|
107
|
+
expect(title).toContain('Product:') // product_id → "Product" (en)
|
|
69
108
|
})
|
|
70
109
|
|
|
71
|
-
it('renders
|
|
72
|
-
render(
|
|
110
|
+
it('renders Spanish count + headers when locale is es', () => {
|
|
111
|
+
render(
|
|
112
|
+
<CollectionCell
|
|
113
|
+
locale="es"
|
|
114
|
+
value={[
|
|
115
|
+
{ product_id: 'abc', quantity: 2 },
|
|
116
|
+
{ product_id: 'def', quantity: 5 },
|
|
117
|
+
]}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
expect(screen.getByText('2 ítems')).toBeTruthy()
|
|
121
|
+
const title =
|
|
122
|
+
screen.getByText('2 ítems').closest('[title]')!.getAttribute('title') ??
|
|
123
|
+
''
|
|
124
|
+
expect(title).toContain('Producto:')
|
|
125
|
+
expect(title).toContain('Cantidad: 2')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('renders the singular Spanish noun for a single-item array', () => {
|
|
129
|
+
render(<CollectionCell locale="es" value={[{ sku: 'A1' }]} />)
|
|
73
130
|
expect(screen.getByText('1 ítem')).toBeTruthy()
|
|
74
131
|
})
|
|
75
132
|
|
|
133
|
+
it('renders the singular English noun for a single-item array', () => {
|
|
134
|
+
render(<CollectionCell value={[{ sku: 'A1' }]} />)
|
|
135
|
+
expect(screen.getByText('1 item')).toBeTruthy()
|
|
136
|
+
})
|
|
137
|
+
|
|
76
138
|
it('previews the first scalars with overflow for a scalar array', () => {
|
|
77
139
|
render(<CollectionCell value={['a', 'b', 'c', 'd', 'e']} />)
|
|
78
140
|
expect(screen.getByText('a, b, c +2')).toBeTruthy()
|
|
79
141
|
})
|
|
80
142
|
|
|
81
|
-
it('renders inline key: value pairs for a plain object', () => {
|
|
82
|
-
render(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
).toBeTruthy()
|
|
143
|
+
it('renders inline key: value pairs (localized) for a plain object', () => {
|
|
144
|
+
render(
|
|
145
|
+
<CollectionCell locale="es" value={{ price: 10, quantity: 20 }} />
|
|
146
|
+
)
|
|
147
|
+
expect(screen.getByText(/Precio: 10, Cantidad: 20/)).toBeTruthy()
|
|
86
148
|
})
|
|
87
149
|
|
|
88
150
|
it('renders a dash for null / empty values', () => {
|
|
@@ -98,12 +160,13 @@ describe('CollectionCell', () => {
|
|
|
98
160
|
it('parses a JSON-string value into a collection', () => {
|
|
99
161
|
render(
|
|
100
162
|
<CollectionCell
|
|
163
|
+
locale="es"
|
|
101
164
|
value={'[{"product_id":"abc","quantity":1}]'}
|
|
102
165
|
/>
|
|
103
166
|
)
|
|
104
167
|
expect(screen.getByText('1 ítem')).toBeTruthy()
|
|
105
168
|
const badge = screen.getByText('1 ítem').closest('[title]')
|
|
106
|
-
expect(badge!.getAttribute('title')).toContain('
|
|
169
|
+
expect(badge!.getAttribute('title')).toContain('Cantidad: 1')
|
|
107
170
|
})
|
|
108
171
|
|
|
109
172
|
it('truncates an unparseable string instead of crashing', () => {
|
package/src/collection-cell.tsx
CHANGED
|
@@ -25,12 +25,120 @@ import { humanizeToken } from './dynamic-columns-helpers'
|
|
|
25
25
|
const UUID_LIKE_RE =
|
|
26
26
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
export
|
|
28
|
+
/** Host i18n translator (react-i18next `t`), as threaded into the columns factory. */
|
|
29
|
+
export type Translate = (key: string, options?: any) => string
|
|
30
|
+
|
|
31
|
+
/** Normalize an org/UI language tag to a base language code (`es-MX` → `es`). */
|
|
32
|
+
function baseLang(locale?: string): string {
|
|
33
|
+
return (locale || 'en').toLowerCase().split('-')[0]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Built-in generic data/commerce vocabulary. Localizes the common jsonb keys
|
|
37
|
+
// (line-item rows, config blobs) to the org language out of the box. This is
|
|
38
|
+
// intentionally GENERIC — no domain-specific narrative — and a host i18n bundle
|
|
39
|
+
// can override any key via the `t` resolver (which takes precedence). Keys not
|
|
40
|
+
// found here fall back to snake→Title prettify, so unknown shapes still read.
|
|
41
|
+
const KEY_DICTIONARY: Record<string, Record<string, string>> = {
|
|
42
|
+
es: {
|
|
43
|
+
product_id: 'Producto',
|
|
44
|
+
product: 'Producto',
|
|
45
|
+
quantity: 'Cantidad',
|
|
46
|
+
qty: 'Cantidad',
|
|
47
|
+
unit_cost: 'Costo unitario',
|
|
48
|
+
cost: 'Costo',
|
|
49
|
+
price: 'Precio',
|
|
50
|
+
total: 'Total',
|
|
51
|
+
subtotal: 'Subtotal',
|
|
52
|
+
amount: 'Importe',
|
|
53
|
+
name: 'Nombre',
|
|
54
|
+
sku: 'SKU',
|
|
55
|
+
code: 'Código',
|
|
56
|
+
date: 'Fecha',
|
|
57
|
+
notes: 'Notas',
|
|
58
|
+
reason: 'Motivo',
|
|
59
|
+
delta: 'Variación',
|
|
60
|
+
warehouse: 'Almacén',
|
|
61
|
+
description: 'Descripción',
|
|
62
|
+
id: 'ID',
|
|
63
|
+
},
|
|
64
|
+
en: {
|
|
65
|
+
product_id: 'Product',
|
|
66
|
+
product: 'Product',
|
|
67
|
+
quantity: 'Quantity',
|
|
68
|
+
qty: 'Quantity',
|
|
69
|
+
unit_cost: 'Unit Cost',
|
|
70
|
+
cost: 'Cost',
|
|
71
|
+
price: 'Price',
|
|
72
|
+
total: 'Total',
|
|
73
|
+
subtotal: 'Subtotal',
|
|
74
|
+
amount: 'Amount',
|
|
75
|
+
name: 'Name',
|
|
76
|
+
sku: 'SKU',
|
|
77
|
+
code: 'Code',
|
|
78
|
+
date: 'Date',
|
|
79
|
+
notes: 'Notes',
|
|
80
|
+
reason: 'Reason',
|
|
81
|
+
delta: 'Delta',
|
|
82
|
+
warehouse: 'Warehouse',
|
|
83
|
+
description: 'Description',
|
|
84
|
+
id: 'ID',
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Localized count noun for the array-of-objects badge (`1 ítem` / `2 ítems`). */
|
|
89
|
+
const ITEM_NOUN: Record<string, { one: string; other: string }> = {
|
|
90
|
+
es: { one: 'ítem', other: 'ítems' },
|
|
91
|
+
en: { one: 'item', other: 'items' },
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Localized key label for a popover column header. Resolution order:
|
|
96
|
+
* (a) host i18n `t(rawKey)` if it resolves to something ≠ rawKey;
|
|
97
|
+
* (b) the built-in generic es/en data/commerce dictionary;
|
|
98
|
+
* (c) snake_case → Title Case prettify (`product_id` → "Product ID").
|
|
99
|
+
* `locale` defaults to 'en' when absent.
|
|
100
|
+
*/
|
|
101
|
+
export function prettifyKey(
|
|
102
|
+
key: string,
|
|
103
|
+
locale?: string,
|
|
104
|
+
t?: Translate,
|
|
105
|
+
): string {
|
|
106
|
+
if (t) {
|
|
107
|
+
const translated = t(key)
|
|
108
|
+
if (translated && translated !== key) return translated
|
|
109
|
+
}
|
|
110
|
+
const lang = baseLang(locale)
|
|
111
|
+
const dict = KEY_DICTIONARY[lang] ?? KEY_DICTIONARY.en
|
|
112
|
+
const hit = dict[key.toLowerCase()]
|
|
113
|
+
if (hit) return hit
|
|
30
114
|
const pretty = humanizeToken(key)
|
|
31
115
|
return pretty || key
|
|
32
116
|
}
|
|
33
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Localized, pluralized count noun. Prefers the host i18n `t` (react-i18next
|
|
120
|
+
* count plural via `defaultValue`); otherwise the built-in es/en noun map.
|
|
121
|
+
*/
|
|
122
|
+
export function countLabel(
|
|
123
|
+
count: number,
|
|
124
|
+
locale?: string,
|
|
125
|
+
t?: Translate,
|
|
126
|
+
): string {
|
|
127
|
+
const lang = baseLang(locale)
|
|
128
|
+
const noun = ITEM_NOUN[lang] ?? ITEM_NOUN.en
|
|
129
|
+
const fallback = `${count} ${count === 1 ? noun.one : noun.other}`
|
|
130
|
+
if (t) {
|
|
131
|
+
const translated = t('runtime.collectionCell.itemCount', {
|
|
132
|
+
count,
|
|
133
|
+
defaultValue: fallback,
|
|
134
|
+
})
|
|
135
|
+
if (translated && translated !== 'runtime.collectionCell.itemCount') {
|
|
136
|
+
return translated
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return fallback
|
|
140
|
+
}
|
|
141
|
+
|
|
34
142
|
/**
|
|
35
143
|
* Render a single scalar (or near-scalar) value for compact display.
|
|
36
144
|
* - uuid-like or very long (32+ char) strings → first 8 chars + "…"
|
|
@@ -92,7 +200,15 @@ function unionKeys(rows: Record<string, unknown>[]): string[] {
|
|
|
92
200
|
|
|
93
201
|
const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0'
|
|
94
202
|
|
|
95
|
-
function MiniTable({
|
|
203
|
+
function MiniTable({
|
|
204
|
+
rows,
|
|
205
|
+
locale,
|
|
206
|
+
t,
|
|
207
|
+
}: {
|
|
208
|
+
rows: Record<string, unknown>[]
|
|
209
|
+
locale?: string
|
|
210
|
+
t?: Translate
|
|
211
|
+
}) {
|
|
96
212
|
const keys = unionKeys(rows)
|
|
97
213
|
if (keys.length === 0) {
|
|
98
214
|
return <div className="p-3 text-xs text-muted-foreground">-</div>
|
|
@@ -103,7 +219,7 @@ function MiniTable({ rows }: { rows: Record<string, unknown>[] }) {
|
|
|
103
219
|
<TableRow>
|
|
104
220
|
{keys.map((key) => (
|
|
105
221
|
<TableHead key={key} className="text-xs whitespace-nowrap">
|
|
106
|
-
{prettifyKey(key)}
|
|
222
|
+
{prettifyKey(key, locale, t)}
|
|
107
223
|
</TableHead>
|
|
108
224
|
))}
|
|
109
225
|
</TableRow>
|
|
@@ -138,13 +254,21 @@ function ScalarList({ values }: { values: unknown[] }) {
|
|
|
138
254
|
)
|
|
139
255
|
}
|
|
140
256
|
|
|
141
|
-
function PairList({
|
|
257
|
+
function PairList({
|
|
258
|
+
entries,
|
|
259
|
+
locale,
|
|
260
|
+
t,
|
|
261
|
+
}: {
|
|
262
|
+
entries: [string, unknown][]
|
|
263
|
+
locale?: string
|
|
264
|
+
t?: Translate
|
|
265
|
+
}) {
|
|
142
266
|
return (
|
|
143
267
|
<ul className="p-3 space-y-1">
|
|
144
268
|
{entries.map(([key, v]) => (
|
|
145
269
|
<li key={key} className="text-xs">
|
|
146
270
|
<span className="text-muted-foreground">
|
|
147
|
-
{prettifyKey(key)}:
|
|
271
|
+
{prettifyKey(key, locale, t)}:
|
|
148
272
|
</span>{' '}
|
|
149
273
|
<span className="text-foreground">{formatScalar(v)}</span>
|
|
150
274
|
</li>
|
|
@@ -191,13 +315,22 @@ export interface CollectionCellProps {
|
|
|
191
315
|
value: unknown
|
|
192
316
|
/** Max items previewed inline for scalar arrays. */
|
|
193
317
|
maxInline?: number
|
|
318
|
+
/** Org/UI language tag (e.g. `es`, `en-US`). Defaults to `'en'`. */
|
|
319
|
+
locale?: string
|
|
320
|
+
/** Host i18n translator; takes precedence over the built-in dictionary. */
|
|
321
|
+
t?: Translate
|
|
194
322
|
}
|
|
195
323
|
|
|
196
324
|
/**
|
|
197
325
|
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
198
|
-
* compact, dark-mode friendly. Never throws on unexpected shapes.
|
|
326
|
+
* compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
|
|
199
327
|
*/
|
|
200
|
-
export function CollectionCell({
|
|
328
|
+
export function CollectionCell({
|
|
329
|
+
value,
|
|
330
|
+
maxInline = 3,
|
|
331
|
+
locale,
|
|
332
|
+
t,
|
|
333
|
+
}: CollectionCellProps) {
|
|
201
334
|
const parsed = parseValue(value)
|
|
202
335
|
|
|
203
336
|
// Empty-ish → muted dash.
|
|
@@ -230,17 +363,20 @@ export function CollectionCell({ value, maxInline = 3 }: CollectionCellProps) {
|
|
|
230
363
|
if (allObjects) {
|
|
231
364
|
const rows = parsed as Record<string, unknown>[]
|
|
232
365
|
const count = rows.length
|
|
233
|
-
const label = count
|
|
366
|
+
const label = countLabel(count, locale, t)
|
|
234
367
|
const title = rows
|
|
235
368
|
.map((row) =>
|
|
236
369
|
Object.entries(row)
|
|
237
|
-
.map(
|
|
370
|
+
.map(
|
|
371
|
+
([k, v]) =>
|
|
372
|
+
`${prettifyKey(k, locale, t)}: ${formatScalar(v)}`
|
|
373
|
+
)
|
|
238
374
|
.join(', ')
|
|
239
375
|
)
|
|
240
376
|
.join(' | ')
|
|
241
377
|
return (
|
|
242
378
|
<PopoverShell label={label} title={title}>
|
|
243
|
-
<MiniTable rows={rows} />
|
|
379
|
+
<MiniTable rows={rows} locale={locale} t={t} />
|
|
244
380
|
</PopoverShell>
|
|
245
381
|
)
|
|
246
382
|
}
|
|
@@ -262,16 +398,16 @@ export function CollectionCell({ value, maxInline = 3 }: CollectionCellProps) {
|
|
|
262
398
|
const entries = Object.entries(parsed)
|
|
263
399
|
const inline = entries
|
|
264
400
|
.slice(0, maxInline)
|
|
265
|
-
.map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
|
|
401
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
266
402
|
.join(', ')
|
|
267
403
|
const overflow = entries.length - maxInline
|
|
268
404
|
const label = overflow > 0 ? `${inline} +${overflow}` : inline
|
|
269
405
|
const title = entries
|
|
270
|
-
.map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
|
|
406
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
271
407
|
.join(', ')
|
|
272
408
|
return (
|
|
273
409
|
<PopoverShell label={label} title={title} icon={false}>
|
|
274
|
-
<PairList entries={entries} />
|
|
410
|
+
<PairList entries={entries} locale={locale} t={t} />
|
|
275
411
|
</PopoverShell>
|
|
276
412
|
)
|
|
277
413
|
}
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -1174,7 +1174,13 @@ export function makeDefaultGetDynamicColumns(
|
|
|
1174
1174
|
|
|
1175
1175
|
default: {
|
|
1176
1176
|
if (typeof value === 'object' && value !== null) {
|
|
1177
|
-
return
|
|
1177
|
+
return (
|
|
1178
|
+
<CollectionCell
|
|
1179
|
+
value={value}
|
|
1180
|
+
locale={currentLanguage}
|
|
1181
|
+
t={t}
|
|
1182
|
+
/>
|
|
1183
|
+
)
|
|
1178
1184
|
}
|
|
1179
1185
|
if (
|
|
1180
1186
|
col.key === 'description' ||
|
package/src/index.ts
CHANGED
|
@@ -105,7 +105,9 @@ export {
|
|
|
105
105
|
CollectionCell,
|
|
106
106
|
formatScalar,
|
|
107
107
|
prettifyKey,
|
|
108
|
+
countLabel,
|
|
108
109
|
type CollectionCellProps,
|
|
110
|
+
type Translate as CollectionCellTranslate,
|
|
109
111
|
} from './collection-cell'
|
|
110
112
|
export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid'
|
|
111
113
|
export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record'
|