@asteby/metacore-runtime-react 18.20.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 +31 -0
- package/dist/collection-cell.d.ts +43 -1
- package/dist/collection-cell.d.ts.map +1 -1
- package/dist/collection-cell.js +79 -11
- 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/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +1 -1
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/collection-cell.test.tsx +248 -1
- package/src/__tests__/structured-view-value.test.tsx +99 -0
- package/src/collection-cell.tsx +174 -14
- package/src/dialogs/dynamic-record.tsx +58 -34
- package/src/dynamic-columns.tsx +1 -0
- package/src/types.ts +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
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
|
+
|
|
19
|
+
## 18.21.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- 53950ed: CollectionCell renders jsonb line-items from a declared sub-field schema when
|
|
24
|
+
the column carries one (`col.itemFields` / snake `col.item_fields`, kernel v3
|
|
25
|
+
`item_fields`). Headers use the schema's already-localized `label` verbatim (in
|
|
26
|
+
the declared order, no prettify/translate); `ref` columns resolve to the
|
|
27
|
+
backend-injected sibling label — the FK key without `_id` (`product_id` →
|
|
28
|
+
`product`), else `<key>_label` — showing the resolved name instead of the raw
|
|
29
|
+
uuid (`{ value, label }` → `label`, bare string → itself, missing → truncated
|
|
30
|
+
uuid fallback). The badge count noun stays locale-aware. When no schema is
|
|
31
|
+
present the generic dict/prettify behaviour is unchanged. `itemFields` is
|
|
32
|
+
threaded from the dynamic columns factory callsite.
|
|
33
|
+
|
|
3
34
|
## 18.20.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
/** Host i18n translator (react-i18next `t`), as threaded into the columns factory. */
|
|
3
3
|
export type Translate = (key: string, options?: any) => string;
|
|
4
|
+
/**
|
|
5
|
+
* Declared schema for one column of a jsonb line-items array. Mirrors the
|
|
6
|
+
* kernel v3 `item_fields` entry the backend serves on the column metadata
|
|
7
|
+
* (`col.itemFields` / snake `col.item_fields`). When present it drives the
|
|
8
|
+
* popover mini-table: headers come from `label` (already LOCALIZED by the
|
|
9
|
+
* backend — never re-translated here) and `ref` columns resolve to the
|
|
10
|
+
* backend-injected sibling label instead of the raw uuid.
|
|
11
|
+
*/
|
|
12
|
+
export interface ItemField {
|
|
13
|
+
/** jsonb key this column maps to (e.g. `product_id`, `quantity`). */
|
|
14
|
+
key: string;
|
|
15
|
+
/** Header text — ALREADY localized by the backend. Used verbatim. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Declarative cell type hint (informational; not branched on today). */
|
|
18
|
+
type?: string;
|
|
19
|
+
/** FK target model. When set, the cell renders the resolved sibling label. */
|
|
20
|
+
ref?: string;
|
|
21
|
+
}
|
|
4
22
|
/**
|
|
5
23
|
* Localized key label for a popover column header. Resolution order:
|
|
6
24
|
* (a) host i18n `t(rawKey)` if it resolves to something ≠ rawKey;
|
|
@@ -30,10 +48,34 @@ export interface CollectionCellProps {
|
|
|
30
48
|
locale?: string;
|
|
31
49
|
/** Host i18n translator; takes precedence over the built-in dictionary. */
|
|
32
50
|
t?: Translate;
|
|
51
|
+
/**
|
|
52
|
+
* Declared schema for the jsonb line-items columns (kernel v3 `item_fields`,
|
|
53
|
+
* read from `col.itemFields ?? col.item_fields` at the callsite). When
|
|
54
|
+
* present AND the value is an array of objects, the popover mini-table uses
|
|
55
|
+
* these (already-localized) headers in order and resolves `ref` columns to
|
|
56
|
+
* the backend-injected sibling label. Absent → the generic dict/prettify
|
|
57
|
+
* behaviour is unchanged.
|
|
58
|
+
*/
|
|
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';
|
|
33
69
|
}
|
|
34
70
|
/**
|
|
35
71
|
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
36
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.
|
|
37
79
|
*/
|
|
38
|
-
export declare function CollectionCell({ value, maxInline, locale, t, }: CollectionCellProps): React.JSX.Element;
|
|
80
|
+
export declare function CollectionCell({ value, maxInline, locale, t, itemFields, variant, }: CollectionCellProps): React.JSX.Element;
|
|
39
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;
|
|
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
|
@@ -3,6 +3,36 @@ import { List } from 'lucide-react';
|
|
|
3
3
|
import { Badge, Popover, PopoverContent, PopoverTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, } from '@asteby/metacore-ui';
|
|
4
4
|
import { humanizeToken } from './dynamic-columns-helpers';
|
|
5
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
|
+
/**
|
|
7
|
+
* Resolves the backend-injected resolved sibling key for a ref item-field,
|
|
8
|
+
* mirroring `relationKeyFor` in dynamic-columns: the raw key with a trailing
|
|
9
|
+
* `_id` stripped (`product_id` → `product`), else `<key>_label`.
|
|
10
|
+
*/
|
|
11
|
+
function siblingKeyFor(key) {
|
|
12
|
+
return key.endsWith('_id') ? key.slice(0, -3) : `${key}_label`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Renders the cell value for one declared item-field of a jsonb row. For a
|
|
16
|
+
* `ref` field it prefers the backend-injected resolved sibling (the FK key
|
|
17
|
+
* without `_id`, else `<key>_label`): a `{ value, label }` object shows its
|
|
18
|
+
* `label`, a bare string shows itself; absent → the raw value via
|
|
19
|
+
* `formatScalar` (truncated uuid). Non-ref fields render `formatScalar(value)`.
|
|
20
|
+
*/
|
|
21
|
+
function renderItemFieldValue(field, row) {
|
|
22
|
+
if (field.ref) {
|
|
23
|
+
const sibling = row[siblingKeyFor(field.key)];
|
|
24
|
+
if (sibling && typeof sibling === 'object' && !Array.isArray(sibling)) {
|
|
25
|
+
const label = sibling.label;
|
|
26
|
+
if (label !== undefined && label !== null && label !== '') {
|
|
27
|
+
return String(label);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (typeof sibling === 'string' && sibling !== '') {
|
|
31
|
+
return sibling;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return formatScalar(row[field.key]);
|
|
35
|
+
}
|
|
6
36
|
/** Normalize an org/UI language tag to a base language code (`es-MX` → `es`). */
|
|
7
37
|
function baseLang(locale) {
|
|
8
38
|
return (locale || 'en').toLowerCase().split('-')[0];
|
|
@@ -164,7 +194,15 @@ function unionKeys(rows) {
|
|
|
164
194
|
return seen;
|
|
165
195
|
}
|
|
166
196
|
const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0';
|
|
167
|
-
function MiniTable({ rows, locale, t, }) {
|
|
197
|
+
function MiniTable({ rows, locale, t, itemFields, }) {
|
|
198
|
+
// Schema-driven path: a declared `item_fields` schema fixes the column
|
|
199
|
+
// order + headers (already localized by the backend, used VERBATIM) and
|
|
200
|
+
// resolves ref columns to the injected sibling label instead of the raw
|
|
201
|
+
// uuid. Sibling/raw keys not covered by the schema are dropped from the
|
|
202
|
+
// table (the schema is the source of truth for what to surface).
|
|
203
|
+
if (itemFields && itemFields.length > 0) {
|
|
204
|
+
return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: itemFields.map((field) => (_jsx(TableHead, { className: "text-xs whitespace-nowrap", children: field.label }, field.key))) }) }), _jsx(TableBody, { children: rows.map((row, i) => (_jsx(TableRow, { children: itemFields.map((field) => (_jsx(TableCell, { className: "text-xs whitespace-nowrap", children: renderItemFieldValue(field, row) }, field.key))) }, i))) })] }));
|
|
205
|
+
}
|
|
168
206
|
const keys = unionKeys(rows);
|
|
169
207
|
if (keys.length === 0) {
|
|
170
208
|
return _jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "-" });
|
|
@@ -184,9 +222,16 @@ function PopoverShell({ label, title, children, icon = true, }) {
|
|
|
184
222
|
/**
|
|
185
223
|
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
186
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.
|
|
187
231
|
*/
|
|
188
|
-
export function CollectionCell({ value, maxInline = 3, locale, t, }) {
|
|
232
|
+
export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, variant = 'badge', }) {
|
|
189
233
|
const parsed = parseValue(value);
|
|
234
|
+
const inline = variant === 'inline';
|
|
190
235
|
// Empty-ish → muted dash.
|
|
191
236
|
if (parsed === null ||
|
|
192
237
|
parsed === undefined ||
|
|
@@ -205,16 +250,35 @@ export function CollectionCell({ value, maxInline = 3, locale, t, }) {
|
|
|
205
250
|
const allObjects = parsed.every((item) => isPlainObject(item));
|
|
206
251
|
if (allObjects) {
|
|
207
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
|
+
}
|
|
208
258
|
const count = rows.length;
|
|
209
259
|
const label = countLabel(count, locale, t);
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
260
|
+
const hasSchema = !!(itemFields && itemFields.length > 0);
|
|
261
|
+
// The no-JS tooltip mirrors the rendered table: schema-driven
|
|
262
|
+
// labels + resolved ref values when a schema is present, else the
|
|
263
|
+
// generic prettify/scalar pairs.
|
|
264
|
+
const title = hasSchema
|
|
265
|
+
? rows
|
|
266
|
+
.map((row) => itemFields
|
|
267
|
+
.map((field) => `${field.label}: ${renderItemFieldValue(field, row)}`)
|
|
268
|
+
.join(', '))
|
|
269
|
+
.join(' | ')
|
|
270
|
+
: rows
|
|
271
|
+
.map((row) => Object.entries(row)
|
|
272
|
+
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
273
|
+
.join(', '))
|
|
274
|
+
.join(' | ');
|
|
275
|
+
return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows, locale: locale, t: t, itemFields: itemFields }) }));
|
|
276
|
+
}
|
|
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 });
|
|
216
281
|
}
|
|
217
|
-
// Array of scalars (or mixed): preview first N joined, "+N" overflow.
|
|
218
282
|
const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ');
|
|
219
283
|
const overflow = parsed.length - maxInline;
|
|
220
284
|
const label = overflow > 0 ? `${preview} +${overflow}` : preview;
|
|
@@ -223,12 +287,16 @@ export function CollectionCell({ value, maxInline = 3, locale, t, }) {
|
|
|
223
287
|
}
|
|
224
288
|
// PLAIN OBJECT -----------------------------------------------------------
|
|
225
289
|
const entries = Object.entries(parsed);
|
|
226
|
-
|
|
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
|
|
227
295
|
.slice(0, maxInline)
|
|
228
296
|
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
229
297
|
.join(', ');
|
|
230
298
|
const overflow = entries.length - maxInline;
|
|
231
|
-
const label = overflow > 0 ? `${
|
|
299
|
+
const label = overflow > 0 ? `${previewPairs} +${overflow}` : previewPairs;
|
|
232
300
|
const title = entries
|
|
233
301
|
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
234
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') {
|
|
@@ -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;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,
|
|
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,CA8nBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -736,7 +736,7 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
736
736
|
}
|
|
737
737
|
default: {
|
|
738
738
|
if (typeof value === 'object' && value !== null) {
|
|
739
|
-
return (_jsx(CollectionCell, { value: value, locale: currentLanguage, t: t }));
|
|
739
|
+
return (_jsx(CollectionCell, { value: value, locale: currentLanguage, t: t, itemFields: col.itemFields ?? col.item_fields }));
|
|
740
740
|
}
|
|
741
741
|
if (col.key === 'description' ||
|
|
742
742
|
col.key === 'features' ||
|
package/dist/types.d.ts
CHANGED
|
@@ -137,6 +137,32 @@ export interface ColumnDefinition {
|
|
|
137
137
|
* reference resolved through the OrgConfigProvider.
|
|
138
138
|
*/
|
|
139
139
|
validation?: FieldValidation;
|
|
140
|
+
/**
|
|
141
|
+
* Declared schema for a jsonb line-items column (kernel v3 `item_fields`).
|
|
142
|
+
* Each entry describes one sub-field of the array's row objects: a `key`
|
|
143
|
+
* (the jsonb key), an already-LOCALIZED `label` (backend-translated), an
|
|
144
|
+
* optional `type` hint and an optional `ref` (FK target). When present the
|
|
145
|
+
* `CollectionCell` renders the popover mini-table with these headers in
|
|
146
|
+
* order and resolves `ref` columns to the backend-injected sibling label
|
|
147
|
+
* (the FK key without `_id`, else `<key>_label`) instead of the raw uuid.
|
|
148
|
+
* Tolerates the snake_case `item_fields` the kernel serves.
|
|
149
|
+
*/
|
|
150
|
+
itemFields?: ColumnItemField[];
|
|
151
|
+
/** snake_case alias served by the kernel for `itemFields`. */
|
|
152
|
+
item_fields?: ColumnItemField[];
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* One declared sub-field of a jsonb line-items column (see
|
|
156
|
+
* `ColumnDefinition.itemFields`). `label` is already localized by the backend
|
|
157
|
+
* and consumed verbatim; a non-empty `ref` flags the column for resolved-label
|
|
158
|
+
* rendering against the injected sibling. Structurally compatible with the
|
|
159
|
+
* `ItemField` consumed by `collection-cell`.
|
|
160
|
+
*/
|
|
161
|
+
export interface ColumnItemField {
|
|
162
|
+
key: string;
|
|
163
|
+
label: string;
|
|
164
|
+
type?: string;
|
|
165
|
+
ref?: string;
|
|
140
166
|
}
|
|
141
167
|
export interface ActionCondition {
|
|
142
168
|
field: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
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",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// - locale-aware: count noun + header keys render in the org language; the
|
|
10
10
|
// host `t` overrides; unknown keys fall back to snake→Title prettify.
|
|
11
11
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
12
|
-
import { cleanup, render, screen } from '@testing-library/react'
|
|
12
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
13
13
|
|
|
14
14
|
// Sin `globals: true` en vitest, RTL no auto-limpia entre tests.
|
|
15
15
|
afterEach(cleanup)
|
|
@@ -176,3 +176,250 @@ describe('CollectionCell', () => {
|
|
|
176
176
|
expect(container.textContent).toContain('{not valid json')
|
|
177
177
|
})
|
|
178
178
|
})
|
|
179
|
+
|
|
180
|
+
describe('CollectionCell with itemFields schema', () => {
|
|
181
|
+
const itemFields = [
|
|
182
|
+
{ key: 'product_id', label: 'Producto', ref: 'Product' },
|
|
183
|
+
{ key: 'quantity', label: 'Cantidad' },
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
// The popover mini-table mounts lazily — open it by clicking the count
|
|
187
|
+
// badge (Radix opens on pointerDown + click under happy-dom).
|
|
188
|
+
const openPopover = (badgeText: string) => {
|
|
189
|
+
const badge = screen.getByText(badgeText)
|
|
190
|
+
fireEvent.pointerDown(badge)
|
|
191
|
+
fireEvent.click(badge)
|
|
192
|
+
return badge
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
it('uses the schema labels verbatim as headers (no prettify/dict)', () => {
|
|
196
|
+
render(
|
|
197
|
+
<CollectionCell
|
|
198
|
+
locale="es"
|
|
199
|
+
itemFields={itemFields}
|
|
200
|
+
value={[
|
|
201
|
+
{
|
|
202
|
+
product_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
203
|
+
product: { value: '550e8400-e29b-41d4-a716-446655440000', label: 'Llanta 195/65' },
|
|
204
|
+
quantity: 2,
|
|
205
|
+
},
|
|
206
|
+
]}
|
|
207
|
+
/>
|
|
208
|
+
)
|
|
209
|
+
openPopover('1 ítem')
|
|
210
|
+
// Headers come from the schema `label` verbatim.
|
|
211
|
+
expect(screen.getByRole('columnheader', { name: 'Producto' })).toBeTruthy()
|
|
212
|
+
expect(screen.getByRole('columnheader', { name: 'Cantidad' })).toBeTruthy()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('resolves a ref field to the injected sibling label, not the uuid', () => {
|
|
216
|
+
render(
|
|
217
|
+
<CollectionCell
|
|
218
|
+
itemFields={itemFields}
|
|
219
|
+
value={[
|
|
220
|
+
{
|
|
221
|
+
product_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
222
|
+
product: { value: '550e8400-e29b-41d4-a716-446655440000', label: 'Llanta 195/65' },
|
|
223
|
+
quantity: 2,
|
|
224
|
+
},
|
|
225
|
+
]}
|
|
226
|
+
/>
|
|
227
|
+
)
|
|
228
|
+
openPopover('1 item')
|
|
229
|
+
expect(screen.getByRole('cell', { name: 'Llanta 195/65' })).toBeTruthy()
|
|
230
|
+
// The raw uuid (truncated form) must NOT appear in any cell.
|
|
231
|
+
expect(screen.queryByText('550e8400…')).toBeNull()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('resolves a ref field from a `<key>_label` sibling when key has no _id suffix', () => {
|
|
235
|
+
render(
|
|
236
|
+
<CollectionCell
|
|
237
|
+
itemFields={[{ key: 'product', label: 'Producto', ref: 'Product' }]}
|
|
238
|
+
value={[
|
|
239
|
+
{
|
|
240
|
+
product: '550e8400-e29b-41d4-a716-446655440000',
|
|
241
|
+
product_label: { value: '550e8400-e29b-41d4-a716-446655440000', label: 'Balanceo' },
|
|
242
|
+
},
|
|
243
|
+
]}
|
|
244
|
+
/>
|
|
245
|
+
)
|
|
246
|
+
openPopover('1 item')
|
|
247
|
+
expect(screen.getByRole('cell', { name: 'Balanceo' })).toBeTruthy()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('accepts a bare string sibling for a ref field', () => {
|
|
251
|
+
render(
|
|
252
|
+
<CollectionCell
|
|
253
|
+
itemFields={[{ key: 'product_id', label: 'Producto', ref: 'Product' }]}
|
|
254
|
+
value={[{ product_id: 'x', product: 'Aceite 5W30' }]}
|
|
255
|
+
/>
|
|
256
|
+
)
|
|
257
|
+
openPopover('1 item')
|
|
258
|
+
expect(screen.getByRole('cell', { name: 'Aceite 5W30' })).toBeTruthy()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('falls back to a truncated uuid when the ref sibling is missing', () => {
|
|
262
|
+
render(
|
|
263
|
+
<CollectionCell
|
|
264
|
+
itemFields={[{ key: 'product_id', label: 'Producto', ref: 'Product' }]}
|
|
265
|
+
value={[{ product_id: '550e8400-e29b-41d4-a716-446655440000' }]}
|
|
266
|
+
/>
|
|
267
|
+
)
|
|
268
|
+
openPopover('1 item')
|
|
269
|
+
expect(screen.getByRole('cell', { name: '550e8400…' })).toBeTruthy()
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('keeps the locale-aware count noun on the badge', () => {
|
|
273
|
+
render(
|
|
274
|
+
<CollectionCell
|
|
275
|
+
locale="es"
|
|
276
|
+
itemFields={itemFields}
|
|
277
|
+
value={[
|
|
278
|
+
{ product_id: 'a', product: { value: 'a', label: 'A' }, quantity: 1 },
|
|
279
|
+
{ product_id: 'b', product: { value: 'b', label: 'B' }, quantity: 2 },
|
|
280
|
+
]}
|
|
281
|
+
/>
|
|
282
|
+
)
|
|
283
|
+
expect(screen.getByText('2 ítems')).toBeTruthy()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('renders non-ref fields via formatScalar under the schema header', () => {
|
|
287
|
+
render(
|
|
288
|
+
<CollectionCell
|
|
289
|
+
itemFields={itemFields}
|
|
290
|
+
value={[{ product_id: 'a', product: { value: 'a', label: 'A' }, quantity: 7 }]}
|
|
291
|
+
/>
|
|
292
|
+
)
|
|
293
|
+
openPopover('1 item')
|
|
294
|
+
expect(screen.getByRole('cell', { name: '7' })).toBeTruthy()
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('mirrors the schema labels + resolved ref values in the badge title', () => {
|
|
298
|
+
render(
|
|
299
|
+
<CollectionCell
|
|
300
|
+
itemFields={itemFields}
|
|
301
|
+
value={[
|
|
302
|
+
{
|
|
303
|
+
product_id: '550e8400-e29b-41d4-a716-446655440000',
|
|
304
|
+
product: { value: 'x', label: 'Llanta 195/65' },
|
|
305
|
+
quantity: 2,
|
|
306
|
+
},
|
|
307
|
+
]}
|
|
308
|
+
/>
|
|
309
|
+
)
|
|
310
|
+
const title =
|
|
311
|
+
screen.getByText('1 item').closest('[title]')!.getAttribute('title') ?? ''
|
|
312
|
+
expect(title).toContain('Producto: Llanta 195/65')
|
|
313
|
+
expect(title).toContain('Cantidad: 2')
|
|
314
|
+
expect(title).not.toContain('550e8400')
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('is unchanged (generic prettify) when no itemFields are provided', () => {
|
|
318
|
+
render(
|
|
319
|
+
<CollectionCell
|
|
320
|
+
locale="es"
|
|
321
|
+
value={[{ product_id: 'abc', quantity: 2 }]}
|
|
322
|
+
/>
|
|
323
|
+
)
|
|
324
|
+
// Generic dict path: the badge title carries the prettified headers and
|
|
325
|
+
// the raw (unresolved) values, exactly as before.
|
|
326
|
+
const title =
|
|
327
|
+
screen.getByText('1 ítem').closest('[title]')!.getAttribute('title') ?? ''
|
|
328
|
+
expect(title).toContain('Producto:')
|
|
329
|
+
expect(title).toContain('Cantidad: 2')
|
|
330
|
+
})
|
|
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
|
@@ -28,6 +28,59 @@ const UUID_LIKE_RE =
|
|
|
28
28
|
/** Host i18n translator (react-i18next `t`), as threaded into the columns factory. */
|
|
29
29
|
export type Translate = (key: string, options?: any) => string
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Declared schema for one column of a jsonb line-items array. Mirrors the
|
|
33
|
+
* kernel v3 `item_fields` entry the backend serves on the column metadata
|
|
34
|
+
* (`col.itemFields` / snake `col.item_fields`). When present it drives the
|
|
35
|
+
* popover mini-table: headers come from `label` (already LOCALIZED by the
|
|
36
|
+
* backend — never re-translated here) and `ref` columns resolve to the
|
|
37
|
+
* backend-injected sibling label instead of the raw uuid.
|
|
38
|
+
*/
|
|
39
|
+
export interface ItemField {
|
|
40
|
+
/** jsonb key this column maps to (e.g. `product_id`, `quantity`). */
|
|
41
|
+
key: string
|
|
42
|
+
/** Header text — ALREADY localized by the backend. Used verbatim. */
|
|
43
|
+
label: string
|
|
44
|
+
/** Declarative cell type hint (informational; not branched on today). */
|
|
45
|
+
type?: string
|
|
46
|
+
/** FK target model. When set, the cell renders the resolved sibling label. */
|
|
47
|
+
ref?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves the backend-injected resolved sibling key for a ref item-field,
|
|
52
|
+
* mirroring `relationKeyFor` in dynamic-columns: the raw key with a trailing
|
|
53
|
+
* `_id` stripped (`product_id` → `product`), else `<key>_label`.
|
|
54
|
+
*/
|
|
55
|
+
function siblingKeyFor(key: string): string {
|
|
56
|
+
return key.endsWith('_id') ? key.slice(0, -3) : `${key}_label`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Renders the cell value for one declared item-field of a jsonb row. For a
|
|
61
|
+
* `ref` field it prefers the backend-injected resolved sibling (the FK key
|
|
62
|
+
* without `_id`, else `<key>_label`): a `{ value, label }` object shows its
|
|
63
|
+
* `label`, a bare string shows itself; absent → the raw value via
|
|
64
|
+
* `formatScalar` (truncated uuid). Non-ref fields render `formatScalar(value)`.
|
|
65
|
+
*/
|
|
66
|
+
function renderItemFieldValue(
|
|
67
|
+
field: ItemField,
|
|
68
|
+
row: Record<string, unknown>,
|
|
69
|
+
): string {
|
|
70
|
+
if (field.ref) {
|
|
71
|
+
const sibling = row[siblingKeyFor(field.key)]
|
|
72
|
+
if (sibling && typeof sibling === 'object' && !Array.isArray(sibling)) {
|
|
73
|
+
const label = (sibling as Record<string, unknown>).label
|
|
74
|
+
if (label !== undefined && label !== null && label !== '') {
|
|
75
|
+
return String(label)
|
|
76
|
+
}
|
|
77
|
+
} else if (typeof sibling === 'string' && sibling !== '') {
|
|
78
|
+
return sibling
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return formatScalar(row[field.key])
|
|
82
|
+
}
|
|
83
|
+
|
|
31
84
|
/** Normalize an org/UI language tag to a base language code (`es-MX` → `es`). */
|
|
32
85
|
function baseLang(locale?: string): string {
|
|
33
86
|
return (locale || 'en').toLowerCase().split('-')[0]
|
|
@@ -204,11 +257,51 @@ function MiniTable({
|
|
|
204
257
|
rows,
|
|
205
258
|
locale,
|
|
206
259
|
t,
|
|
260
|
+
itemFields,
|
|
207
261
|
}: {
|
|
208
262
|
rows: Record<string, unknown>[]
|
|
209
263
|
locale?: string
|
|
210
264
|
t?: Translate
|
|
265
|
+
itemFields?: ItemField[]
|
|
211
266
|
}) {
|
|
267
|
+
// Schema-driven path: a declared `item_fields` schema fixes the column
|
|
268
|
+
// order + headers (already localized by the backend, used VERBATIM) and
|
|
269
|
+
// resolves ref columns to the injected sibling label instead of the raw
|
|
270
|
+
// uuid. Sibling/raw keys not covered by the schema are dropped from the
|
|
271
|
+
// table (the schema is the source of truth for what to surface).
|
|
272
|
+
if (itemFields && itemFields.length > 0) {
|
|
273
|
+
return (
|
|
274
|
+
<Table>
|
|
275
|
+
<TableHeader>
|
|
276
|
+
<TableRow>
|
|
277
|
+
{itemFields.map((field) => (
|
|
278
|
+
<TableHead
|
|
279
|
+
key={field.key}
|
|
280
|
+
className="text-xs whitespace-nowrap"
|
|
281
|
+
>
|
|
282
|
+
{field.label}
|
|
283
|
+
</TableHead>
|
|
284
|
+
))}
|
|
285
|
+
</TableRow>
|
|
286
|
+
</TableHeader>
|
|
287
|
+
<TableBody>
|
|
288
|
+
{rows.map((row, i) => (
|
|
289
|
+
<TableRow key={i}>
|
|
290
|
+
{itemFields.map((field) => (
|
|
291
|
+
<TableCell
|
|
292
|
+
key={field.key}
|
|
293
|
+
className="text-xs whitespace-nowrap"
|
|
294
|
+
>
|
|
295
|
+
{renderItemFieldValue(field, row)}
|
|
296
|
+
</TableCell>
|
|
297
|
+
))}
|
|
298
|
+
</TableRow>
|
|
299
|
+
))}
|
|
300
|
+
</TableBody>
|
|
301
|
+
</Table>
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
212
305
|
const keys = unionKeys(rows)
|
|
213
306
|
if (keys.length === 0) {
|
|
214
307
|
return <div className="p-3 text-xs text-muted-foreground">-</div>
|
|
@@ -319,19 +412,46 @@ export interface CollectionCellProps {
|
|
|
319
412
|
locale?: string
|
|
320
413
|
/** Host i18n translator; takes precedence over the built-in dictionary. */
|
|
321
414
|
t?: Translate
|
|
415
|
+
/**
|
|
416
|
+
* Declared schema for the jsonb line-items columns (kernel v3 `item_fields`,
|
|
417
|
+
* read from `col.itemFields ?? col.item_fields` at the callsite). When
|
|
418
|
+
* present AND the value is an array of objects, the popover mini-table uses
|
|
419
|
+
* these (already-localized) headers in order and resolves `ref` columns to
|
|
420
|
+
* the backend-injected sibling label. Absent → the generic dict/prettify
|
|
421
|
+
* behaviour is unchanged.
|
|
422
|
+
*/
|
|
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'
|
|
322
433
|
}
|
|
323
434
|
|
|
324
435
|
/**
|
|
325
436
|
* Generic renderer for jsonb / array / object cell values. Brand-neutral,
|
|
326
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.
|
|
327
444
|
*/
|
|
328
445
|
export function CollectionCell({
|
|
329
446
|
value,
|
|
330
447
|
maxInline = 3,
|
|
331
448
|
locale,
|
|
332
449
|
t,
|
|
450
|
+
itemFields,
|
|
451
|
+
variant = 'badge',
|
|
333
452
|
}: CollectionCellProps) {
|
|
334
453
|
const parsed = parseValue(value)
|
|
454
|
+
const inline = variant === 'inline'
|
|
335
455
|
|
|
336
456
|
// Empty-ish → muted dash.
|
|
337
457
|
if (
|
|
@@ -362,26 +482,62 @@ export function CollectionCell({
|
|
|
362
482
|
const allObjects = parsed.every((item) => isPlainObject(item))
|
|
363
483
|
if (allObjects) {
|
|
364
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
|
+
}
|
|
365
497
|
const count = rows.length
|
|
366
498
|
const label = countLabel(count, locale, t)
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
499
|
+
const hasSchema = !!(itemFields && itemFields.length > 0)
|
|
500
|
+
// The no-JS tooltip mirrors the rendered table: schema-driven
|
|
501
|
+
// labels + resolved ref values when a schema is present, else the
|
|
502
|
+
// generic prettify/scalar pairs.
|
|
503
|
+
const title = hasSchema
|
|
504
|
+
? rows
|
|
505
|
+
.map((row) =>
|
|
506
|
+
itemFields!
|
|
507
|
+
.map(
|
|
508
|
+
(field) =>
|
|
509
|
+
`${field.label}: ${renderItemFieldValue(field, row)}`
|
|
510
|
+
)
|
|
511
|
+
.join(', ')
|
|
512
|
+
)
|
|
513
|
+
.join(' | ')
|
|
514
|
+
: rows
|
|
515
|
+
.map((row) =>
|
|
516
|
+
Object.entries(row)
|
|
517
|
+
.map(
|
|
518
|
+
([k, v]) =>
|
|
519
|
+
`${prettifyKey(k, locale, t)}: ${formatScalar(v)}`
|
|
520
|
+
)
|
|
521
|
+
.join(', ')
|
|
522
|
+
)
|
|
523
|
+
.join(' | ')
|
|
377
524
|
return (
|
|
378
525
|
<PopoverShell label={label} title={title}>
|
|
379
|
-
<MiniTable
|
|
526
|
+
<MiniTable
|
|
527
|
+
rows={rows}
|
|
528
|
+
locale={locale}
|
|
529
|
+
t={t}
|
|
530
|
+
itemFields={itemFields}
|
|
531
|
+
/>
|
|
380
532
|
</PopoverShell>
|
|
381
533
|
)
|
|
382
534
|
}
|
|
383
535
|
|
|
384
|
-
// 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
|
+
}
|
|
385
541
|
const preview = parsed.slice(0, maxInline).map(formatScalar).join(', ')
|
|
386
542
|
const overflow = parsed.length - maxInline
|
|
387
543
|
const label =
|
|
@@ -396,12 +552,16 @@ export function CollectionCell({
|
|
|
396
552
|
|
|
397
553
|
// PLAIN OBJECT -----------------------------------------------------------
|
|
398
554
|
const entries = Object.entries(parsed)
|
|
399
|
-
|
|
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
|
|
400
560
|
.slice(0, maxInline)
|
|
401
561
|
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
402
562
|
.join(', ')
|
|
403
563
|
const overflow = entries.length - maxInline
|
|
404
|
-
const label = overflow > 0 ? `${
|
|
564
|
+
const label = overflow > 0 ? `${previewPairs} +${overflow}` : previewPairs
|
|
405
565
|
const title = entries
|
|
406
566
|
.map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
|
|
407
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
|
|
package/src/dynamic-columns.tsx
CHANGED
package/src/types.ts
CHANGED
|
@@ -170,6 +170,33 @@ export interface ColumnDefinition {
|
|
|
170
170
|
* reference resolved through the OrgConfigProvider.
|
|
171
171
|
*/
|
|
172
172
|
validation?: FieldValidation
|
|
173
|
+
/**
|
|
174
|
+
* Declared schema for a jsonb line-items column (kernel v3 `item_fields`).
|
|
175
|
+
* Each entry describes one sub-field of the array's row objects: a `key`
|
|
176
|
+
* (the jsonb key), an already-LOCALIZED `label` (backend-translated), an
|
|
177
|
+
* optional `type` hint and an optional `ref` (FK target). When present the
|
|
178
|
+
* `CollectionCell` renders the popover mini-table with these headers in
|
|
179
|
+
* order and resolves `ref` columns to the backend-injected sibling label
|
|
180
|
+
* (the FK key without `_id`, else `<key>_label`) instead of the raw uuid.
|
|
181
|
+
* Tolerates the snake_case `item_fields` the kernel serves.
|
|
182
|
+
*/
|
|
183
|
+
itemFields?: ColumnItemField[]
|
|
184
|
+
/** snake_case alias served by the kernel for `itemFields`. */
|
|
185
|
+
item_fields?: ColumnItemField[]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* One declared sub-field of a jsonb line-items column (see
|
|
190
|
+
* `ColumnDefinition.itemFields`). `label` is already localized by the backend
|
|
191
|
+
* and consumed verbatim; a non-empty `ref` flags the column for resolved-label
|
|
192
|
+
* rendering against the injected sibling. Structurally compatible with the
|
|
193
|
+
* `ItemField` consumed by `collection-cell`.
|
|
194
|
+
*/
|
|
195
|
+
export interface ColumnItemField {
|
|
196
|
+
key: string
|
|
197
|
+
label: string
|
|
198
|
+
type?: string
|
|
199
|
+
ref?: string
|
|
173
200
|
}
|
|
174
201
|
|
|
175
202
|
export interface ActionCondition {
|