@asteby/metacore-runtime-react 18.18.0 → 18.19.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,27 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - de0b4bb: Add a generic `CollectionCell` renderer for jsonb / array / object table-cell values.
8
+
9
+ Previously the `default:` branch of `defaultGetDynamicColumns` rendered such
10
+ values as raw `JSON.stringify(value)`, which was unreadable. Every jsonb column
11
+ now renders a compact, brand-neutral, dark-mode-friendly cell with no per-addon
12
+ config:
13
+ - **Array of objects** (e.g. line items): a count Badge (`2 ítems`) that opens a
14
+ Popover mini-table — columns are the prettified union of row keys, cells go
15
+ through a shared `formatScalar` (uuid/long strings truncated, booleans as
16
+ ✓/✗, nested shapes summarized).
17
+ - **Array of scalars**: first few joined inline with a `+N` overflow, full list
18
+ in the popover.
19
+ - **Plain object**: first few `key: value` pairs inline, all pairs in the popover.
20
+ - **null / empty**: muted `-`.
21
+ - JSON-string values are defensively parsed; unparseable strings are truncated.
22
+
23
+ Exports `CollectionCell`, `formatScalar`, `prettifyKey`, and `CollectionCellProps`.
24
+
3
25
  ## 18.18.0
4
26
 
5
27
  ### Minor Changes
