@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 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;CAC3B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAa,EACb,MAAM,EACN,CAAC,EACD,UAAU,GACb,EAAE,mBAAmB,qBAoGrB"}
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"}
@@ -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): preview first N joined, "+N" overflow.
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
- const inline = entries
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 ? `${inline} +${overflow}` : inline;
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;AA8C1C,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAK1F,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;IACpH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACpC;AAiCD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;2DAEuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAwID,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAUjE;AAED,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,aAAa,EACb,WAA8B,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACX,EAAE,wBAAwB,+BAuY1B;AAgGD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,KAAK,EAAE,QAAQ,EACf,MAAM,EACN,WAAW,EAAE,eAAe,EAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,GACzB,EAAE;IACC,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,EAAE,GAAG,CAAA;IACX,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,+BA8KA"}
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;AA6C1C,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnE,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAK1F,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;IACpH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,SAAS,EAAE,CAAA;CAC5B;AAiCD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;2DAEuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAwID,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAUjE;AAED,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,aAAa,EACb,WAA8B,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACX,EAAE,wBAAwB,+BAuY1B;AAgGD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,KAAK,EAAE,QAAQ,EACf,MAAM,EACN,WAAW,EAAE,eAAe,EAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,GACzB,EAAE;IACC,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,EAAE,GAAG,CAAA;IACX,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,+BAqLA"}
@@ -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
- // plain objects become a key→value list (keys humanized), primitive arrays a
611
- // comma-joined line, and anything deeper a pretty-printed JSON block. Empty
612
- // structures render the same "—" marker as null scalars.
613
- function StructuredViewValue({ value }) {
614
- if (Array.isArray(value)) {
615
- if (value.length === 0) {
616
- return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
617
- }
618
- if (value.every(v => v === null || typeof v !== 'object')) {
619
- return _jsx("p", { className: "text-sm py-1", children: value.map(v => String(v ?? '—')).join(', ') });
620
- }
621
- return (_jsx("pre", { className: "text-xs whitespace-pre-wrap rounded-md bg-muted/40 p-3 overflow-x-auto", children: JSON.stringify(value, null, 2) }));
622
- }
623
- const entries = Object.entries(value).filter(([, v]) => v !== null && v !== undefined && v !== '');
624
- if (entries.length === 0) {
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("dl", { className: "text-sm py-1 space-y-0.5", children: entries.map(([k, v]) => (_jsxs("div", { className: "flex gap-2", children: [_jsxs("dt", { className: "text-muted-foreground shrink-0", children: [humanizeToken(k), ":"] }), _jsx("dd", { className: "break-words", children: typeof v === 'object' ? JSON.stringify(v) : String(v) })] }, k))) }));
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.21.0",
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-sdk": "3.2.0",
68
- "@asteby/metacore-ui": "2.5.2"
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
+ })
@@ -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): preview first N joined, "+N" overflow.
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
- const inline = entries
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 ? `${inline} +${overflow}` : inline
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 <StructuredViewValue value={value} />
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
- // plain objects become a key→value list (keys humanized), primitive arrays a
1104
- // comma-joined line, and anything deeper a pretty-printed JSON block. Empty
1105
- // structures render the same "—" marker as null scalars.
1106
- function StructuredViewValue({ value }: { value: any }) {
1107
- if (Array.isArray(value)) {
1108
- if (value.length === 0) {
1109
- return <p className="text-sm py-1 text-muted-foreground">—</p>
1110
- }
1111
- if (value.every(v => v === null || typeof v !== 'object')) {
1112
- return <p className="text-sm py-1">{value.map(v => String(v ?? '—')).join(', ')}</p>
1113
- }
1114
- return (
1115
- <pre className="text-xs whitespace-pre-wrap rounded-md bg-muted/40 p-3 overflow-x-auto">
1116
- {JSON.stringify(value, null, 2)}
1117
- </pre>
1118
- )
1119
- }
1120
- const entries = Object.entries(value).filter(
1121
- ([, v]) => v !== null && v !== undefined && v !== '',
1122
- )
1123
- if (entries.length === 0) {
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
- <dl className="text-sm py-1 space-y-0.5">
1128
- {entries.map(([k, v]) => (
1129
- <div key={k} className="flex gap-2">
1130
- <dt className="text-muted-foreground shrink-0">{humanizeToken(k)}:</dt>
1131
- <dd className="break-words">
1132
- {typeof v === 'object' ? JSON.stringify(v) : String(v)}
1133
- </dd>
1134
- </div>
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