@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 +22 -0
- package/dist/collection-cell.d.ts +22 -0
- package/dist/collection-cell.d.ts.map +1 -0
- package/dist/collection-cell.js +141 -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 +115 -0
- package/src/collection-cell.tsx +277 -0
- package/src/dynamic-columns.tsx +2 -5
- package/src/index.ts +6 -0
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;
|
|
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"}
|
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
|
|
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';
|
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, } 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,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
|
+
}
|
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'
|
|
@@ -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'
|