@@ -0,0 +1,22 @@
1
+ import * as React from 'react';
2
+ /** snake_case / dotted / kebab key → Title Case (`product_id` → "Product ID"). */
3
+ export declare function prettifyKey(key: string): string;
4
+ /**
5
+ * Render a single scalar (or near-scalar) value for compact display.
6
+ * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
7
+ * - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
8
+ * - nested object → "{…}", nested array → "[N]"
9
+ * - null / undefined / "" → "-"
10
+ */
11
+ export declare function formatScalar(value: unknown): string;
12
+ export interface CollectionCellProps {
13
+ value: unknown;
14
+ /** Max items previewed inline for scalar arrays. */
15
+ maxInline?: number;
16
+ }
17
+ /**
18
+ * Generic renderer for jsonb / array / object cell values. Brand-neutral,
19
+ * compact, dark-mode friendly. Never throws on unexpected shapes.
20
+ */
21
+ export declare function CollectionCell({ value, maxInline }: CollectionCellProps): React.JSX.Element;
22
+ //# 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,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAYnD;AAyID,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,OAAO,CAAA;IACd,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,SAAa,EAAE,EAAE,mBAAmB,qBA6E3E"}
@@ -0,0 +1,141 @@
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
+ /** snake_case / dotted / kebab key → Title Case (`product_id` → "Product ID"). */
7
+ export function prettifyKey(key) {
8
+ const pretty = humanizeToken(key);
9
+ return pretty || key;
10
+ }
11
+ /**
12
+ * Render a single scalar (or near-scalar) value for compact display.
13
+ * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
14
+ * - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
15
+ * - nested object → "{…}", nested array → "[N]"
16
+ * - null / undefined / "" → "-"
17
+ */
18
+ export function formatScalar(value) {
19
+ if (value === null || value === undefined)
20
+ return '-';
21
+ if (typeof value === 'boolean')
22
+ return value ? '✓' : '✗';
23
+ if (typeof value === 'number')
24
+ return String(value);
25
+ if (Array.isArray(value))
26
+ return `[${value.length}]`;
27
+ if (typeof value === 'object')
28
+ return '{…}';
29
+ const str = String(value);
30
+ if (str === '')
31
+ return '-';
32
+ if (UUID_LIKE_RE.test(str) || str.length >= 32) {
33
+ return `${str.slice(0, 8)}…`;
34
+ }
35
+ return str;
36
+ }
37
+ const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
38
+ /**
39
+ * Defensively coerce a raw cell value into something renderable. Strings that
40
+ * look like JSON (`[`/`{` start) are parsed; everything else is passed through.
41
+ */
42
+ function parseValue(value) {
43
+ if (typeof value !== 'string')
44
+ return value;
45
+ const trimmed = value.trim();
46
+ if ((trimmed.startsWith('[') && trimmed.endsWith(']')) ||
47
+ (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
48
+ try {
49
+ return JSON.parse(trimmed);
50
+ }
51
+ catch {
52
+ return value;
53
+ }
54
+ }
55
+ return value;
56
+ }
57
+ /** Stable union of keys across an array of row objects, first-seen order. */
58
+ function unionKeys(rows) {
59
+ const seen = [];
60
+ const set = new Set();
61
+ for (const row of rows) {
62
+ for (const key of Object.keys(row)) {
63
+ if (!set.has(key)) {
64
+ set.add(key);
65
+ seen.push(key);
66
+ }
67
+ }
68
+ }
69
+ return seen;
70
+ }
71
+ const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0';
72
+ function MiniTable({ rows }) {
73
+ const keys = unionKeys(rows);
74
+ if (keys.length === 0) {
75
+ return _jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "-" });
76
+ }
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))) })] }));
78
+ }
79
+ function ScalarList({ values }) {
80
+ 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
+ }
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))) }));
84
+ }
85
+ /** Compact badge trigger that opens a popover panel. */
86
+ function PopoverShell({ label, title, children, icon = true, }) {
87
+ 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 })] }));
88
+ }
89
+ /**
90
+ * Generic renderer for jsonb / array / object cell values. Brand-neutral,
91
+ * compact, dark-mode friendly. Never throws on unexpected shapes.
92
+ */
93
+ export function CollectionCell({ value, maxInline = 3 }) {
94
+ const parsed = parseValue(value);
95
+ // Empty-ish → muted dash.
96
+ if (parsed === null ||
97
+ parsed === undefined ||
98
+ parsed === '' ||
99
+ (Array.isArray(parsed) && parsed.length === 0) ||
100
+ (isPlainObject(parsed) && Object.keys(parsed).length === 0)) {
101
+ return _jsx("span", { className: "text-muted-foreground text-xs", children: "-" });
102
+ }
103
+ // Non-collection scalar fell through here (e.g. unparseable string): truncate.
104
+ if (!Array.isArray(parsed) && !isPlainObject(parsed)) {
105
+ const str = String(parsed);
106
+ 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 }));
107
+ }
108
+ // ARRAY ------------------------------------------------------------------
109
+ if (Array.isArray(parsed)) {
110
+ const allObjects = parsed.every((item) => isPlainObject(item));
111
+ if (allObjects) {
112
+ const rows = parsed;
113
+ const count = rows.length;
114
+ const label = count === 1 ? '1 ítem' : `${count} ítems`;
115
+ const title = rows
116
+ .map((row) => Object.entries(row)
117
+ .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
118
+ .join(', '))
119
+ .join(' | ');
120
+ return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows }) }));
121
+ }
122
+ // Array of scalars (or mixed): preview first N joined, "+N" overflow.
123
+ const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ');
124
+ const overflow = parsed.length - maxInline;
125
+ const label = overflow > 0 ? `${preview} +${overflow}` : preview;
126
+ const title = parsed.map(formatScalar).join(', ');
127
+ return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(ScalarList, { values: parsed }) }));
128
+ }
129
+ // PLAIN OBJECT -----------------------------------------------------------
130
+ const entries = Object.entries(parsed);
131
+ const inline = entries
132
+ .slice(0, maxInline)
133
+ .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
134
+ .join(', ');
135
+ const overflow = entries.length - maxInline;
136
+ const label = overflow > 0 ? `${inline} +${overflow}` : inline;
137
+ const title = entries
138
+ .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
139
+ .join(', ');
140
+ return (_jsx(PopoverShell, { label: label, title: title, icon: false, children: _jsx(PairList, { entries: entries }) }));
141
+ }
@@ -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;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,CA2nBnB;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;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,CAunBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -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("span", { className: "text-muted-foreground text-xs", children: JSON.stringify(value) }));
739
+ return _jsx(CollectionCell, { value: value });
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, type CollectionCellProps, } 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';
@@ -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":"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,GAC3B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAClE,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACzE,YAAY,EAAE,wBAAwB,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAC5G,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAC/D,YAAY,EACR,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,GACxB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACH,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GAC3B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACH,sBAAsB,EACtB,uBAAuB,GAC1B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,kBAAkB,EAClB,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACH,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,GACvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,0BAA0B,GAClC,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACzB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACH,aAAa,EACb,KAAK,kBAAkB,GAC1B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACH,gBAAgB,EAChB,KAAK,qBAAqB,GAC7B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,aAAa,EACb,eAAe,GAClB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACR,UAAU,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,oBAAoB,GACvB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACH,UAAU,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,cAAc,EACd,KAAK,iBAAiB,GACzB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,cAAc,EACd,cAAc,EACd,SAAS,EACT,UAAU,EACV,KAAK,mBAAmB,GAC3B,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACH,UAAU,EACV,SAAS,EACT,WAAW,EACX,WAAW,EACX,KAAK,eAAe,GACvB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACH,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,aAAa,EACb,YAAY,EACZ,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA"}
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, } 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.18.0",
3
+ "version": "18.19.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,115 @@
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
+ import { afterEach, describe, expect, it } from 'vitest'
10
+ import { cleanup, render, screen } from '@testing-library/react'
11
+
12
+ // Sin `globals: true` en vitest, RTL no auto-limpia entre tests.
13
+ afterEach(cleanup)
14
+
15
+ import {
16
+ CollectionCell,
17
+ formatScalar,
18
+ prettifyKey,
19
+ } from '../collection-cell'
20
+
21
+ describe('formatScalar', () => {
22
+ it('truncates uuid-like / long strings to first 8 chars + ellipsis', () => {
23
+ expect(formatScalar('550e8400-e29b-41d4-a716-446655440000')).toBe(
24
+ '550e8400…'
25
+ )
26
+ expect(formatScalar('x'.repeat(40))).toBe(`${'x'.repeat(8)}…`)
27
+ })
28
+
29
+ it('passes numbers through and renders booleans as check/cross', () => {
30
+ expect(formatScalar(42)).toBe('42')
31
+ expect(formatScalar(true)).toBe('✓')
32
+ expect(formatScalar(false)).toBe('✗')
33
+ })
34
+
35
+ it('summarizes nested object / array and empties as dash', () => {
36
+ expect(formatScalar({ a: 1 })).toBe('{…}')
37
+ expect(formatScalar([1, 2, 3])).toBe('[3]')
38
+ expect(formatScalar(null)).toBe('-')
39
+ expect(formatScalar('')).toBe('-')
40
+ })
41
+ })
42
+
43
+ describe('prettifyKey', () => {
44
+ it('snake_case → Title Case with acronyms', () => {
45
+ expect(prettifyKey('product_id')).toBe('Product ID')
46
+ expect(prettifyKey('quantity')).toBe('Quantity')
47
+ })
48
+ })
49
+
50
+ describe('CollectionCell', () => {
51
+ it('renders a count badge for an array of objects', () => {
52
+ render(
53
+ <CollectionCell
54
+ value={[
55
+ { product_id: 'abc', quantity: 2 },
56
+ { product_id: 'def', quantity: 5 },
57
+ ]}
58
+ />
59
+ )
60
+ // Count badge (plural).
61
+ expect(screen.getByText('2 ítems')).toBeTruthy()
62
+ // The trigger's title carries the formatted rows for the no-JS fallback.
63
+ const badge = screen.getByText('2 ítems').closest('[title]')
64
+ expect(badge).toBeTruthy()
65
+ const title = badge!.getAttribute('title') ?? ''
66
+ expect(title).toContain('Quantity: 2')
67
+ expect(title).toContain('Quantity: 5')
68
+ expect(title).toContain('Product ID')
69
+ })
70
+
71
+ it('renders singular label for a single-item array', () => {
72
+ render(<CollectionCell value={[{ sku: 'A1' }]} />)
73
+ expect(screen.getByText('1 ítem')).toBeTruthy()
74
+ })
75
+
76
+ it('previews the first scalars with overflow for a scalar array', () => {
77
+ render(<CollectionCell value={['a', 'b', 'c', 'd', 'e']} />)
78
+ expect(screen.getByText('a, b, c +2')).toBeTruthy()
79
+ })
80
+
81
+ it('renders inline key: value pairs for a plain object', () => {
82
+ render(<CollectionCell value={{ width: 10, height: 20 }} />)
83
+ expect(
84
+ screen.getByText(/Width: 10, Height: 20/)
85
+ ).toBeTruthy()
86
+ })
87
+
88
+ it('renders a dash for null / empty values', () => {
89
+ const { container } = render(<CollectionCell value={null} />)
90
+ expect(container.textContent).toBe('-')
91
+ })
92
+
93
+ it('renders a dash for an empty array', () => {
94
+ const { container } = render(<CollectionCell value={[]} />)
95
+ expect(container.textContent).toBe('-')
96
+ })
97
+
98
+ it('parses a JSON-string value into a collection', () => {
99
+ render(
100
+ <CollectionCell
101
+ value={'[{"product_id":"abc","quantity":1}]'}
102
+ />
103
+ )
104
+ expect(screen.getByText('1 ítem')).toBeTruthy()
105
+ const badge = screen.getByText('1 ítem').closest('[title]')
106
+ expect(badge!.getAttribute('title')).toContain('Quantity: 1')
107
+ })
108
+
109
+ it('truncates an unparseable string instead of crashing', () => {
110
+ const { container } = render(
111
+ <CollectionCell value={'{not valid json'} />
112
+ )
113
+ expect(container.textContent).toContain('{not valid json')
114
+ })
115
+ })
@@ -0,0 +1,277 @@
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
+ /** snake_case / dotted / kebab key → Title Case (`product_id` → "Product ID"). */
29
+ export function prettifyKey(key: string): string {
30
+ const pretty = humanizeToken(key)
31
+ return pretty || key
32
+ }
33
+
34
+ /**
35
+ * Render a single scalar (or near-scalar) value for compact display.
36
+ * - uuid-like or very long (32+ char) strings → first 8 chars + "…"
37
+ * - numbers / booleans → rendered as-is (booleans as ✓ / ✗)
38
+ * - nested object → "{…}", nested array → "[N]"
39
+ * - null / undefined / "" → "-"
40
+ */
41
+ export function formatScalar(value: unknown): string {
42
+ if (value === null || value === undefined) return '-'
43
+ if (typeof value === 'boolean') return value ? '✓' : '✗'
44
+ if (typeof value === 'number') return String(value)
45
+ if (Array.isArray(value)) return `[${value.length}]`
46
+ if (typeof value === 'object') return '{…}'
47
+ const str = String(value)
48
+ if (str === '') return '-'
49
+ if (UUID_LIKE_RE.test(str) || str.length >= 32) {
50
+ return `${str.slice(0, 8)}…`
51
+ }
52
+ return str
53
+ }
54
+
55
+ const isPlainObject = (v: unknown): v is Record<string, unknown> =>
56
+ typeof v === 'object' && v !== null && !Array.isArray(v)
57
+
58
+ /**
59
+ * Defensively coerce a raw cell value into something renderable. Strings that
60
+ * look like JSON (`[`/`{` start) are parsed; everything else is passed through.
61
+ */
62
+ function parseValue(value: unknown): unknown {
63
+ if (typeof value !== 'string') return value
64
+ const trimmed = value.trim()
65
+ if (
66
+ (trimmed.startsWith('[') && trimmed.endsWith(']')) ||
67
+ (trimmed.startsWith('{') && trimmed.endsWith('}'))
68
+ ) {
69
+ try {
70
+ return JSON.parse(trimmed)
71
+ } catch {
72
+ return value
73
+ }
74
+ }
75
+ return value
76
+ }
77
+
78
+ /** Stable union of keys across an array of row objects, first-seen order. */
79
+ function unionKeys(rows: Record<string, unknown>[]): string[] {
80
+ const seen: string[] = []
81
+ const set = new Set<string>()
82
+ for (const row of rows) {
83
+ for (const key of Object.keys(row)) {
84
+ if (!set.has(key)) {
85
+ set.add(key)
86
+ seen.push(key)
87
+ }
88
+ }
89
+ }
90
+ return seen
91
+ }
92
+
93
+ const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0'
94
+
95
+ function MiniTable({ rows }: { rows: Record<string, unknown>[] }) {
96
+ const keys = unionKeys(rows)
97
+ if (keys.length === 0) {
98
+ return <div className="p-3 text-xs text-muted-foreground">-</div>
99
+ }
100
+ return (
101
+ <Table>
102
+ <TableHeader>
103
+ <TableRow>
104
+ {keys.map((key) => (
105
+ <TableHead key={key} className="text-xs whitespace-nowrap">
106
+ {prettifyKey(key)}
107
+ </TableHead>
108
+ ))}
109
+ </TableRow>
110
+ </TableHeader>
111
+ <TableBody>
112
+ {rows.map((row, i) => (
113
+ <TableRow key={i}>
114
+ {keys.map((key) => (
115
+ <TableCell
116
+ key={key}
117
+ className="text-xs whitespace-nowrap"
118
+ >
119
+ {formatScalar(row[key])}
120
+ </TableCell>
121
+ ))}
122
+ </TableRow>
123
+ ))}
124
+ </TableBody>
125
+ </Table>
126
+ )
127
+ }
128
+
129
+ function ScalarList({ values }: { values: unknown[] }) {
130
+ return (
131
+ <ul className="p-3 space-y-1">
132
+ {values.map((v, i) => (
133
+ <li key={i} className="text-xs text-foreground">
134
+ {formatScalar(v)}
135
+ </li>
136
+ ))}
137
+ </ul>
138
+ )
139
+ }
140
+
141
+ function PairList({ entries }: { entries: [string, unknown][] }) {
142
+ return (
143
+ <ul className="p-3 space-y-1">
144
+ {entries.map(([key, v]) => (
145
+ <li key={key} className="text-xs">
146
+ <span className="text-muted-foreground">
147
+ {prettifyKey(key)}:
148
+ </span>{' '}
149
+ <span className="text-foreground">{formatScalar(v)}</span>
150
+ </li>
151
+ ))}
152
+ </ul>
153
+ )
154
+ }
155
+
156
+ /** Compact badge trigger that opens a popover panel. */
157
+ function PopoverShell({
158
+ label,
159
+ title,
160
+ children,
161
+ icon = true,
162
+ }: {
163
+ label: string
164
+ title: string
165
+ children: React.ReactNode
166
+ icon?: boolean
167
+ }) {
168
+ return (
169
+ <Popover>
170
+ <PopoverTrigger asChild>
171
+ <Badge
172
+ variant="secondary"
173
+ className="cursor-pointer gap-1 font-normal"
174
+ title={title}
175
+ >
176
+ {icon ? <List className="h-3 w-3" /> : null}
177
+ {label}
178
+ </Badge>
179
+ </PopoverTrigger>
180
+ <PopoverContent
181
+ align="start"
182
+ className={cn(PANEL_CLASS)}
183
+ >
184
+ {children}
185
+ </PopoverContent>
186
+ </Popover>
187
+ )
188
+ }
189
+
190
+ export interface CollectionCellProps {
191
+ value: unknown
192
+ /** Max items previewed inline for scalar arrays. */
193
+ maxInline?: number
194
+ }
195
+
196
+ /**
197
+ * Generic renderer for jsonb / array / object cell values. Brand-neutral,
198
+ * compact, dark-mode friendly. Never throws on unexpected shapes.
199
+ */
200
+ export function CollectionCell({ value, maxInline = 3 }: CollectionCellProps) {
201
+ const parsed = parseValue(value)
202
+
203
+ // Empty-ish → muted dash.
204
+ if (
205
+ parsed === null ||
206
+ parsed === undefined ||
207
+ parsed === '' ||
208
+ (Array.isArray(parsed) && parsed.length === 0) ||
209
+ (isPlainObject(parsed) && Object.keys(parsed).length === 0)
210
+ ) {
211
+ return <span className="text-muted-foreground text-xs">-</span>
212
+ }
213
+
214
+ // Non-collection scalar fell through here (e.g. unparseable string): truncate.
215
+ if (!Array.isArray(parsed) && !isPlainObject(parsed)) {
216
+ const str = String(parsed)
217
+ return (
218
+ <span
219
+ className="text-muted-foreground text-xs truncate block max-w-[300px]"
220
+ title={str}
221
+ >
222
+ {str.length > 80 ? `${str.slice(0, 80)}…` : str}
223
+ </span>
224
+ )
225
+ }
226
+
227
+ // ARRAY ------------------------------------------------------------------
228
+ if (Array.isArray(parsed)) {
229
+ const allObjects = parsed.every((item) => isPlainObject(item))
230
+ if (allObjects) {
231
+ const rows = parsed as Record<string, unknown>[]
232
+ const count = rows.length
233
+ const label = count === 1 ? '1 ítem' : `${count} ítems`
234
+ const title = rows
235
+ .map((row) =>
236
+ Object.entries(row)
237
+ .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
238
+ .join(', ')
239
+ )
240
+ .join(' | ')
241
+ return (
242
+ <PopoverShell label={label} title={title}>
243
+ <MiniTable rows={rows} />
244
+ </PopoverShell>
245
+ )
246
+ }
247
+
248
+ // Array of scalars (or mixed): preview first N joined, "+N" overflow.
249
+ const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ')
250
+ const overflow = parsed.length - maxInline
251
+ const label =
252
+ overflow > 0 ? `${preview} +${overflow}` : preview
253
+ const title = parsed.map(formatScalar).join(', ')
254
+ return (
255
+ <PopoverShell label={label} title={title} icon={false}>
256
+ <ScalarList values={parsed} />
257
+ </PopoverShell>
258
+ )
259
+ }
260
+
261
+ // PLAIN OBJECT -----------------------------------------------------------
262
+ const entries = Object.entries(parsed)
263
+ const inline = entries
264
+ .slice(0, maxInline)
265
+ .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
266
+ .join(', ')
267
+ const overflow = entries.length - maxInline
268
+ const label = overflow > 0 ? `${inline} +${overflow}` : inline
269
+ const title = entries
270
+ .map(([k, v]) => `${prettifyKey(k)}: ${formatScalar(v)}`)
271
+ .join(', ')
272
+ return (
273
+ <PopoverShell label={label} title={title} icon={false}>
274
+ <PairList entries={entries} />
275
+ </PopoverShell>
276
+ )
277
+ }
@@ -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'
@@ -1173,11 +1174,7 @@ export function makeDefaultGetDynamicColumns(
1173
1174
 
1174
1175
  default: {
1175
1176
  if (typeof value === 'object' && value !== null) {
1176
- return (
1177
- <span className="text-muted-foreground text-xs">
1178
- {JSON.stringify(value)}
1179
- </span>
1180
- )
1177
+ return <CollectionCell value={value} />
1181
1178
  }
1182
1179
  if (
1183
1180
  col.key === 'description' ||
package/src/index.ts CHANGED
@@ -101,6 +101,12 @@ 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
+ type CollectionCellProps,
109
+ } from './collection-cell'
104
110
  export { NIL_UUID, isNilUuid, normalizeNilUuid } from './nil-uuid'
105
111
  export { DynamicRecordDialog, ViewValue } from './dialogs/dynamic-record'
106
112
  export type { DynamicRecordDialogProps, FieldDef, FieldOption, GetImageUrl } from './dialogs/dynamic-record'