@asteby/metacore-runtime-react 18.21.0 → 18.22.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 +16 -0
- package/dist/collection-cell.d.ts +16 -1
- package/dist/collection-cell.d.ts.map +1 -1
- package/dist/collection-cell.js +24 -4
- package/dist/dialogs/dynamic-record.d.ts +12 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +21 -19
- package/package.json +3 -3
- package/src/__tests__/collection-cell.test.tsx +94 -0
- package/src/__tests__/structured-view-value.test.tsx +99 -0
- package/src/collection-cell.tsx +40 -3
- package/src/dialogs/dynamic-record.tsx +58 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.22.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 24cced0: The read-only record detail view now renders jsonb line-items with the same pro
|
|
8
|
+
rendering as the table instead of raw `JSON.stringify`. `CollectionCell` gains a
|
|
9
|
+
`variant?: 'badge' | 'inline'` prop (default `'badge'` = unchanged behaviour);
|
|
10
|
+
`'inline'` renders the mini-table / pair-list / scalar-list directly, with no
|
|
11
|
+
badge or popover, for the full-width detail dialog. The detail view's
|
|
12
|
+
`StructuredViewValue` delegates to `<CollectionCell variant="inline" …>`,
|
|
13
|
+
threading the field's `item_fields` schema plus locale + translator: an
|
|
14
|
+
`item_fields` schema drives localized headers + resolved ref labels (the
|
|
15
|
+
injected `{ value, label }` sibling — product name instead of the raw uuid),
|
|
16
|
+
and without a schema it falls back to a localized mini-table / pair list. The
|
|
17
|
+
"—" empty marker is preserved.
|
|
18
|
+
|
|
3
19
|
## 18.21.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
|
@@ -57,10 +57,25 @@ export interface CollectionCellProps {
|
|
|
57
57
|
* behaviour is unchanged.
|
|
58
58
|
*/
|
|
59
59
|
itemFields?: ItemField[];
|
|
60
|
+
/**
|
|
61
|
+
* Presentation mode.
|
|
62
|
+
* - `'badge'` (default): the compact count/preview badge that opens a
|
|
63
|
+
* popover with the mini-table / pair-list. Used in dense table cells.
|
|
64
|
+
* - `'inline'`: render the mini-table / pair-list DIRECTLY, with no badge
|
|
65
|
+
* or popover. Used by the read-only record detail view, which has full
|
|
66
|
+
* width and shows one field per row. All schema/locale logic is shared.
|
|
67
|
+
*/
|
|
68
|
+
variant?: 'badge' | 'inline';
|
|
60
69
|
}
|
|
61
70
|
/**
|
|
62
71
|
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
63
72
|
* compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
|
|
73
|
+
*
|
|
74
|
+
* `variant` selects the surface: the default `'badge'` shows a compact trigger
|
|
75
|
+
* + popover (dense table cells); `'inline'` renders the mini-table / pair-list
|
|
76
|
+
* directly for the full-width record detail view. Both paths share the
|
|
77
|
+
* itemFields schema (localized headers + resolved ref labels) and the
|
|
78
|
+
* locale-aware generic fallback.
|
|
64
79
|
*/
|
|
65
|
-
export declare function CollectionCell({ value, maxInline, locale, t, itemFields, }: CollectionCellProps): React.JSX.Element;
|
|
80
|
+
export declare function CollectionCell({ value, maxInline, locale, t, itemFields, variant, }: CollectionCellProps): React.JSX.Element;
|
|
66
81
|
//# sourceMappingURL=collection-cell.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-cell.d.ts","sourceRoot":"","sources":["../src/collection-cell.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAoB9B,sFAAsF;AACtF,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,CAAA;AAE9D;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACtB,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAA;IACX,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAA;CACf;AAmGD;;;;;;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;AAiMD,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;IACb;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;
|
|
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;AAE9D;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACtB,qEAAqE;IACrE,GAAG,EAAE,MAAM,CAAA;IACX,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAA;IACb,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAA;CACf;AAmGD;;;;;;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;AAiMD,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;IACb;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAA;CAC/B;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAa,EACb,MAAM,EACN,CAAC,EACD,UAAU,EACV,OAAiB,GACpB,EAAE,mBAAmB,qBAyHrB"}
|
package/dist/collection-cell.js
CHANGED
|
@@ -222,9 +222,16 @@ function PopoverShell({ label, title, children, icon = true, }) {
|
|
|
222
222
|
/**
|
|
223
223
|
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
224
224
|
* compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
|
|
225
|
+
*
|
|
226
|
+
* `variant` selects the surface: the default `'badge'` shows a compact trigger
|
|
227
|
+
* + popover (dense table cells); `'inline'` renders the mini-table / pair-list
|
|
228
|
+
* directly for the full-width record detail view. Both paths share the
|
|
229
|
+
* itemFields schema (localized headers + resolved ref labels) and the
|
|
230
|
+
* locale-aware generic fallback.
|
|
225
231
|
*/
|
|
226
|
-
export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, }) {
|
|
232
|
+
export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, variant = 'badge', }) {
|
|
227
233
|
const parsed = parseValue(value);
|
|
234
|
+
const inline = variant === 'inline';
|
|
228
235
|
// Empty-ish → muted dash.
|
|
229
236
|
if (parsed === null ||
|
|
230
237
|
parsed === undefined ||
|
|
@@ -243,6 +250,11 @@ export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, })
|
|
|
243
250
|
const allObjects = parsed.every((item) => isPlainObject(item));
|
|
244
251
|
if (allObjects) {
|
|
245
252
|
const rows = parsed;
|
|
253
|
+
// Inline mode (detail view): render the mini-table directly, no
|
|
254
|
+
// badge/popover. The same schema-driven path applies.
|
|
255
|
+
if (inline) {
|
|
256
|
+
return (_jsx(MiniTable, { rows: rows, locale: locale, t: t, itemFields: itemFields }));
|
|
257
|
+
}
|
|
246
258
|
const count = rows.length;
|
|
247
259
|
const label = countLabel(count, locale, t);
|
|
248
260
|
const hasSchema = !!(itemFields && itemFields.length > 0);
|
|
@@ -262,7 +274,11 @@ export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, })
|
|
|
262
274
|
.join(' | ');
|
|
263
275
|
return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows, locale: locale, t: t, itemFields: itemFields }) }));
|
|
264
276
|
}
|
|
265
|
-
// Array of scalars (or mixed)
|
|
277
|
+
// Array of scalars (or mixed). Inline mode renders the full list; badge
|
|
278
|
+
// mode previews the first N joined with a "+N" overflow trigger.
|
|
279
|
+
if (inline) {
|
|
280
|
+
return _jsx(ScalarList, { values: parsed });
|
|
281
|
+
}
|
|
266
282
|
const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ');
|
|
267
283
|
const overflow = parsed.length - maxInline;
|
|
268
284
|
const label = overflow > 0 ? `${preview} +${overflow}` : preview;
|
|
@@ -271,12 +287,16 @@ export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, })
|
|
|
271
287
|
}
|
|
272
288
|
// PLAIN OBJECT -----------------------------------------------------------
|
|
273
289
|
const entries = Object.entries(parsed);
|
|
274
|
-
|
|
290
|
+
// Inline mode renders the full key→value pair list directly.
|
|
291
|
+
if (inline) {
|
|
292
|
+
return _jsx(PairList, { entries: entries, locale: locale, t: t });
|
|
293
|
+
}
|
|
294
|
+
const previewPairs = entries
|
|
275
295
|
.slice(0, maxInline)
|
|
276
296
|
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
277
297
|
.join(', ');
|
|
278
298
|
const overflow = entries.length - maxInline;
|
|
279
|
-
const label = overflow > 0 ? `${
|
|
299
|
+
const label = overflow > 0 ? `${previewPairs} +${overflow}` : previewPairs;
|
|
280
300
|
const title = entries
|
|
281
301
|
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
282
302
|
.join(', ');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModelSchema } from './types';
|
|
2
|
+
import { type ItemField } from '../collection-cell';
|
|
2
3
|
import { type GetImageUrl } from '../image-url-context';
|
|
3
4
|
export type { GetImageUrl };
|
|
4
5
|
export interface FieldOption {
|
|
@@ -58,6 +59,17 @@ export interface FieldDef {
|
|
|
58
59
|
* over the org fallback.
|
|
59
60
|
*/
|
|
60
61
|
styleConfig?: Record<string, any>;
|
|
62
|
+
/**
|
|
63
|
+
* Declared schema for a jsonb line-items field (kernel v3 `item_fields`).
|
|
64
|
+
* The backend serves this on modal/detail fields the same way it does on
|
|
65
|
+
* table columns. When present the read-only detail view renders the
|
|
66
|
+
* `CollectionCell` mini-table with these (already-localized) headers in
|
|
67
|
+
* order and resolves `ref` columns to the backend-injected sibling label.
|
|
68
|
+
* Tolerates the snake_case `item_fields` the kernel serves.
|
|
69
|
+
*/
|
|
70
|
+
itemFields?: ItemField[];
|
|
71
|
+
/** snake_case alias served by the kernel for `itemFields`. */
|
|
72
|
+
item_fields?: ItemField[];
|
|
61
73
|
}
|
|
62
74
|
export interface DynamicRecordDialogProps {
|
|
63
75
|
open: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
|
|
@@ -27,6 +27,7 @@ import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
|
|
|
27
27
|
import { DynamicIcon, isLucideIconName } from '../dynamic-icon';
|
|
28
28
|
import { humanizeToken } from '../dynamic-columns-helpers';
|
|
29
29
|
import { formatDateCell } from '../dynamic-columns';
|
|
30
|
+
import { CollectionCell } from '../collection-cell';
|
|
30
31
|
import { ImageUrlContext, identityImageUrl } from '../image-url-context';
|
|
31
32
|
import { TimeZoneContext, CurrencyContext } from '../org-runtime-context';
|
|
32
33
|
// localizedModelName resolves the (possibly addon-i18n) model name: prefer the
|
|
@@ -496,7 +497,7 @@ function RelationViewValue({ field, value, record }) {
|
|
|
496
497
|
return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(OptionLead, { option: lead, size: 24 }), _jsx("span", { className: "text-sm", children: label ?? '—' })] }));
|
|
497
498
|
}
|
|
498
499
|
export function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, currency: currencyProp, }) {
|
|
499
|
-
const { i18n } = useTranslation();
|
|
500
|
+
const { t, i18n } = useTranslation();
|
|
500
501
|
const ctxImageUrl = useContext(ImageUrlContext);
|
|
501
502
|
const ctxTimeZone = useContext(TimeZoneContext);
|
|
502
503
|
const ctxCurrency = useContext(CurrencyContext);
|
|
@@ -592,7 +593,7 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
592
593
|
// to surface — render readable key/value pairs instead of falling through to
|
|
593
594
|
// String(value) ("[object Object]").
|
|
594
595
|
if (value !== null && typeof value === 'object') {
|
|
595
|
-
return _jsx(StructuredViewValue, { value: value });
|
|
596
|
+
return (_jsx(StructuredViewValue, { value: value, field: field, locale: i18n.language, t: t }));
|
|
596
597
|
}
|
|
597
598
|
const display = formatDisplayValue(value, field);
|
|
598
599
|
if (field.type === 'textarea') {
|
|
@@ -606,25 +607,26 @@ export function ViewValue({ field, value: rawValue, record, getImageUrl: getImag
|
|
|
606
607
|
function IconNameViewValue({ name }) {
|
|
607
608
|
return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx("div", { className: "h-8 w-8 flex items-center justify-center rounded bg-muted", children: _jsx(DynamicIcon, { name: name, className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm text-muted-foreground", children: name })] }));
|
|
608
609
|
}
|
|
609
|
-
// StructuredViewValue renders a jsonb object/array that has no resolvable label
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
610
|
+
// StructuredViewValue renders a jsonb object/array that has no resolvable label.
|
|
611
|
+
// It delegates to the shared `CollectionCell` in `'inline'` mode so the detail
|
|
612
|
+
// view gets the SAME pro rendering as the table: a declared `item_fields` schema
|
|
613
|
+
// drives localized headers + resolved ref labels (the injected `{value,label}`
|
|
614
|
+
// sibling) for line-items; without a schema it falls back to a localized
|
|
615
|
+
// key→value pair list / mini-table — never raw `JSON.stringify`. Empty arrays /
|
|
616
|
+
// empty objects keep the "—" marker (CollectionCell renders a muted dash, which
|
|
617
|
+
// we normalize to the em-dash the detail view uses elsewhere).
|
|
618
|
+
function StructuredViewValue({ value, field, locale, t, }) {
|
|
619
|
+
const isEmpty = value === null ||
|
|
620
|
+
value === undefined ||
|
|
621
|
+
value === '' ||
|
|
622
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
623
|
+
(typeof value === 'object' &&
|
|
624
|
+
!Array.isArray(value) &&
|
|
625
|
+
Object.keys(value).length === 0);
|
|
626
|
+
if (isEmpty) {
|
|
625
627
|
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
626
628
|
}
|
|
627
|
-
return (_jsx("
|
|
629
|
+
return (_jsx("div", { className: "text-sm py-1", children: _jsx(CollectionCell, { value: value, itemFields: field?.itemFields ?? field?.item_fields, variant: "inline", locale: locale, t: t }) }));
|
|
628
630
|
}
|
|
629
631
|
function EditField({ field, value, onChange }) {
|
|
630
632
|
if (field.type === 'boolean') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.22.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"typescript": "^6.0.0",
|
|
65
65
|
"vitest": "^4.0.0",
|
|
66
66
|
"zustand": "^5.0.0",
|
|
67
|
-
"@asteby/metacore-
|
|
68
|
-
"@asteby/metacore-
|
|
67
|
+
"@asteby/metacore-ui": "2.5.2",
|
|
68
|
+
"@asteby/metacore-sdk": "3.2.0"
|
|
69
69
|
},
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "tsc -p tsconfig.json",
|
|
@@ -329,3 +329,97 @@ describe('CollectionCell with itemFields schema', () => {
|
|
|
329
329
|
expect(title).toContain('Cantidad: 2')
|
|
330
330
|
})
|
|
331
331
|
})
|
|
332
|
+
|
|
333
|
+
describe('CollectionCell variant="inline" (detail view)', () => {
|
|
334
|
+
const itemFields = [
|
|
335
|
+
{ key: 'product_id', label: 'Producto', ref: 'Product' },
|
|
336
|
+
{ key: 'quantity', label: 'Cantidad' },
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
it('renders the mini-table DIRECTLY with no popover trigger / badge', () => {
|
|
340
|
+
render(
|
|
341
|
+
<CollectionCell
|
|
342
|
+
variant="inline"
|
|
343
|
+
value={[{ product_id: 'a', quantity: 2 }]}
|
|
344
|
+
/>
|
|
345
|
+
)
|
|
346
|
+
// The table is in the DOM immediately — no click needed.
|
|
347
|
+
expect(screen.getByRole('table')).toBeTruthy()
|
|
348
|
+
// No count-badge trigger ("1 item") is rendered in inline mode.
|
|
349
|
+
expect(screen.queryByText('1 item')).toBeNull()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('uses localized schema headers + resolved ref labels (no raw uuid/JSON)', () => {
|
|
353
|
+
const { container } = render(
|
|
354
|
+
<CollectionCell
|
|
355
|
+
variant="inline"
|
|
356
|
+
locale="es"
|
|
357
|
+
itemFields={itemFields}
|
|
358
|
+
value={[
|
|
359
|
+
{
|
|
360
|
+
product_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
361
|
+
product: { value: 'x', label: 'Test' },
|
|
362
|
+
quantity: 10,
|
|
363
|
+
},
|
|
364
|
+
]}
|
|
365
|
+
/>
|
|
366
|
+
)
|
|
367
|
+
// "Producto | Cantidad / Test | 10"
|
|
368
|
+
expect(screen.getByRole('columnheader', { name: 'Producto' })).toBeTruthy()
|
|
369
|
+
expect(screen.getByRole('columnheader', { name: 'Cantidad' })).toBeTruthy()
|
|
370
|
+
expect(screen.getByRole('cell', { name: 'Test' })).toBeTruthy()
|
|
371
|
+
expect(screen.getByRole('cell', { name: '10' })).toBeTruthy()
|
|
372
|
+
// The raw uuid must not leak, and there is no JSON.stringify <pre> block.
|
|
373
|
+
expect(screen.queryByText('550e8400…')).toBeNull()
|
|
374
|
+
expect(container.querySelector('pre')).toBeNull()
|
|
375
|
+
expect(container.textContent).not.toContain('550e8400-e29b')
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('falls back to a generic localized mini-table when no itemFields (no raw JSON)', () => {
|
|
379
|
+
const { container } = render(
|
|
380
|
+
<CollectionCell
|
|
381
|
+
variant="inline"
|
|
382
|
+
locale="es"
|
|
383
|
+
value={[{ product_id: 'abc', quantity: 2 }]}
|
|
384
|
+
/>
|
|
385
|
+
)
|
|
386
|
+
// Generic dict path still drives the header; no JSON dump.
|
|
387
|
+
expect(screen.getByRole('columnheader', { name: 'Producto' })).toBeTruthy()
|
|
388
|
+
expect(screen.getByRole('cell', { name: '2' })).toBeTruthy()
|
|
389
|
+
expect(container.querySelector('pre')).toBeNull()
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('renders a plain object as an inline localized pair list (no popover)', () => {
|
|
393
|
+
render(
|
|
394
|
+
<CollectionCell
|
|
395
|
+
variant="inline"
|
|
396
|
+
locale="es"
|
|
397
|
+
value={{ price: 10, quantity: 20 }}
|
|
398
|
+
/>
|
|
399
|
+
)
|
|
400
|
+
// Pair list is rendered directly; the badge "+N" preview is not used.
|
|
401
|
+
expect(screen.getByText('Precio:')).toBeTruthy()
|
|
402
|
+
expect(screen.getByText('Cantidad:')).toBeTruthy()
|
|
403
|
+
expect(screen.queryByRole('table')).toBeNull()
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('renders a scalar array as an inline list directly', () => {
|
|
407
|
+
render(
|
|
408
|
+
<CollectionCell variant="inline" value={['a', 'b', 'c', 'd', 'e']} />
|
|
409
|
+
)
|
|
410
|
+
// Full list, no "+2" overflow badge.
|
|
411
|
+
expect(screen.getByText('e')).toBeTruthy()
|
|
412
|
+
expect(screen.queryByText('a, b, c +2')).toBeNull()
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('keeps the muted dash for empty / null values', () => {
|
|
416
|
+
const { container: empty } = render(
|
|
417
|
+
<CollectionCell variant="inline" value={[]} />
|
|
418
|
+
)
|
|
419
|
+
expect(empty.textContent).toBe('-')
|
|
420
|
+
const { container: nul } = render(
|
|
421
|
+
<CollectionCell variant="inline" value={null} />
|
|
422
|
+
)
|
|
423
|
+
expect(nul.textContent).toBe('-')
|
|
424
|
+
})
|
|
425
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
//
|
|
3
|
+
// Detail-view wiring: the read-only "Información detallada del registro" dialog
|
|
4
|
+
// renders jsonb line-items through `ViewValue` → `StructuredViewValue` →
|
|
5
|
+
// `CollectionCell variant="inline"`. This locks in the regression fix: the
|
|
6
|
+
// detail view no longer dumps raw `JSON.stringify`, and an `item_fields` schema
|
|
7
|
+
// drives localized headers + resolved ref labels (the injected `{value,label}`
|
|
8
|
+
// sibling) — "Producto | Cantidad / Test | 10".
|
|
9
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
10
|
+
import { cleanup, render, screen } from '@testing-library/react'
|
|
11
|
+
|
|
12
|
+
// Identity translator + a Spanish locale, matching the host dialog.
|
|
13
|
+
vi.mock('react-i18next', () => ({
|
|
14
|
+
useTranslation: () => ({ t: (k: string) => k, i18n: { language: 'es' } }),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
import { ViewValue } from '../dialogs/dynamic-record'
|
|
18
|
+
|
|
19
|
+
afterEach(cleanup)
|
|
20
|
+
|
|
21
|
+
describe('detail-view jsonb line-items (ViewValue → inline CollectionCell)', () => {
|
|
22
|
+
const itemFields = [
|
|
23
|
+
{ key: 'product_id', label: 'Producto', ref: 'Product' },
|
|
24
|
+
{ key: 'quantity', label: 'Cantidad' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
it('renders the schema mini-table with resolved ref labels, not raw JSON', () => {
|
|
28
|
+
const { container } = render(
|
|
29
|
+
<ViewValue
|
|
30
|
+
field={{ key: 'items', label: 'Items', type: 'json', itemFields }}
|
|
31
|
+
value={[
|
|
32
|
+
{
|
|
33
|
+
product_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
34
|
+
product: { value: 'x', label: 'Test' },
|
|
35
|
+
quantity: 10,
|
|
36
|
+
},
|
|
37
|
+
]}
|
|
38
|
+
record={{}}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
// Localized headers from the schema (verbatim) + resolved ref value.
|
|
42
|
+
expect(screen.getByRole('columnheader', { name: 'Producto' })).toBeTruthy()
|
|
43
|
+
expect(screen.getByRole('columnheader', { name: 'Cantidad' })).toBeTruthy()
|
|
44
|
+
expect(screen.getByRole('cell', { name: 'Test' })).toBeTruthy()
|
|
45
|
+
expect(screen.getByRole('cell', { name: '10' })).toBeTruthy()
|
|
46
|
+
// No raw JSON dump, no leaked uuid.
|
|
47
|
+
expect(container.querySelector('pre')).toBeNull()
|
|
48
|
+
expect(container.textContent).not.toContain('550e8400-e29b')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('tolerates the snake_case item_fields alias', () => {
|
|
52
|
+
render(
|
|
53
|
+
<ViewValue
|
|
54
|
+
field={{ key: 'items', label: 'Items', type: 'json', item_fields: itemFields }}
|
|
55
|
+
value={[
|
|
56
|
+
{ product_id: 'a', product: { value: 'a', label: 'Aceite' }, quantity: 3 },
|
|
57
|
+
]}
|
|
58
|
+
record={{}}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
expect(screen.getByRole('cell', { name: 'Aceite' })).toBeTruthy()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('falls back to a generic localized mini-table when no schema (no raw JSON)', () => {
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<ViewValue
|
|
67
|
+
field={{ key: 'items', label: 'Items', type: 'json' }}
|
|
68
|
+
value={[{ product_id: 'abc', quantity: 2 }]}
|
|
69
|
+
record={{}}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
// product_id → "Producto" via the built-in es dictionary; no <pre>.
|
|
73
|
+
expect(screen.getByRole('columnheader', { name: 'Producto' })).toBeTruthy()
|
|
74
|
+
expect(container.querySelector('pre')).toBeNull()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('renders a plain jsonb object as a localized pair list (not [object Object])', () => {
|
|
78
|
+
const { container } = render(
|
|
79
|
+
<ViewValue
|
|
80
|
+
field={{ key: 'fiscal_data', label: 'Fiscal', type: 'json' }}
|
|
81
|
+
value={{ price: 10, quantity: 20 }}
|
|
82
|
+
record={{}}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
expect(screen.getByText('Precio:')).toBeTruthy()
|
|
86
|
+
expect(container.textContent).not.toContain('[object Object]')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('keeps the "—" empty marker for an empty array', () => {
|
|
90
|
+
const { container } = render(
|
|
91
|
+
<ViewValue
|
|
92
|
+
field={{ key: 'items', label: 'Items', type: 'json' }}
|
|
93
|
+
value={[]}
|
|
94
|
+
record={{}}
|
|
95
|
+
/>
|
|
96
|
+
)
|
|
97
|
+
expect(container.textContent).toContain('—')
|
|
98
|
+
})
|
|
99
|
+
})
|
package/src/collection-cell.tsx
CHANGED
|
@@ -421,11 +421,26 @@ export interface CollectionCellProps {
|
|
|
421
421
|
* behaviour is unchanged.
|
|
422
422
|
*/
|
|
423
423
|
itemFields?: ItemField[]
|
|
424
|
+
/**
|
|
425
|
+
* Presentation mode.
|
|
426
|
+
* - `'badge'` (default): the compact count/preview badge that opens a
|
|
427
|
+
* popover with the mini-table / pair-list. Used in dense table cells.
|
|
428
|
+
* - `'inline'`: render the mini-table / pair-list DIRECTLY, with no badge
|
|
429
|
+
* or popover. Used by the read-only record detail view, which has full
|
|
430
|
+
* width and shows one field per row. All schema/locale logic is shared.
|
|
431
|
+
*/
|
|
432
|
+
variant?: 'badge' | 'inline'
|
|
424
433
|
}
|
|
425
434
|
|
|
426
435
|
/**
|
|
427
436
|
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
428
437
|
* compact, dark-mode friendly, locale-aware. Never throws on unexpected shapes.
|
|
438
|
+
*
|
|
439
|
+
* `variant` selects the surface: the default `'badge'` shows a compact trigger
|
|
440
|
+
* + popover (dense table cells); `'inline'` renders the mini-table / pair-list
|
|
441
|
+
* directly for the full-width record detail view. Both paths share the
|
|
442
|
+
* itemFields schema (localized headers + resolved ref labels) and the
|
|
443
|
+
* locale-aware generic fallback.
|
|
429
444
|
*/
|
|
430
445
|
export function CollectionCell({
|
|
431
446
|
value,
|
|
@@ -433,8 +448,10 @@ export function CollectionCell({
|
|
|
433
448
|
locale,
|
|
434
449
|
t,
|
|
435
450
|
itemFields,
|
|
451
|
+
variant = 'badge',
|
|
436
452
|
}: CollectionCellProps) {
|
|
437
453
|
const parsed = parseValue(value)
|
|
454
|
+
const inline = variant === 'inline'
|
|
438
455
|
|
|
439
456
|
// Empty-ish → muted dash.
|
|
440
457
|
if (
|
|
@@ -465,6 +482,18 @@ export function CollectionCell({
|
|
|
465
482
|
const allObjects = parsed.every((item) => isPlainObject(item))
|
|
466
483
|
if (allObjects) {
|
|
467
484
|
const rows = parsed as Record<string, unknown>[]
|
|
485
|
+
// Inline mode (detail view): render the mini-table directly, no
|
|
486
|
+
// badge/popover. The same schema-driven path applies.
|
|
487
|
+
if (inline) {
|
|
488
|
+
return (
|
|
489
|
+
<MiniTable
|
|
490
|
+
rows={rows}
|
|
491
|
+
locale={locale}
|
|
492
|
+
t={t}
|
|
493
|
+
itemFields={itemFields}
|
|
494
|
+
/>
|
|
495
|
+
)
|
|
496
|
+
}
|
|
468
497
|
const count = rows.length
|
|
469
498
|
const label = countLabel(count, locale, t)
|
|
470
499
|
const hasSchema = !!(itemFields && itemFields.length > 0)
|
|
@@ -504,7 +533,11 @@ export function CollectionCell({
|
|
|
504
533
|
)
|
|
505
534
|
}
|
|
506
535
|
|
|
507
|
-
// Array of scalars (or mixed)
|
|
536
|
+
// Array of scalars (or mixed). Inline mode renders the full list; badge
|
|
537
|
+
// mode previews the first N joined with a "+N" overflow trigger.
|
|
538
|
+
if (inline) {
|
|
539
|
+
return <ScalarList values={parsed} />
|
|
540
|
+
}
|
|
508
541
|
const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ')
|
|
509
542
|
const overflow = parsed.length - maxInline
|
|
510
543
|
const label =
|
|
@@ -519,12 +552,16 @@ export function CollectionCell({
|
|
|
519
552
|
|
|
520
553
|
// PLAIN OBJECT -----------------------------------------------------------
|
|
521
554
|
const entries = Object.entries(parsed)
|
|
522
|
-
|
|
555
|
+
// Inline mode renders the full key→value pair list directly.
|
|
556
|
+
if (inline) {
|
|
557
|
+
return <PairList entries={entries} locale={locale} t={t} />
|
|
558
|
+
}
|
|
559
|
+
const previewPairs = entries
|
|
523
560
|
.slice(0, maxInline)
|
|
524
561
|
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
525
562
|
.join(', ')
|
|
526
563
|
const overflow = entries.length - maxInline
|
|
527
|
-
const label = overflow > 0 ? `${
|
|
564
|
+
const label = overflow > 0 ? `${previewPairs} +${overflow}` : previewPairs
|
|
528
565
|
const title = entries
|
|
529
566
|
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
530
567
|
.join(', ')
|
|
@@ -56,6 +56,7 @@ import { isNilUuid, normalizeNilUuid } from '../nil-uuid'
|
|
|
56
56
|
import { DynamicIcon, isLucideIconName } from '../dynamic-icon'
|
|
57
57
|
import { humanizeToken } from '../dynamic-columns-helpers'
|
|
58
58
|
import { formatDateCell } from '../dynamic-columns'
|
|
59
|
+
import { CollectionCell, type ItemField } from '../collection-cell'
|
|
59
60
|
import type { ActionFieldDef, RelationMeta } from '../types'
|
|
60
61
|
import { ImageUrlContext, identityImageUrl, type GetImageUrl } from '../image-url-context'
|
|
61
62
|
import { TimeZoneContext, CurrencyContext } from '../org-runtime-context'
|
|
@@ -122,6 +123,17 @@ export interface FieldDef {
|
|
|
122
123
|
* over the org fallback.
|
|
123
124
|
*/
|
|
124
125
|
styleConfig?: Record<string, any>
|
|
126
|
+
/**
|
|
127
|
+
* Declared schema for a jsonb line-items field (kernel v3 `item_fields`).
|
|
128
|
+
* The backend serves this on modal/detail fields the same way it does on
|
|
129
|
+
* table columns. When present the read-only detail view renders the
|
|
130
|
+
* `CollectionCell` mini-table with these (already-localized) headers in
|
|
131
|
+
* order and resolves `ref` columns to the backend-injected sibling label.
|
|
132
|
+
* Tolerates the snake_case `item_fields` the kernel serves.
|
|
133
|
+
*/
|
|
134
|
+
itemFields?: ItemField[]
|
|
135
|
+
/** snake_case alias served by the kernel for `itemFields`. */
|
|
136
|
+
item_fields?: ItemField[]
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
// Permissive shape: the wire payload may omit some fields (e.g. `title` is
|
|
@@ -910,7 +922,7 @@ export function ViewValue({
|
|
|
910
922
|
/** Optional override; when omitted falls back to the nearest provider. */
|
|
911
923
|
currency?: string
|
|
912
924
|
}) {
|
|
913
|
-
const { i18n } = useTranslation()
|
|
925
|
+
const { t, i18n } = useTranslation()
|
|
914
926
|
const ctxImageUrl = useContext(ImageUrlContext)
|
|
915
927
|
const ctxTimeZone = useContext(TimeZoneContext)
|
|
916
928
|
const ctxCurrency = useContext(CurrencyContext)
|
|
@@ -1069,7 +1081,14 @@ export function ViewValue({
|
|
|
1069
1081
|
// to surface — render readable key/value pairs instead of falling through to
|
|
1070
1082
|
// String(value) ("[object Object]").
|
|
1071
1083
|
if (value !== null && typeof value === 'object') {
|
|
1072
|
-
return
|
|
1084
|
+
return (
|
|
1085
|
+
<StructuredViewValue
|
|
1086
|
+
value={value}
|
|
1087
|
+
field={field}
|
|
1088
|
+
locale={i18n.language}
|
|
1089
|
+
t={t}
|
|
1090
|
+
/>
|
|
1091
|
+
)
|
|
1073
1092
|
}
|
|
1074
1093
|
|
|
1075
1094
|
const display = formatDisplayValue(value, field)
|
|
@@ -1099,41 +1118,46 @@ function IconNameViewValue({ name }: { name: string }) {
|
|
|
1099
1118
|
)
|
|
1100
1119
|
}
|
|
1101
1120
|
|
|
1102
|
-
// StructuredViewValue renders a jsonb object/array that has no resolvable label
|
|
1103
|
-
//
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1121
|
+
// StructuredViewValue renders a jsonb object/array that has no resolvable label.
|
|
1122
|
+
// It delegates to the shared `CollectionCell` in `'inline'` mode so the detail
|
|
1123
|
+
// view gets the SAME pro rendering as the table: a declared `item_fields` schema
|
|
1124
|
+
// drives localized headers + resolved ref labels (the injected `{value,label}`
|
|
1125
|
+
// sibling) for line-items; without a schema it falls back to a localized
|
|
1126
|
+
// key→value pair list / mini-table — never raw `JSON.stringify`. Empty arrays /
|
|
1127
|
+
// empty objects keep the "—" marker (CollectionCell renders a muted dash, which
|
|
1128
|
+
// we normalize to the em-dash the detail view uses elsewhere).
|
|
1129
|
+
function StructuredViewValue({
|
|
1130
|
+
value,
|
|
1131
|
+
field,
|
|
1132
|
+
locale,
|
|
1133
|
+
t,
|
|
1134
|
+
}: {
|
|
1135
|
+
value: any
|
|
1136
|
+
field?: FieldDef
|
|
1137
|
+
locale?: string
|
|
1138
|
+
t?: (key: string, options?: any) => string
|
|
1139
|
+
}) {
|
|
1140
|
+
const isEmpty =
|
|
1141
|
+
value === null ||
|
|
1142
|
+
value === undefined ||
|
|
1143
|
+
value === '' ||
|
|
1144
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
1145
|
+
(typeof value === 'object' &&
|
|
1146
|
+
!Array.isArray(value) &&
|
|
1147
|
+
Object.keys(value).length === 0)
|
|
1148
|
+
if (isEmpty) {
|
|
1124
1149
|
return <p className="text-sm py-1 text-muted-foreground">—</p>
|
|
1125
1150
|
}
|
|
1126
1151
|
return (
|
|
1127
|
-
<
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
</dl>
|
|
1152
|
+
<div className="text-sm py-1">
|
|
1153
|
+
<CollectionCell
|
|
1154
|
+
value={value}
|
|
1155
|
+
itemFields={field?.itemFields ?? field?.item_fields}
|
|
1156
|
+
variant="inline"
|
|
1157
|
+
locale={locale}
|
|
1158
|
+
t={t}
|
|
1159
|
+
/>
|
|
1160
|
+
</div>
|
|
1137
1161
|
)
|
|
1138
1162
|
}
|
|
1139
1163
|
|