@asteby/metacore-runtime-react 18.18.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 +33 -0
- package/dist/collection-cell.d.ts +39 -0
- package/dist/collection-cell.d.ts.map +1 -0
- package/dist/collection-cell.js +236 -0
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/__tests__/collection-cell.test.tsx +178 -0
- package/src/collection-cell.tsx +413 -0
- package/src/dynamic-columns.tsx +6 -3
- package/src/index.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
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
|
+
|
|
14
|
+
## 18.19.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- de0b4bb: Add a generic `CollectionCell` renderer for jsonb / array / object table-cell values.
|
|
19
|
+
|
|
20
|
+
Previously the `default:` branch of `defaultGetDynamicColumns` rendered such
|
|
21
|
+
values as raw `JSON.stringify(value)`, which was unreadable. Every jsonb column
|
|
22
|
+
now renders a compact, brand-neutral, dark-mode-friendly cell with no per-addon
|
|
23
|
+
config:
|
|
24
|
+
- **Array of objects** (e.g. line items): a count Badge (`2 ítems`) that opens a
|
|
25
|
+
Popover mini-table — columns are the prettified union of row keys, cells go
|
|
26
|
+
through a shared `formatScalar` (uuid/long strings truncated, booleans as
|
|
27
|
+
✓/✗, nested shapes summarized).
|
|
28
|
+
- **Array of scalars**: first few joined inline with a `+N` overflow, full list
|
|
29
|
+
in the popover.
|
|
30
|
+
- **Plain object**: first few `key: value` pairs inline, all pairs in the popover.
|
|
31
|
+
- **null / empty**: muted `-`.
|
|
32
|
+
- JSON-string values are defensively parsed; unparseable strings are truncated.
|
|
33
|
+
|
|
34
|
+
Exports `CollectionCell`, `formatScalar`, `prettifyKey`, and `CollectionCellProps`.
|
|
35
|
+
|
|
3
36
|
## 18.18.0
|
|
4
37
|
|
|
5
38
|
### Minor Changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
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;
|
|
17
|
+
/**
|
|
18
|
+
* Render a single scalar (or near-scalar) value for compact display.
|
|
19
|
+
* - uuid-like or very long (32+ char) strings → first 8 chars + "…"
|
|
20
|
+
* - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
|
|
21
|
+
* - nested object → "{…}", nested array → "[N]"
|
|
22
|
+
* - null / undefined / "" → "-"
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatScalar(value: unknown): string;
|
|
25
|
+
export interface CollectionCellProps {
|
|
26
|
+
value: unknown;
|
|
27
|
+
/** Max items previewed inline for scalar arrays. */
|
|
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;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
36
|
+
* compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
|
|
37
|
+
*/
|
|
38
|
+
export declare function CollectionCell({ value, maxInline, locale, t, }: CollectionCellProps): React.JSX.Element;
|
|
39
|
+
//# sourceMappingURL=collection-cell.d.ts.map
|
|
@@ -0,0 +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,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"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { List } from 'lucide-react';
|
|
3
|
+
import { Badge, Popover, PopoverContent, PopoverTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, } from '@asteby/metacore-ui';
|
|
4
|
+
import { humanizeToken } from './dynamic-columns-helpers';
|
|
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
|
+
/** 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;
|
|
84
|
+
const pretty = humanizeToken(key);
|
|
85
|
+
return pretty || key;
|
|
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
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Render a single scalar (or near-scalar) value for compact display.
|
|
108
|
+
* - uuid-like or very long (32+ char) strings → first 8 chars + "…"
|
|
109
|
+
* - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
|
|
110
|
+
* - nested object → "{…}", nested array → "[N]"
|
|
111
|
+
* - null / undefined / "" → "-"
|
|
112
|
+
*/
|
|
113
|
+
export function formatScalar(value) {
|
|
114
|
+
if (value === null || value === undefined)
|
|
115
|
+
return '-';
|
|
116
|
+
if (typeof value === 'boolean')
|
|
117
|
+
return value ? '✓' : '✗';
|
|
118
|
+
if (typeof value === 'number')
|
|
119
|
+
return String(value);
|
|
120
|
+
if (Array.isArray(value))
|
|
121
|
+
return `[${value.length}]`;
|
|
122
|
+
if (typeof value === 'object')
|
|
123
|
+
return '{…}';
|
|
124
|
+
const str = String(value);
|
|
125
|
+
if (str === '')
|
|
126
|
+
return '-';
|
|
127
|
+
if (UUID_LIKE_RE.test(str) || str.length >= 32) {
|
|
128
|
+
return `${str.slice(0, 8)}…`;
|
|
129
|
+
}
|
|
130
|
+
return str;
|
|
131
|
+
}
|
|
132
|
+
const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
133
|
+
/**
|
|
134
|
+
* Defensively coerce a raw cell value into something renderable. Strings that
|
|
135
|
+
* look like JSON (`[`/`{` start) are parsed; everything else is passed through.
|
|
136
|
+
*/
|
|
137
|
+
function parseValue(value) {
|
|
138
|
+
if (typeof value !== 'string')
|
|
139
|
+
return value;
|
|
140
|
+
const trimmed = value.trim();
|
|
141
|
+
if ((trimmed.startsWith('[') && trimmed.endsWith(']')) ||
|
|
142
|
+
(trimmed.startsWith('{') && trimmed.endsWith('}'))) {
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(trimmed);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
/** Stable union of keys across an array of row objects, first-seen order. */
|
|
153
|
+
function unionKeys(rows) {
|
|
154
|
+
const seen = [];
|
|
155
|
+
const set = new Set();
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
for (const key of Object.keys(row)) {
|
|
158
|
+
if (!set.has(key)) {
|
|
159
|
+
set.add(key);
|
|
160
|
+
seen.push(key);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return seen;
|
|
165
|
+
}
|
|
166
|
+
const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0';
|
|
167
|
+
function MiniTable({ rows, locale, t, }) {
|
|
168
|
+
const keys = unionKeys(rows);
|
|
169
|
+
if (keys.length === 0) {
|
|
170
|
+
return _jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "-" });
|
|
171
|
+
}
|
|
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))) })] }));
|
|
173
|
+
}
|
|
174
|
+
function ScalarList({ values }) {
|
|
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))) }));
|
|
176
|
+
}
|
|
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))) }));
|
|
179
|
+
}
|
|
180
|
+
/** Compact badge trigger that opens a popover panel. */
|
|
181
|
+
function PopoverShell({ label, title, children, icon = true, }) {
|
|
182
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Badge, { variant: "secondary", className: "cursor-pointer gap-1 font-normal", title: title, children: [icon ? _jsx(List, { className: "h-3 w-3" }) : null, label] }) }), _jsx(PopoverContent, { align: "start", className: cn(PANEL_CLASS), children: children })] }));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
186
|
+
* compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
|
|
187
|
+
*/
|
|
188
|
+
export function CollectionCell({ value, maxInline = 3, locale, t, }) {
|
|
189
|
+
const parsed = parseValue(value);
|
|
190
|
+
// Empty-ish → muted dash.
|
|
191
|
+
if (parsed === null ||
|
|
192
|
+
parsed === undefined ||
|
|
193
|
+
parsed === '' ||
|
|
194
|
+
(Array.isArray(parsed) && parsed.length === 0) ||
|
|
195
|
+
(isPlainObject(parsed) && Object.keys(parsed).length === 0)) {
|
|
196
|
+
return _jsx("span", { className: "text-muted-foreground text-xs", children: "-" });
|
|
197
|
+
}
|
|
198
|
+
// Non-collection scalar fell through here (e.g. unparseable string): truncate.
|
|
199
|
+
if (!Array.isArray(parsed) && !isPlainObject(parsed)) {
|
|
200
|
+
const str = String(parsed);
|
|
201
|
+
return (_jsx("span", { className: "text-muted-foreground text-xs truncate block max-w-[300px]", title: str, children: str.length > 80 ? `${str.slice(0, 80)}…` : str }));
|
|
202
|
+
}
|
|
203
|
+
// ARRAY ------------------------------------------------------------------
|
|
204
|
+
if (Array.isArray(parsed)) {
|
|
205
|
+
const allObjects = parsed.every((item) => isPlainObject(item));
|
|
206
|
+
if (allObjects) {
|
|
207
|
+
const rows = parsed;
|
|
208
|
+
const count = rows.length;
|
|
209
|
+
const label = countLabel(count, locale, t);
|
|
210
|
+
const title = rows
|
|
211
|
+
.map((row) => Object.entries(row)
|
|
212
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
213
|
+
.join(', '))
|
|
214
|
+
.join(' | ');
|
|
215
|
+
return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows, locale: locale, t: t }) }));
|
|
216
|
+
}
|
|
217
|
+
// Array of scalars (or mixed): preview first N joined, "+N" overflow.
|
|
218
|
+
const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ');
|
|
219
|
+
const overflow = parsed.length - maxInline;
|
|
220
|
+
const label = overflow > 0 ? `${preview} +${overflow}` : preview;
|
|
221
|
+
const title = parsed.map(formatScalar).join(', ');
|
|
222
|
+
return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(ScalarList, { values: parsed }) }));
|
|
223
|
+
}
|
|
224
|
+
// PLAIN OBJECT -----------------------------------------------------------
|
|
225
|
+
const entries = Object.entries(parsed);
|
|
226
|
+
const inline = entries
|
|
227
|
+
.slice(0, maxInline)
|
|
228
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
229
|
+
.join(', ');
|
|
230
|
+
const overflow = entries.length - maxInline;
|
|
231
|
+
const label = overflow > 0 ? `${inline} +${overflow}` : inline;
|
|
232
|
+
const title = entries
|
|
233
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
234
|
+
.join(', ');
|
|
235
|
+
return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(PairList, { entries: entries, locale: locale, t: t }) }));
|
|
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;
|
|
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
|
@@ -24,6 +24,7 @@ import { Progress } from './dialogs/_primitives';
|
|
|
24
24
|
import { humanizeToken } from './dynamic-columns-helpers';
|
|
25
25
|
import { OptionsContext } from './options-context';
|
|
26
26
|
import { DynamicIcon, isLucideIconName } from './dynamic-icon';
|
|
27
|
+
import { CollectionCell } from './collection-cell';
|
|
27
28
|
import { isNilUuid, normalizeNilUuid } from './nil-uuid';
|
|
28
29
|
import { isColumnVisibleInTable } from './column-visibility';
|
|
29
30
|
const defaultGetImageUrl = (path) => path;
|
|
@@ -735,7 +736,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
735
736
|
}
|
|
736
737
|
default: {
|
|
737
738
|
if (typeof value === 'object' && value !== null) {
|
|
738
|
-
return (_jsx(
|
|
739
|
+
return (_jsx(CollectionCell, { value: value, locale: currentLanguage, t: t }));
|
|
739
740
|
}
|
|
740
741
|
if (col.key === 'description' ||
|
|
741
742
|
col.key === 'features' ||
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +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, countLabel, type CollectionCellProps, type Translate as CollectionCellTranslate, } from './collection-cell';
|
|
25
26
|
export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
|
|
26
27
|
export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
|
|
27
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":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -26,6 +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, countLabel, } from './collection-cell';
|
|
29
30
|
export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid';
|
|
30
31
|
export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record';
|
|
31
32
|
export { CreateRecordDialog } from './dialogs/create-record-dialog';
|
package/package.json
CHANGED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// CollectionCell contract: the generic jsonb / array / object cell renderer.
|
|
4
|
+
// - array of objects → count badge + popover mini-table (title carries rows)
|
|
5
|
+
// - array of scalars → inline preview + "+N" overflow
|
|
6
|
+
// - plain object → inline key: value pairs
|
|
7
|
+
// - null / empty → "-"
|
|
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.
|
|
11
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
12
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
13
|
+
|
|
14
|
+
// Sin `globals: true` en vitest, RTL no auto-limpia entre tests.
|
|
15
|
+
afterEach(cleanup)
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
CollectionCell,
|
|
19
|
+
countLabel,
|
|
20
|
+
formatScalar,
|
|
21
|
+
prettifyKey,
|
|
22
|
+
} from '../collection-cell'
|
|
23
|
+
|
|
24
|
+
describe('formatScalar', () => {
|
|
25
|
+
it('truncates uuid-like / long strings to first 8 chars + ellipsis', () => {
|
|
26
|
+
expect(formatScalar('550e8400-e29b-41d4-a716-446655440000')).toBe(
|
|
27
|
+
'550e8400…'
|
|
28
|
+
)
|
|
29
|
+
expect(formatScalar('x'.repeat(40))).toBe(`${'x'.repeat(8)}…`)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('passes numbers through and renders booleans as check/cross', () => {
|
|
33
|
+
expect(formatScalar(42)).toBe('42')
|
|
34
|
+
expect(formatScalar(true)).toBe('✓')
|
|
35
|
+
expect(formatScalar(false)).toBe('✗')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('summarizes nested object / array and empties as dash', () => {
|
|
39
|
+
expect(formatScalar({ a: 1 })).toBe('{…}')
|
|
40
|
+
expect(formatScalar([1, 2, 3])).toBe('[3]')
|
|
41
|
+
expect(formatScalar(null)).toBe('-')
|
|
42
|
+
expect(formatScalar('')).toBe('-')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('prettifyKey', () => {
|
|
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')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('CollectionCell', () => {
|
|
90
|
+
it('renders an English count badge by default for an array of objects', () => {
|
|
91
|
+
render(
|
|
92
|
+
<CollectionCell
|
|
93
|
+
value={[
|
|
94
|
+
{ product_id: 'abc', quantity: 2 },
|
|
95
|
+
{ product_id: 'def', quantity: 5 },
|
|
96
|
+
]}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
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]')
|
|
103
|
+
expect(badge).toBeTruthy()
|
|
104
|
+
const title = badge!.getAttribute('title') ?? ''
|
|
105
|
+
expect(title).toContain('Quantity: 2')
|
|
106
|
+
expect(title).toContain('Quantity: 5')
|
|
107
|
+
expect(title).toContain('Product:') // product_id → "Product" (en)
|
|
108
|
+
})
|
|
109
|
+
|
|
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' }]} />)
|
|
130
|
+
expect(screen.getByText('1 ítem')).toBeTruthy()
|
|
131
|
+
})
|
|
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
|
+
|
|
138
|
+
it('previews the first scalars with overflow for a scalar array', () => {
|
|
139
|
+
render(<CollectionCell value={['a', 'b', 'c', 'd', 'e']} />)
|
|
140
|
+
expect(screen.getByText('a, b, c +2')).toBeTruthy()
|
|
141
|
+
})
|
|
142
|
+
|
|
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()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('renders a dash for null / empty values', () => {
|
|
151
|
+
const { container } = render(<CollectionCell value={null} />)
|
|
152
|
+
expect(container.textContent).toBe('-')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('renders a dash for an empty array', () => {
|
|
156
|
+
const { container } = render(<CollectionCell value={[]} />)
|
|
157
|
+
expect(container.textContent).toBe('-')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('parses a JSON-string value into a collection', () => {
|
|
161
|
+
render(
|
|
162
|
+
<CollectionCell
|
|
163
|
+
locale="es"
|
|
164
|
+
value={'[{"product_id":"abc","quantity":1}]'}
|
|
165
|
+
/>
|
|
166
|
+
)
|
|
167
|
+
expect(screen.getByText('1 ítem')).toBeTruthy()
|
|
168
|
+
const badge = screen.getByText('1 ítem').closest('[title]')
|
|
169
|
+
expect(badge!.getAttribute('title')).toContain('Cantidad: 1')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('truncates an unparseable string instead of crashing', () => {
|
|
173
|
+
const { container } = render(
|
|
174
|
+
<CollectionCell value={'{not valid json'} />
|
|
175
|
+
)
|
|
176
|
+
expect(container.textContent).toContain('{not valid json')
|
|
177
|
+
})
|
|
178
|
+
})
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// Generic, brand-neutral table-cell renderer for jsonb / array / object column
|
|
2
|
+
// values. Kernel-derived dynamic tables surface raw jsonb columns (line items,
|
|
3
|
+
// nested config blobs, scalar arrays) with no per-column metadata; without this
|
|
4
|
+
// they rendered as raw `JSON.stringify(value)` which is unreadable. This renders
|
|
5
|
+
// a compact trigger (count badge / inline pairs) plus a Popover with a clean
|
|
6
|
+
// mini-table — no per-addon config required, safe on any shape.
|
|
7
|
+
|
|
8
|
+
import * as React from 'react'
|
|
9
|
+
import { List } from 'lucide-react'
|
|
10
|
+
import {
|
|
11
|
+
Badge,
|
|
12
|
+
Popover,
|
|
13
|
+
PopoverContent,
|
|
14
|
+
PopoverTrigger,
|
|
15
|
+
Table,
|
|
16
|
+
TableBody,
|
|
17
|
+
TableCell,
|
|
18
|
+
TableHead,
|
|
19
|
+
TableHeader,
|
|
20
|
+
TableRow,
|
|
21
|
+
cn,
|
|
22
|
+
} from '@asteby/metacore-ui'
|
|
23
|
+
import { humanizeToken } from './dynamic-columns-helpers'
|
|
24
|
+
|
|
25
|
+
const UUID_LIKE_RE =
|
|
26
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
27
|
+
|
|
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
|
|
114
|
+
const pretty = humanizeToken(key)
|
|
115
|
+
return pretty || key
|
|
116
|
+
}
|
|
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
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Render a single scalar (or near-scalar) value for compact display.
|
|
144
|
+
* - uuid-like or very long (32+ char) strings → first 8 chars + "…"
|
|
145
|
+
* - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
|
|
146
|
+
* - nested object → "{…}", nested array → "[N]"
|
|
147
|
+
* - null / undefined / "" → "-"
|
|
148
|
+
*/
|
|
149
|
+
export function formatScalar(value: unknown): string {
|
|
150
|
+
if (value === null || value === undefined) return '-'
|
|
151
|
+
if (typeof value === 'boolean') return value ? '✓' : '✗'
|
|
152
|
+
if (typeof value === 'number') return String(value)
|
|
153
|
+
if (Array.isArray(value)) return `[${value.length}]`
|
|
154
|
+
if (typeof value === 'object') return '{…}'
|
|
155
|
+
const str = String(value)
|
|
156
|
+
if (str === '') return '-'
|
|
157
|
+
if (UUID_LIKE_RE.test(str) || str.length >= 32) {
|
|
158
|
+
return `${str.slice(0, 8)}…`
|
|
159
|
+
}
|
|
160
|
+
return str
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
164
|
+
typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Defensively coerce a raw cell value into something renderable. Strings that
|
|
168
|
+
* look like JSON (`[`/`{` start) are parsed; everything else is passed through.
|
|
169
|
+
*/
|
|
170
|
+
function parseValue(value: unknown): unknown {
|
|
171
|
+
if (typeof value !== 'string') return value
|
|
172
|
+
const trimmed = value.trim()
|
|
173
|
+
if (
|
|
174
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']')) ||
|
|
175
|
+
(trimmed.startsWith('{') && trimmed.endsWith('}'))
|
|
176
|
+
) {
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(trimmed)
|
|
179
|
+
} catch {
|
|
180
|
+
return value
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return value
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Stable union of keys across an array of row objects, first-seen order. */
|
|
187
|
+
function unionKeys(rows: Record<string, unknown>[]): string[] {
|
|
188
|
+
const seen: string[] = []
|
|
189
|
+
const set = new Set<string>()
|
|
190
|
+
for (const row of rows) {
|
|
191
|
+
for (const key of Object.keys(row)) {
|
|
192
|
+
if (!set.has(key)) {
|
|
193
|
+
set.add(key)
|
|
194
|
+
seen.push(key)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return seen
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0'
|
|
202
|
+
|
|
203
|
+
function MiniTable({
|
|
204
|
+
rows,
|
|
205
|
+
locale,
|
|
206
|
+
t,
|
|
207
|
+
}: {
|
|
208
|
+
rows: Record<string, unknown>[]
|
|
209
|
+
locale?: string
|
|
210
|
+
t?: Translate
|
|
211
|
+
}) {
|
|
212
|
+
const keys = unionKeys(rows)
|
|
213
|
+
if (keys.length === 0) {
|
|
214
|
+
return <div className="p-3 text-xs text-muted-foreground">-</div>
|
|
215
|
+
}
|
|
216
|
+
return (
|
|
217
|
+
<Table>
|
|
218
|
+
<TableHeader>
|
|
219
|
+
<TableRow>
|
|
220
|
+
{keys.map((key) => (
|
|
221
|
+
<TableHead key={key} className="text-xs whitespace-nowrap">
|
|
222
|
+
{prettifyKey(key, locale, t)}
|
|
223
|
+
</TableHead>
|
|
224
|
+
))}
|
|
225
|
+
</TableRow>
|
|
226
|
+
</TableHeader>
|
|
227
|
+
<TableBody>
|
|
228
|
+
{rows.map((row, i) => (
|
|
229
|
+
<TableRow key={i}>
|
|
230
|
+
{keys.map((key) => (
|
|
231
|
+
<TableCell
|
|
232
|
+
key={key}
|
|
233
|
+
className="text-xs whitespace-nowrap"
|
|
234
|
+
>
|
|
235
|
+
{formatScalar(row[key])}
|
|
236
|
+
</TableCell>
|
|
237
|
+
))}
|
|
238
|
+
</TableRow>
|
|
239
|
+
))}
|
|
240
|
+
</TableBody>
|
|
241
|
+
</Table>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function ScalarList({ values }: { values: unknown[] }) {
|
|
246
|
+
return (
|
|
247
|
+
<ul className="p-3 space-y-1">
|
|
248
|
+
{values.map((v, i) => (
|
|
249
|
+
<li key={i} className="text-xs text-foreground">
|
|
250
|
+
{formatScalar(v)}
|
|
251
|
+
</li>
|
|
252
|
+
))}
|
|
253
|
+
</ul>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function PairList({
|
|
258
|
+
entries,
|
|
259
|
+
locale,
|
|
260
|
+
t,
|
|
261
|
+
}: {
|
|
262
|
+
entries: [string, unknown][]
|
|
263
|
+
locale?: string
|
|
264
|
+
t?: Translate
|
|
265
|
+
}) {
|
|
266
|
+
return (
|
|
267
|
+
<ul className="p-3 space-y-1">
|
|
268
|
+
{entries.map(([key, v]) => (
|
|
269
|
+
<li key={key} className="text-xs">
|
|
270
|
+
<span className="text-muted-foreground">
|
|
271
|
+
{prettifyKey(key, locale, t)}:
|
|
272
|
+
</span>{' '}
|
|
273
|
+
<span className="text-foreground">{formatScalar(v)}</span>
|
|
274
|
+
</li>
|
|
275
|
+
))}
|
|
276
|
+
</ul>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Compact badge trigger that opens a popover panel. */
|
|
281
|
+
function PopoverShell({
|
|
282
|
+
label,
|
|
283
|
+
title,
|
|
284
|
+
children,
|
|
285
|
+
icon = true,
|
|
286
|
+
}: {
|
|
287
|
+
label: string
|
|
288
|
+
title: string
|
|
289
|
+
children: React.ReactNode
|
|
290
|
+
icon?: boolean
|
|
291
|
+
}) {
|
|
292
|
+
return (
|
|
293
|
+
<Popover>
|
|
294
|
+
<PopoverTrigger asChild>
|
|
295
|
+
<Badge
|
|
296
|
+
variant="secondary"
|
|
297
|
+
className="cursor-pointer gap-1 font-normal"
|
|
298
|
+
title={title}
|
|
299
|
+
>
|
|
300
|
+
{icon ? <List className="h-3 w-3" /> : null}
|
|
301
|
+
{label}
|
|
302
|
+
</Badge>
|
|
303
|
+
</PopoverTrigger>
|
|
304
|
+
<PopoverContent
|
|
305
|
+
align="start"
|
|
306
|
+
className={cn(PANEL_CLASS)}
|
|
307
|
+
>
|
|
308
|
+
{children}
|
|
309
|
+
</PopoverContent>
|
|
310
|
+
</Popover>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface CollectionCellProps {
|
|
315
|
+
value: unknown
|
|
316
|
+
/** Max items previewed inline for scalar arrays. */
|
|
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
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
326
|
+
* compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
|
|
327
|
+
*/
|
|
328
|
+
export function CollectionCell({
|
|
329
|
+
value,
|
|
330
|
+
maxInline = 3,
|
|
331
|
+
locale,
|
|
332
|
+
t,
|
|
333
|
+
}: CollectionCellProps) {
|
|
334
|
+
const parsed = parseValue(value)
|
|
335
|
+
|
|
336
|
+
// Empty-ish → muted dash.
|
|
337
|
+
if (
|
|
338
|
+
parsed === null ||
|
|
339
|
+
parsed === undefined ||
|
|
340
|
+
parsed === '' ||
|
|
341
|
+
(Array.isArray(parsed) && parsed.length === 0) ||
|
|
342
|
+
(isPlainObject(parsed) && Object.keys(parsed).length === 0)
|
|
343
|
+
) {
|
|
344
|
+
return <span className="text-muted-foreground text-xs">-</span>
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Non-collection scalar fell through here (e.g. unparseable string): truncate.
|
|
348
|
+
if (!Array.isArray(parsed) && !isPlainObject(parsed)) {
|
|
349
|
+
const str = String(parsed)
|
|
350
|
+
return (
|
|
351
|
+
<span
|
|
352
|
+
className="text-muted-foreground text-xs truncate block max-w-[300px]"
|
|
353
|
+
title={str}
|
|
354
|
+
>
|
|
355
|
+
{str.length > 80 ? `${str.slice(0, 80)}…` : str}
|
|
356
|
+
</span>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ARRAY ------------------------------------------------------------------
|
|
361
|
+
if (Array.isArray(parsed)) {
|
|
362
|
+
const allObjects = parsed.every((item) => isPlainObject(item))
|
|
363
|
+
if (allObjects) {
|
|
364
|
+
const rows = parsed as Record<string, unknown>[]
|
|
365
|
+
const count = rows.length
|
|
366
|
+
const label = countLabel(count, locale, t)
|
|
367
|
+
const title = rows
|
|
368
|
+
.map((row) =>
|
|
369
|
+
Object.entries(row)
|
|
370
|
+
.map(
|
|
371
|
+
([k, v]) =>
|
|
372
|
+
`${prettifyKey(k, locale, t)}: ${formatScalar(v)}`
|
|
373
|
+
)
|
|
374
|
+
.join(', ')
|
|
375
|
+
)
|
|
376
|
+
.join(' | ')
|
|
377
|
+
return (
|
|
378
|
+
<PopoverShell label={label} title={title}>
|
|
379
|
+
<MiniTable rows={rows} locale={locale} t={t} />
|
|
380
|
+
</PopoverShell>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Array of scalars (or mixed): preview first N joined, "+N" overflow.
|
|
385
|
+
const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ')
|
|
386
|
+
const overflow = parsed.length - maxInline
|
|
387
|
+
const label =
|
|
388
|
+
overflow > 0 ? `${preview} +${overflow}` : preview
|
|
389
|
+
const title = parsed.map(formatScalar).join(', ')
|
|
390
|
+
return (
|
|
391
|
+
<PopoverShell label={label} title={title} icon={false}>
|
|
392
|
+
<ScalarList values={parsed} />
|
|
393
|
+
</PopoverShell>
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// PLAIN OBJECT -----------------------------------------------------------
|
|
398
|
+
const entries = Object.entries(parsed)
|
|
399
|
+
const inline = entries
|
|
400
|
+
.slice(0, maxInline)
|
|
401
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
402
|
+
.join(', ')
|
|
403
|
+
const overflow = entries.length - maxInline
|
|
404
|
+
const label = overflow > 0 ? `${inline} +${overflow}` : inline
|
|
405
|
+
const title = entries
|
|
406
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
407
|
+
.join(', ')
|
|
408
|
+
return (
|
|
409
|
+
<PopoverShell label={label} title={title} icon={false}>
|
|
410
|
+
<PairList entries={entries} locale={locale} t={t} />
|
|
411
|
+
</PopoverShell>
|
|
412
|
+
)
|
|
413
|
+
}
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -45,6 +45,7 @@ import { Progress } from './dialogs/_primitives'
|
|
|
45
45
|
import { humanizeToken } from './dynamic-columns-helpers'
|
|
46
46
|
import { OptionsContext } from './options-context'
|
|
47
47
|
import { DynamicIcon, isLucideIconName } from './dynamic-icon'
|
|
48
|
+
import { CollectionCell } from './collection-cell'
|
|
48
49
|
import { isNilUuid, normalizeNilUuid } from './nil-uuid'
|
|
49
50
|
import type { TableMetadata, ColumnDefinition } from './types'
|
|
50
51
|
import { isColumnVisibleInTable } from './column-visibility'
|
|
@@ -1174,9 +1175,11 @@ export function makeDefaultGetDynamicColumns(
|
|
|
1174
1175
|
default: {
|
|
1175
1176
|
if (typeof value === 'object' && value !== null) {
|
|
1176
1177
|
return (
|
|
1177
|
-
<
|
|
1178
|
-
{
|
|
1179
|
-
|
|
1178
|
+
<CollectionCell
|
|
1179
|
+
value={value}
|
|
1180
|
+
locale={currentLanguage}
|
|
1181
|
+
t={t}
|
|
1182
|
+
/>
|
|
1180
1183
|
)
|
|
1181
1184
|
}
|
|
1182
1185
|
if (
|
package/src/index.ts
CHANGED
|
@@ -101,6 +101,14 @@ export {
|
|
|
101
101
|
type DynamicColumnsHelpers,
|
|
102
102
|
} from './dynamic-columns'
|
|
103
103
|
export { humanizeToken } from './dynamic-columns-helpers'
|
|
104
|
+
export {
|
|
105
|
+
CollectionCell,
|
|
106
|
+
formatScalar,
|
|
107
|
+
prettifyKey,
|
|
108
|
+
countLabel,
|
|
109
|
+
type CollectionCellProps,
|
|
110
|
+
type Translate as CollectionCellTranslate,
|
|
111
|
+
} from './collection-cell'
|
|
104
112
|
export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid'
|
|
105
113
|
export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record'
|
|
106
114
|
export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record'
|