@asteby/metacore-runtime-react 18.22.0 → 18.24.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,49 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.24.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 39c43ea: jsonb line-item `ref` cells render as relation chips (icon/photo + name).
8
+
9
+ In `CollectionCell` (table popover, inline detail view, and the read-only edit
10
+ field), a resolved ref sub-field — e.g. `product_id` inside a transfer's `items`
11
+ — now renders with the same "pro" relation look the FK table columns use: a
12
+ subtle deterministic tint, the resolved record's thumbnail (product photo / logo
13
+ / avatar, resolved via the threaded `getImageUrl`) or a generic entity icon
14
+ fallback, and the resolved name — instead of a truncated uuid.
15
+
16
+ `CollectionCell` gains an optional `getImageUrl` prop, threaded from the columns
17
+ factory and from the record dialog's `ImageUrlContext`. Backend-agnostic: it
18
+ drives off the backend-injected `{ value, label, image }` sibling; an unresolved
19
+ ref still falls back to the scalar value.
20
+
21
+ ## 18.23.0
22
+
23
+ ### Minor Changes
24
+
25
+ - dc5e552: Polish the generic record EDIT dialog (`DynamicRecordDialog` mode='edit'),
26
+ fixing two prod issues that affected every module using it (transfers, orders,
27
+ customers):
28
+ - **jsonb line-items render read-only instead of "[object Object]".** A field
29
+ that is a jsonb line-items column (declares `item_fields`, or its value is an
30
+ array/plain object) is no longer rendered as a broken text input. The edit
31
+ form now renders it read-only with the same inline `CollectionCell` table the
32
+ detail view uses — localized headers + resolved ref labels — plus a
33
+ translatable "Solo lectura" hint (`datatable.readOnly`). These are
34
+ action-built documents; field-by-field array editing stays out of scope.
35
+ - **FK selects show the related record's name, not the raw uuid.** A
36
+ `dynamic_select` / `ref` field in edit mode now seeds its trigger from the
37
+ backend-injected relation sibling (`source_warehouse_id` →
38
+ `source_warehouse: { value, label }`, the key without `_id`) via the existing
39
+ `DynamicSelectField` `seedOption` prop, so an existing selection displays the
40
+ label immediately without waiting for a lookup. Falls back to the raw value
41
+ (today's behaviour) when no sibling is present; creating/changing the
42
+ selection is unchanged.
43
+
44
+ `EditField`, `isLineItemsField`, and `fkSeedOption` are exported from the
45
+ dialogs module.
46
+
3
47
  ## 18.22.0
4
48
 
5
49
  ### Minor Changes
@@ -57,6 +57,12 @@ export interface CollectionCellProps {
57
57
  * behaviour is unchanged.
58
58
  */
59
59
  itemFields?: ItemField[];
60
+ /**
61
+ * Resolves a stored image path to a displayable URL (same resolver the table
62
+ * columns use). Threaded to the ref-chip thumbnails so resolved line-item
63
+ * relations show a product photo / logo / avatar like the FK columns do.
64
+ */
65
+ getImageUrl?: (path: string) => string;
60
66
  /**
61
67
  * Presentation mode.
62
68
  * - `'badge'` (default): the compact count/preview badge that opens a
@@ -77,5 +83,5 @@ export interface CollectionCellProps {
77
83
  * itemFields schema (localized headers + resolved ref labels) and the
78
84
  * locale-aware generic fallback.
79
85
  */
80
- export declare function CollectionCell({ value, maxInline, locale, t, itemFields, variant, }: CollectionCellProps): React.JSX.Element;
86
+ export declare function CollectionCell({ value, maxInline, locale, t, itemFields, getImageUrl, variant, }: CollectionCellProps): React.JSX.Element;
81
87
  //# 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;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"}
1
+ {"version":3,"file":"collection-cell.d.ts","sourceRoot":"","sources":["../src/collection-cell.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAkD9B,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;AA6KD;;;;;;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;AAuMD,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;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;;;;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,WAAW,EACX,OAAiB,GACpB,EAAE,mBAAmB,qBA2HrB"}
@@ -1,7 +1,37 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { List } from 'lucide-react';
3
- import { Badge, Popover, PopoverContent, PopoverTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, } from '@asteby/metacore-ui';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // Generic, brand-neutral table-cell renderer for jsonb / array / object column
3
+ // values. Kernel-derived dynamic tables surface raw jsonb columns (line items,
4
+ // nested config blobs, scalar arrays) with no per-column metadata; without this
5
+ // they rendered as raw `JSON.stringify(value)` which is unreadable. This renders
6
+ // a compact trigger (count badge / inline pairs) plus a Popover with a clean
7
+ // mini-table — no per-addon config required, safe on any shape.
8
+ import * as React from 'react';
9
+ import { Box, List } from 'lucide-react';
10
+ import { Avatar, AvatarFallback, AvatarImage, Badge, Popover, PopoverContent, PopoverTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, } from '@asteby/metacore-ui';
11
+ import { getInitials, relationChipStyles } from '@asteby/metacore-ui/lib';
4
12
  import { humanizeToken } from './dynamic-columns-helpers';
13
+ /**
14
+ * Tracks the host's dark-mode class on <html> so relation chips pick a tint
15
+ * tuned for the active theme. Replicated from dynamic-columns (kept local to
16
+ * avoid a cross-module import); mirror changes if that one evolves.
17
+ */
18
+ function useIsDarkTheme() {
19
+ const [isDark, setIsDark] = React.useState(() => typeof document !== 'undefined' &&
20
+ document.documentElement.classList.contains('dark'));
21
+ React.useEffect(() => {
22
+ if (typeof document === 'undefined')
23
+ return;
24
+ const sync = () => setIsDark(document.documentElement.classList.contains('dark'));
25
+ sync();
26
+ const observer = new MutationObserver(sync);
27
+ observer.observe(document.documentElement, {
28
+ attributes: true,
29
+ attributeFilter: ['class'],
30
+ });
31
+ return () => observer.disconnect();
32
+ }, []);
33
+ return isDark;
34
+ }
5
35
  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
36
  /**
7
37
  * Resolves the backend-injected resolved sibling key for a ref item-field,
@@ -12,27 +42,59 @@ function siblingKeyFor(key) {
12
42
  return key.endsWith('_id') ? key.slice(0, -3) : `${key}_label`;
13
43
  }
14
44
  /**
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)`.
45
+ * Reads the backend-injected resolved sibling for a `ref` item-field (the FK
46
+ * key without `_id`, else `<key>_label`). Returns the normalized `{label,
47
+ * image}` when present, a `{label}` for a bare-string sibling, or null when the
48
+ * ref is unresolved (so the caller falls back to the raw value).
20
49
  */
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;
50
+ function resolvedRefFor(field, row) {
51
+ if (!field.ref)
52
+ return null;
53
+ const sibling = row[siblingKeyFor(field.key)];
54
+ if (sibling && typeof sibling === 'object' && !Array.isArray(sibling)) {
55
+ const obj = sibling;
56
+ const label = obj.label;
57
+ if (label !== undefined && label !== null && label !== '') {
58
+ return {
59
+ label: String(label),
60
+ value: obj.value,
61
+ image: typeof obj.image === 'string' && obj.image !== ''
62
+ ? obj.image
63
+ : undefined,
64
+ };
32
65
  }
33
66
  }
67
+ else if (typeof sibling === 'string' && sibling !== '') {
68
+ return { label: sibling };
69
+ }
70
+ return null;
71
+ }
72
+ /**
73
+ * Plain-text value for one declared item-field — used for the no-JS popover
74
+ * `title` tooltip (which can't render JSX). Ref fields show the resolved label;
75
+ * everything else uses `formatScalar` (truncated uuid for unresolved ids).
76
+ */
77
+ function itemFieldText(field, row) {
78
+ const ref = resolvedRefFor(field, row);
79
+ if (ref?.label)
80
+ return ref.label;
34
81
  return formatScalar(row[field.key]);
35
82
  }
83
+ /**
84
+ * Visual cell for one declared item-field. A resolved `ref` renders as a
85
+ * relation chip (subtle deterministic tint + product thumbnail or a generic
86
+ * entity icon + name) — the same "pro" look the table FK columns use — so jsonb
87
+ * line items read like first-class relations instead of raw uuids. Non-ref (or
88
+ * unresolved) fields render the plain scalar.
89
+ */
90
+ function ItemFieldCell({ field, row, getImageUrl, }) {
91
+ const isDark = useIsDarkTheme();
92
+ const ref = resolvedRefFor(field, row);
93
+ if (ref?.label) {
94
+ return (_jsxs("span", { className: "inline-flex max-w-[220px] items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium", style: relationChipStyles(ref.label, { isDark }), title: ref.label, children: [ref.image ? (_jsxs(Avatar, { className: "shrink-0 rounded-sm ring-1 ring-border/40", style: { width: 18, height: 18 }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(ref.image) : ref.image, alt: ref.label, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-sm bg-primary/10 text-[8px] font-bold text-primary", children: getInitials(ref.label) })] })) : (_jsx(Box, { className: "h-3 w-3 shrink-0 opacity-70" })), _jsx("span", { className: "truncate", children: ref.label })] }));
95
+ }
96
+ return _jsx(_Fragment, { children: formatScalar(row[field.key]) });
97
+ }
36
98
  /** Normalize an org/UI language tag to a base language code (`es-MX` → `es`). */
37
99
  function baseLang(locale) {
38
100
  return (locale || 'en').toLowerCase().split('-')[0];
@@ -194,14 +256,14 @@ function unionKeys(rows) {
194
256
  return seen;
195
257
  }
196
258
  const PANEL_CLASS = 'w-auto max-w-[480px] max-h-[320px] overflow-auto p-0';
197
- function MiniTable({ rows, locale, t, itemFields, }) {
259
+ function MiniTable({ rows, locale, t, itemFields, getImageUrl, }) {
198
260
  // Schema-driven path: a declared `item_fields` schema fixes the column
199
261
  // order + headers (already localized by the backend, used VERBATIM) and
200
262
  // resolves ref columns to the injected sibling label instead of the raw
201
263
  // uuid. Sibling/raw keys not covered by the schema are dropped from the
202
264
  // table (the schema is the source of truth for what to surface).
203
265
  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))) })] }));
266
+ 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: _jsx(ItemFieldCell, { field: field, row: row, getImageUrl: getImageUrl }) }, field.key))) }, i))) })] }));
205
267
  }
206
268
  const keys = unionKeys(rows);
207
269
  if (keys.length === 0) {
@@ -229,7 +291,7 @@ function PopoverShell({ label, title, children, icon = true, }) {
229
291
  * itemFields schema (localized headers + resolved ref labels) and the
230
292
  * locale-aware generic fallback.
231
293
  */
232
- export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, variant = 'badge', }) {
294
+ export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, getImageUrl, variant = 'badge', }) {
233
295
  const parsed = parseValue(value);
234
296
  const inline = variant === 'inline';
235
297
  // Empty-ish → muted dash.
@@ -253,7 +315,7 @@ export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, va
253
315
  // Inline mode (detail view): render the mini-table directly, no
254
316
  // badge/popover. The same schema-driven path applies.
255
317
  if (inline) {
256
- return (_jsx(MiniTable, { rows: rows, locale: locale, t: t, itemFields: itemFields }));
318
+ return (_jsx(MiniTable, { rows: rows, locale: locale, t: t, itemFields: itemFields, getImageUrl: getImageUrl }));
257
319
  }
258
320
  const count = rows.length;
259
321
  const label = countLabel(count, locale, t);
@@ -264,7 +326,7 @@ export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, va
264
326
  const title = hasSchema
265
327
  ? rows
266
328
  .map((row) => itemFields
267
- .map((field) => `${field.label}: ${renderItemFieldValue(field, row)}`)
329
+ .map((field) => `${field.label}: ${itemFieldText(field, row)}`)
268
330
  .join(', '))
269
331
  .join(' | ')
270
332
  : rows
@@ -272,7 +334,7 @@ export function CollectionCell({ value, maxInline = 3, locale, t, itemFields, va
272
334
  .map(([k, v]) => `${prettifyKey(k, locale, t)}: ${formatScalar(v)}`)
273
335
  .join(', '))
274
336
  .join(' | ');
275
- return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows, locale: locale, t: t, itemFields: itemFields }) }));
337
+ return (_jsx(PopoverShell, { label: label, title: title, children: _jsx(MiniTable, { rows: rows, locale: locale, t: t, itemFields: itemFields, getImageUrl: getImageUrl }) }));
276
338
  }
277
339
  // Array of scalars (or mixed). Inline mode renders the full list; badge
278
340
  // mode previews the first N joined with a "+N" overflow trigger.
@@ -1,4 +1,5 @@
1
1
  import type { ModelSchema } from './types';
2
+ import { type ResolvedOption } from '../use-options-resolver';
2
3
  import { type ItemField } from '../collection-cell';
3
4
  import { type GetImageUrl } from '../image-url-context';
4
5
  export type { GetImageUrl };
@@ -158,6 +159,8 @@ export interface DynamicRecordDialogProps {
158
159
  */
159
160
  onChange?: () => void;
160
161
  }
162
+ export declare function isLineItemsField(field: FieldDef, value: any): boolean;
163
+ export declare function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null;
161
164
  export declare function isMoneyField(field: FieldDef, value: any): boolean;
162
165
  export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, onChange, }: DynamicRecordDialogProps): import("react").JSX.Element;
163
166
  export declare function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, currency: currencyProp, }: {
@@ -171,4 +174,11 @@ export declare function ViewValue({ field, value: rawValue, record, getImageUrl:
171
174
  /** Optional override; when omitted falls back to the nearest provider. */
172
175
  currency?: string;
173
176
  }): import("react").JSX.Element;
177
+ export declare function EditField({ field, value, onChange, record }: {
178
+ field: FieldDef;
179
+ value: any;
180
+ onChange: (val: any) => void;
181
+ /** The full record being edited — supplies FK relation siblings + line-items. */
182
+ record?: any;
183
+ }): import("react").JSX.Element;
174
184
  //# sourceMappingURL=dynamic-record.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
@@ -85,6 +85,51 @@ function relationSiblingValue(field, record) {
85
85
  }
86
86
  return undefined;
87
87
  }
88
+ // fieldItemFields reads the declared jsonb line-items schema off a field,
89
+ // tolerating the snake_case `item_fields` alias the kernel serves.
90
+ function fieldItemFields(field) {
91
+ return field.itemFields ?? field.item_fields;
92
+ }
93
+ // isLineItemsField — a jsonb line-items column (e.g. Transfer.items): it either
94
+ // declares an `item_fields` schema, or its value is a structured array/object.
95
+ // These are action-built documents; field-by-field editing of the array is out
96
+ // of scope, so the edit dialog renders them read-only (the inline table) rather
97
+ // than an input that would stringify to "[object Object]". Scalars and known
98
+ // editable widgets (media/upload, color, dates) are NOT line-items.
99
+ export function isLineItemsField(field, value) {
100
+ if (field.type === 'image' || field.widget === 'upload')
101
+ return false;
102
+ if (fieldItemFields(field)?.length)
103
+ return true;
104
+ if (Array.isArray(value))
105
+ return true;
106
+ return (value !== null &&
107
+ typeof value === 'object' &&
108
+ !(value instanceof Date));
109
+ }
110
+ // fkSeedOption builds the pre-resolved option for a FK select's CURRENT value
111
+ // from the relation sibling the backend injected alongside the column
112
+ // (`source_warehouse_id` → `source_warehouse = {value,label,image?}`, the key
113
+ // without `_id`; same convention as the jsonb refs). Lets the edit picker show
114
+ // the related record's NAME instead of the raw uuid without waiting for a
115
+ // network lookup. Returns null when there is no usable sibling (the picker then
116
+ // falls back to its existing typeahead/lookup behaviour).
117
+ export function fkSeedOption(field, value, record) {
118
+ if (value === undefined || value === null || value === '')
119
+ return null;
120
+ const sib = relationSiblingValue(field, record);
121
+ const label = typeof sib === 'string' ? sib : objectLabel(sib);
122
+ if (!label)
123
+ return null;
124
+ const id = String(value);
125
+ return {
126
+ id,
127
+ value: id,
128
+ label,
129
+ name: label,
130
+ image: pickImage(sib) ?? null,
131
+ };
132
+ }
88
133
  // servedOption matches a field's served option list (enum/select with
89
134
  // {value,label,color,icon,image}) against the current value.
90
135
  function servedOption(field, value) {
@@ -454,7 +499,7 @@ function LoadingSkeleton() {
454
499
  }
455
500
  function FieldRow({ field, record, value, mode, onChange }) {
456
501
  const isReadonly = field.readonly || mode === 'view';
457
- return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: [field.label, field.required && mode !== 'view' && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), isReadonly ? (_jsx(ViewValue, { field: field, value: value, record: record })) : (_jsx(EditField, { field: field, value: value, onChange: onChange }))] }));
502
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: [field.label, field.required && mode !== 'view' && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), isReadonly ? (_jsx(ViewValue, { field: field, value: value, record: record })) : (_jsx(EditField, { field: field, value: value, onChange: onChange, record: record }))] }));
458
503
  }
459
504
  // RelationViewValue — read-only FK lead. Resolves the relation's label + image
460
505
  // from (1) the sibling object the table served, then (2) the canonical options
@@ -616,6 +661,7 @@ function IconNameViewValue({ name }) {
616
661
  // empty objects keep the "—" marker (CollectionCell renders a muted dash, which
617
662
  // we normalize to the em-dash the detail view uses elsewhere).
618
663
  function StructuredViewValue({ value, field, locale, t, }) {
664
+ const getImageUrl = useContext(ImageUrlContext);
619
665
  const isEmpty = value === null ||
620
666
  value === undefined ||
621
667
  value === '' ||
@@ -626,9 +672,18 @@ function StructuredViewValue({ value, field, locale, t, }) {
626
672
  if (isEmpty) {
627
673
  return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
628
674
  }
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 }) }));
675
+ 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, getImageUrl: getImageUrl }) }));
630
676
  }
631
- function EditField({ field, value, onChange }) {
677
+ export function EditField({ field, value, onChange, record }) {
678
+ const { t, i18n } = useTranslation();
679
+ const editFieldImageUrl = useContext(ImageUrlContext);
680
+ // Jsonb line-items columns (e.g. Transfer.items) are action-built documents:
681
+ // editing the array field-by-field is out of scope. Render them READ-ONLY
682
+ // with the same inline table the detail view uses — a localized, ref-resolved
683
+ // mini-table — instead of an input that stringifies to "[object Object]".
684
+ if (isLineItemsField(field, value)) {
685
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "rounded-md border bg-muted/30 p-2", children: _jsx(CollectionCell, { value: value, itemFields: fieldItemFields(field), variant: "inline", locale: i18n.language, t: t, getImageUrl: editFieldImageUrl }) }), _jsx("p", { className: "text-[11px] text-muted-foreground", children: t('datatable.readOnly', { defaultValue: 'Solo lectura' }) })] }));
686
+ }
632
687
  if (field.type === 'boolean') {
633
688
  return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, onCheckedChange: onChange }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
634
689
  }
@@ -645,7 +700,12 @@ function EditField({ field, value, onChange }) {
645
700
  // option thumbnails and the inline-create "+" — against /api/options/<ref>.
646
701
  // Static inline `options` are handled by the enum <Select> branch below.
647
702
  if ((getFieldRef(field) || field.widget === 'dynamic_select') && !field.options?.length) {
648
- return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
703
+ return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange,
704
+ // Seed the trigger with the related record's NAME (from the
705
+ // backend-injected FK sibling, key without `_id`) so an existing
706
+ // selection shows the label, not the raw uuid — without waiting
707
+ // for the popover to open and fetch a page.
708
+ seedOption: fkSeedOption(field, value, record) }));
649
709
  }
650
710
  if (field.type === 'search' && field.searchEndpoint) {
651
711
  return _jsx(SearchField, { field: field, value: value, onChange: onChange });
@@ -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,CA8nBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
1
+ {"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAA;AAiC9C,OAAO,KAAK,EAAiB,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE9D,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,gBAAgB,EAAE,cAAc,MAAM,KAAG,MACzB,CAAA;AAQrD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,gBAAgB,KAAG,MAAM,GAAG,SAG5D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC7B,KAAK,gBAAgB,EACrB,OAAO,OAAO,EACd,WAAW,MAAM,EACjB,SAAS,MAAM,KAChB,MAyBF,CAAA;AA8DD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B,GAAI,QAAQ,GAAG,EAAE,KAAK,GAAG,KAAG,OAMlE,CAAA;AAqKD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,KAAG,MAGnE,CAAA;AAED,6EAA6E;AAC7E,eAAO,MAAM,eAAe,2DAA4D,CAAA;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAClB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6C5C;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAWtE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,gBAAgB,EAAE,KAAK,GAAG,KAAG,MAOtE,CAAA;AAsID;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CA+nBnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
@@ -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, itemFields: col.itemFields ?? col.item_fields }));
739
+ return (_jsx(CollectionCell, { value: value, locale: currentLanguage, t: t, itemFields: col.itemFields ?? col.item_fields, getImageUrl: getImageUrl }));
740
740
  }
741
741
  if (col.key === 'description' ||
742
742
  col.key === 'features' ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.22.0",
3
+ "version": "18.24.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-ui": "2.5.2",
68
- "@asteby/metacore-sdk": "3.2.0"
67
+ "@asteby/metacore-sdk": "3.2.0",
68
+ "@asteby/metacore-ui": "2.5.2"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsc -p tsconfig.json",
@@ -0,0 +1,170 @@
1
+ // @vitest-environment happy-dom
2
+ //
3
+ // Record EDIT dialog polish (mode='edit'), affecting every module that uses the
4
+ // generic dialog (transfers, orders, customers):
5
+ // PART 1 — a jsonb line-items field renders READ-ONLY as the inline table
6
+ // (the detail-view renderer), never an input that stringifies to
7
+ // "[object Object]".
8
+ // PART 2 — a FK select seeds its trigger from the backend-injected relation
9
+ // sibling (`source_warehouse_id` → `source_warehouse: {value,label}`)
10
+ // so an existing selection shows the related record's NAME, not the
11
+ // raw uuid.
12
+ import { afterEach, describe, expect, it, vi } from 'vitest'
13
+ import { cleanup, render, screen } from '@testing-library/react'
14
+
15
+ // Identity translator (so `defaultValue` surfaces) + Spanish locale.
16
+ vi.mock('react-i18next', () => ({
17
+ useTranslation: () => ({
18
+ t: (k: string, o?: any) => o?.defaultValue ?? k,
19
+ i18n: { language: 'es' },
20
+ }),
21
+ }))
22
+
23
+ import { EditField, isLineItemsField, fkSeedOption } from '../dialogs/dynamic-record'
24
+ import { ApiProvider, type ApiClient } from '../api-context'
25
+
26
+ afterEach(cleanup)
27
+
28
+ const noopApi = { get: vi.fn(async () => ({ data: { data: [] } })) } as unknown as ApiClient
29
+
30
+ describe('isLineItemsField', () => {
31
+ it('matches a field with a declared item_fields schema', () => {
32
+ expect(
33
+ isLineItemsField(
34
+ { key: 'items', label: 'Items', type: 'json', itemFields: [{ key: 'q', label: 'Q' }] },
35
+ null,
36
+ ),
37
+ ).toBe(true)
38
+ // snake_case alias
39
+ expect(
40
+ isLineItemsField(
41
+ { key: 'items', label: 'Items', type: 'json', item_fields: [{ key: 'q', label: 'Q' }] },
42
+ null,
43
+ ),
44
+ ).toBe(true)
45
+ })
46
+
47
+ it('matches any array / plain-object value (would stringify to [object Object])', () => {
48
+ expect(isLineItemsField({ key: 'items', label: 'Items', type: 'json' }, [{ a: 1 }])).toBe(true)
49
+ expect(isLineItemsField({ key: 'data', label: 'Data', type: 'json' }, { a: 1 })).toBe(true)
50
+ })
51
+
52
+ it('does NOT match scalars or dedicated editable widgets', () => {
53
+ expect(isLineItemsField({ key: 'name', label: 'Name', type: 'text' }, 'hi')).toBe(false)
54
+ expect(isLineItemsField({ key: 'n', label: 'N', type: 'number' }, 5)).toBe(false)
55
+ expect(isLineItemsField({ key: 'photo', label: 'Photo', type: 'image' }, { url: 'x' })).toBe(false)
56
+ expect(isLineItemsField({ key: 'f', label: 'F', type: 'text', widget: 'upload' }, {})).toBe(false)
57
+ expect(isLineItemsField({ key: 'when', label: 'When', type: 'date' }, new Date())).toBe(false)
58
+ })
59
+ })
60
+
61
+ describe('fkSeedOption', () => {
62
+ const field = { key: 'source_warehouse_id', label: 'Origen', type: 'text', ref: 'warehouses' }
63
+
64
+ it('builds a seed option from the injected {value,label} sibling', () => {
65
+ const seed = fkSeedOption(field, 'wh-1', {
66
+ source_warehouse_id: 'wh-1',
67
+ source_warehouse: { value: 'wh-1', label: 'Test2', image: 'logo.png' },
68
+ })
69
+ expect(seed).toEqual({ id: 'wh-1', value: 'wh-1', label: 'Test2', name: 'Test2', image: 'logo.png' })
70
+ })
71
+
72
+ it('returns null when there is no usable sibling', () => {
73
+ expect(fkSeedOption(field, 'wh-1', { source_warehouse_id: 'wh-1' })).toBeNull()
74
+ expect(fkSeedOption(field, '', {})).toBeNull()
75
+ expect(fkSeedOption(field, null, { source_warehouse: { label: 'x' } })).toBeNull()
76
+ })
77
+ })
78
+
79
+ describe('EditField — PART 1: jsonb line-items read-only', () => {
80
+ it('renders the inline table (read-only), not an input or [object Object]', () => {
81
+ const onChange = vi.fn()
82
+ const { container } = render(
83
+ <ApiProvider client={noopApi}>
84
+ <EditField
85
+ field={{
86
+ key: 'items',
87
+ label: 'Items',
88
+ type: 'json',
89
+ itemFields: [
90
+ { key: 'product_id', label: 'Producto', ref: 'Product' },
91
+ { key: 'quantity', label: 'Cantidad' },
92
+ ],
93
+ }}
94
+ value={[
95
+ {
96
+ product_id: '550e8400-e29b-41d4-a716-446655440000',
97
+ product: { value: 'x', label: 'Test' },
98
+ quantity: 10,
99
+ },
100
+ ]}
101
+ onChange={onChange}
102
+ record={{}}
103
+ />
104
+ </ApiProvider>,
105
+ )
106
+ // Localized headers + resolved ref label from the inline CollectionCell.
107
+ expect(screen.getByRole('columnheader', { name: 'Producto' })).toBeTruthy()
108
+ expect(screen.getByRole('cell', { name: 'Test' })).toBeTruthy()
109
+ expect(screen.getByRole('cell', { name: '10' })).toBeTruthy()
110
+ // No editable input, no [object Object], no leaked uuid, read-only hint.
111
+ expect(container.querySelector('input')).toBeNull()
112
+ expect(container.querySelector('textarea')).toBeNull()
113
+ expect(container.textContent).not.toContain('[object Object]')
114
+ expect(container.textContent).not.toContain('550e8400-e29b')
115
+ expect(screen.getByText('Solo lectura')).toBeTruthy()
116
+ })
117
+
118
+ it('renders a bare jsonb object read-only (no schema) instead of [object Object]', () => {
119
+ const { container } = render(
120
+ <ApiProvider client={noopApi}>
121
+ <EditField
122
+ field={{ key: 'fiscal_data', label: 'Fiscal', type: 'json' }}
123
+ value={{ price: 10, quantity: 20 }}
124
+ onChange={vi.fn()}
125
+ record={{}}
126
+ />
127
+ </ApiProvider>,
128
+ )
129
+ expect(container.querySelector('input')).toBeNull()
130
+ expect(container.textContent).not.toContain('[object Object]')
131
+ expect(screen.getByText('Precio:')).toBeTruthy()
132
+ })
133
+ })
134
+
135
+ describe('EditField — PART 2: FK select shows the resolved label', () => {
136
+ it('seeds the trigger with the sibling label, not the raw uuid', () => {
137
+ render(
138
+ <ApiProvider client={noopApi}>
139
+ <EditField
140
+ field={{ key: 'source_warehouse_id', label: 'Origen', type: 'text', ref: 'warehouses' }}
141
+ value={'550e8400-e29b-41d4-a716-446655440000'}
142
+ onChange={vi.fn()}
143
+ record={{
144
+ source_warehouse_id: '550e8400-e29b-41d4-a716-446655440000',
145
+ source_warehouse: { value: '550e8400-e29b-41d4-a716-446655440000', label: 'Test2' },
146
+ }}
147
+ />
148
+ </ApiProvider>,
149
+ )
150
+ // The combobox trigger shows the resolved label.
151
+ expect(screen.getByText('Test2')).toBeTruthy()
152
+ // The raw uuid is not shown.
153
+ expect(screen.queryByText('550e8400-e29b-41d4-a716-446655440000')).toBeNull()
154
+ })
155
+
156
+ it('keeps the raw value as fallback when no sibling was injected', () => {
157
+ render(
158
+ <ApiProvider client={noopApi}>
159
+ <EditField
160
+ field={{ key: 'source_warehouse_id', label: 'Origen', type: 'text', ref: 'warehouses' }}
161
+ value={'wh-xyz'}
162
+ onChange={vi.fn()}
163
+ record={{ source_warehouse_id: 'wh-xyz' }}
164
+ />
165
+ </ApiProvider>,
166
+ )
167
+ // No sibling → existing behaviour: the raw value is shown on the trigger.
168
+ expect(screen.getByText('wh-xyz')).toBeTruthy()
169
+ })
170
+ })
@@ -6,8 +6,11 @@
6
6
  // mini-table — no per-addon config required, safe on any shape.
7
7
 
8
8
  import * as React from 'react'
9
- import { List } from 'lucide-react'
9
+ import { Box, List } from 'lucide-react'
10
10
  import {
11
+ Avatar,
12
+ AvatarFallback,
13
+ AvatarImage,
11
14
  Badge,
12
15
  Popover,
13
16
  PopoverContent,
@@ -20,8 +23,35 @@ import {
20
23
  TableRow,
21
24
  cn,
22
25
  } from '@asteby/metacore-ui'
26
+ import { getInitials, relationChipStyles } from '@asteby/metacore-ui/lib'
23
27
  import { humanizeToken } from './dynamic-columns-helpers'
24
28
 
29
+ /**
30
+ * Tracks the host's dark-mode class on <html> so relation chips pick a tint
31
+ * tuned for the active theme. Replicated from dynamic-columns (kept local to
32
+ * avoid a cross-module import); mirror changes if that one evolves.
33
+ */
34
+ function useIsDarkTheme(): boolean {
35
+ const [isDark, setIsDark] = React.useState(
36
+ () =>
37
+ typeof document !== 'undefined' &&
38
+ document.documentElement.classList.contains('dark')
39
+ )
40
+ React.useEffect(() => {
41
+ if (typeof document === 'undefined') return
42
+ const sync = () =>
43
+ setIsDark(document.documentElement.classList.contains('dark'))
44
+ sync()
45
+ const observer = new MutationObserver(sync)
46
+ observer.observe(document.documentElement, {
47
+ attributes: true,
48
+ attributeFilter: ['class'],
49
+ })
50
+ return () => observer.disconnect()
51
+ }, [])
52
+ return isDark
53
+ }
54
+
25
55
  const UUID_LIKE_RE =
26
56
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
27
57
 
@@ -56,31 +86,105 @@ function siblingKeyFor(key: string): string {
56
86
  return key.endsWith('_id') ? key.slice(0, -3) : `${key}_label`
57
87
  }
58
88
 
89
+ /** The backend-injected resolved sibling for a ref item-field. */
90
+ interface ResolvedRef {
91
+ label?: string
92
+ value?: unknown
93
+ /** Optional thumbnail (product photo, logo, avatar) resolved by the backend. */
94
+ image?: string
95
+ }
96
+
59
97
  /**
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)`.
98
+ * Reads the backend-injected resolved sibling for a `ref` item-field (the FK
99
+ * key without `_id`, else `<key>_label`). Returns the normalized `{label,
100
+ * image}` when present, a `{label}` for a bare-string sibling, or null when the
101
+ * ref is unresolved (so the caller falls back to the raw value).
65
102
  */
66
- function renderItemFieldValue(
103
+ function resolvedRefFor(
67
104
  field: ItemField,
68
105
  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)
106
+ ): ResolvedRef | null {
107
+ if (!field.ref) return null
108
+ const sibling = row[siblingKeyFor(field.key)]
109
+ if (sibling && typeof sibling === 'object' && !Array.isArray(sibling)) {
110
+ const obj = sibling as Record<string, unknown>
111
+ const label = obj.label
112
+ if (label !== undefined && label !== null && label !== '') {
113
+ return {
114
+ label: String(label),
115
+ value: obj.value,
116
+ image:
117
+ typeof obj.image === 'string' && obj.image !== ''
118
+ ? obj.image
119
+ : undefined,
76
120
  }
77
- } else if (typeof sibling === 'string' && sibling !== '') {
78
- return sibling
79
121
  }
122
+ } else if (typeof sibling === 'string' && sibling !== '') {
123
+ return { label: sibling }
80
124
  }
125
+ return null
126
+ }
127
+
128
+ /**
129
+ * Plain-text value for one declared item-field — used for the no-JS popover
130
+ * `title` tooltip (which can't render JSX). Ref fields show the resolved label;
131
+ * everything else uses `formatScalar` (truncated uuid for unresolved ids).
132
+ */
133
+ function itemFieldText(field: ItemField, row: Record<string, unknown>): string {
134
+ const ref = resolvedRefFor(field, row)
135
+ if (ref?.label) return ref.label
81
136
  return formatScalar(row[field.key])
82
137
  }
83
138
 
139
+ /**
140
+ * Visual cell for one declared item-field. A resolved `ref` renders as a
141
+ * relation chip (subtle deterministic tint + product thumbnail or a generic
142
+ * entity icon + name) — the same "pro" look the table FK columns use — so jsonb
143
+ * line items read like first-class relations instead of raw uuids. Non-ref (or
144
+ * unresolved) fields render the plain scalar.
145
+ */
146
+ function ItemFieldCell({
147
+ field,
148
+ row,
149
+ getImageUrl,
150
+ }: {
151
+ field: ItemField
152
+ row: Record<string, unknown>
153
+ getImageUrl?: (path: string) => string
154
+ }): React.ReactElement {
155
+ const isDark = useIsDarkTheme()
156
+ const ref = resolvedRefFor(field, row)
157
+ if (ref?.label) {
158
+ return (
159
+ <span
160
+ className="inline-flex max-w-[220px] items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium"
161
+ style={relationChipStyles(ref.label, { isDark })}
162
+ title={ref.label}
163
+ >
164
+ {ref.image ? (
165
+ <Avatar
166
+ className="shrink-0 rounded-sm ring-1 ring-border/40"
167
+ style={{ width: 18, height: 18 }}
168
+ >
169
+ <AvatarImage
170
+ src={getImageUrl ? getImageUrl(ref.image) : ref.image}
171
+ alt={ref.label}
172
+ className="object-cover"
173
+ />
174
+ <AvatarFallback className="rounded-sm bg-primary/10 text-[8px] font-bold text-primary">
175
+ {getInitials(ref.label)}
176
+ </AvatarFallback>
177
+ </Avatar>
178
+ ) : (
179
+ <Box className="h-3 w-3 shrink-0 opacity-70" />
180
+ )}
181
+ <span className="truncate">{ref.label}</span>
182
+ </span>
183
+ )
184
+ }
185
+ return <>{formatScalar(row[field.key])}</>
186
+ }
187
+
84
188
  /** Normalize an org/UI language tag to a base language code (`es-MX` → `es`). */
85
189
  function baseLang(locale?: string): string {
86
190
  return (locale || 'en').toLowerCase().split('-')[0]
@@ -258,11 +362,13 @@ function MiniTable({
258
362
  locale,
259
363
  t,
260
364
  itemFields,
365
+ getImageUrl,
261
366
  }: {
262
367
  rows: Record<string, unknown>[]
263
368
  locale?: string
264
369
  t?: Translate
265
370
  itemFields?: ItemField[]
371
+ getImageUrl?: (path: string) => string
266
372
  }) {
267
373
  // Schema-driven path: a declared `item_fields` schema fixes the column
268
374
  // order + headers (already localized by the backend, used VERBATIM) and
@@ -292,7 +398,11 @@ function MiniTable({
292
398
  key={field.key}
293
399
  className="text-xs whitespace-nowrap"
294
400
  >
295
- {renderItemFieldValue(field, row)}
401
+ <ItemFieldCell
402
+ field={field}
403
+ row={row}
404
+ getImageUrl={getImageUrl}
405
+ />
296
406
  </TableCell>
297
407
  ))}
298
408
  </TableRow>
@@ -421,6 +531,12 @@ export interface CollectionCellProps {
421
531
  * behaviour is unchanged.
422
532
  */
423
533
  itemFields?: ItemField[]
534
+ /**
535
+ * Resolves a stored image path to a displayable URL (same resolver the table
536
+ * columns use). Threaded to the ref-chip thumbnails so resolved line-item
537
+ * relations show a product photo / logo / avatar like the FK columns do.
538
+ */
539
+ getImageUrl?: (path: string) => string
424
540
  /**
425
541
  * Presentation mode.
426
542
  * - `'badge'` (default): the compact count/preview badge that opens a
@@ -448,6 +564,7 @@ export function CollectionCell({
448
564
  locale,
449
565
  t,
450
566
  itemFields,
567
+ getImageUrl,
451
568
  variant = 'badge',
452
569
  }: CollectionCellProps) {
453
570
  const parsed = parseValue(value)
@@ -491,6 +608,7 @@ export function CollectionCell({
491
608
  locale={locale}
492
609
  t={t}
493
610
  itemFields={itemFields}
611
+ getImageUrl={getImageUrl}
494
612
  />
495
613
  )
496
614
  }
@@ -506,7 +624,7 @@ export function CollectionCell({
506
624
  itemFields!
507
625
  .map(
508
626
  (field) =>
509
- `${field.label}: ${renderItemFieldValue(field, row)}`
627
+ `${field.label}: ${itemFieldText(field, row)}`
510
628
  )
511
629
  .join(', ')
512
630
  )
@@ -528,6 +646,7 @@ export function CollectionCell({
528
646
  locale={locale}
529
647
  t={t}
530
648
  itemFields={itemFields}
649
+ getImageUrl={getImageUrl}
531
650
  />
532
651
  </PopoverShell>
533
652
  )
@@ -294,6 +294,51 @@ function relationSiblingValue(field: FieldDef, record: any): any {
294
294
  return undefined
295
295
  }
296
296
 
297
+ // fieldItemFields reads the declared jsonb line-items schema off a field,
298
+ // tolerating the snake_case `item_fields` alias the kernel serves.
299
+ function fieldItemFields(field: FieldDef): ItemField[] | undefined {
300
+ return field.itemFields ?? field.item_fields
301
+ }
302
+
303
+ // isLineItemsField — a jsonb line-items column (e.g. Transfer.items): it either
304
+ // declares an `item_fields` schema, or its value is a structured array/object.
305
+ // These are action-built documents; field-by-field editing of the array is out
306
+ // of scope, so the edit dialog renders them read-only (the inline table) rather
307
+ // than an input that would stringify to "[object Object]". Scalars and known
308
+ // editable widgets (media/upload, color, dates) are NOT line-items.
309
+ export function isLineItemsField(field: FieldDef, value: any): boolean {
310
+ if (field.type === 'image' || field.widget === 'upload') return false
311
+ if (fieldItemFields(field)?.length) return true
312
+ if (Array.isArray(value)) return true
313
+ return (
314
+ value !== null &&
315
+ typeof value === 'object' &&
316
+ !(value instanceof Date)
317
+ )
318
+ }
319
+
320
+ // fkSeedOption builds the pre-resolved option for a FK select's CURRENT value
321
+ // from the relation sibling the backend injected alongside the column
322
+ // (`source_warehouse_id` → `source_warehouse = {value,label,image?}`, the key
323
+ // without `_id`; same convention as the jsonb refs). Lets the edit picker show
324
+ // the related record's NAME instead of the raw uuid without waiting for a
325
+ // network lookup. Returns null when there is no usable sibling (the picker then
326
+ // falls back to its existing typeahead/lookup behaviour).
327
+ export function fkSeedOption(field: FieldDef, value: any, record: any): ResolvedOption | null {
328
+ if (value === undefined || value === null || value === '') return null
329
+ const sib = relationSiblingValue(field, record)
330
+ const label = typeof sib === 'string' ? sib : objectLabel(sib)
331
+ if (!label) return null
332
+ const id = String(value)
333
+ return {
334
+ id,
335
+ value: id,
336
+ label,
337
+ name: label,
338
+ image: pickImage(sib) ?? null,
339
+ }
340
+ }
341
+
297
342
  // servedOption matches a field's served option list (enum/select with
298
343
  // {value,label,color,icon,image}) against the current value.
299
344
  function servedOption(field: FieldDef, value: any): FieldOption | undefined {
@@ -846,7 +891,7 @@ function FieldRow({ field, record, value, mode, onChange }: FieldRowProps) {
846
891
  {isReadonly ? (
847
892
  <ViewValue field={field} value={value} record={record} />
848
893
  ) : (
849
- <EditField field={field} value={value} onChange={onChange} />
894
+ <EditField field={field} value={value} onChange={onChange} record={record} />
850
895
  )}
851
896
  </div>
852
897
  )
@@ -1137,6 +1182,7 @@ function StructuredViewValue({
1137
1182
  locale?: string
1138
1183
  t?: (key: string, options?: any) => string
1139
1184
  }) {
1185
+ const getImageUrl = useContext(ImageUrlContext)
1140
1186
  const isEmpty =
1141
1187
  value === null ||
1142
1188
  value === undefined ||
@@ -1156,16 +1202,46 @@ function StructuredViewValue({
1156
1202
  variant="inline"
1157
1203
  locale={locale}
1158
1204
  t={t}
1205
+ getImageUrl={getImageUrl}
1159
1206
  />
1160
1207
  </div>
1161
1208
  )
1162
1209
  }
1163
1210
 
1164
- function EditField({ field, value, onChange }: {
1211
+ export function EditField({ field, value, onChange, record }: {
1165
1212
  field: FieldDef
1166
1213
  value: any
1167
1214
  onChange: (val: any) => void
1215
+ /** The full record being edited — supplies FK relation siblings + line-items. */
1216
+ record?: any
1168
1217
  }) {
1218
+ const { t, i18n } = useTranslation()
1219
+ const editFieldImageUrl = useContext(ImageUrlContext)
1220
+
1221
+ // Jsonb line-items columns (e.g. Transfer.items) are action-built documents:
1222
+ // editing the array field-by-field is out of scope. Render them READ-ONLY
1223
+ // with the same inline table the detail view uses — a localized, ref-resolved
1224
+ // mini-table — instead of an input that stringifies to "[object Object]".
1225
+ if (isLineItemsField(field, value)) {
1226
+ return (
1227
+ <div className="space-y-1">
1228
+ <div className="rounded-md border bg-muted/30 p-2">
1229
+ <CollectionCell
1230
+ value={value}
1231
+ itemFields={fieldItemFields(field)}
1232
+ variant="inline"
1233
+ locale={i18n.language}
1234
+ t={t}
1235
+ getImageUrl={editFieldImageUrl}
1236
+ />
1237
+ </div>
1238
+ <p className="text-[11px] text-muted-foreground">
1239
+ {t('datatable.readOnly', { defaultValue: 'Solo lectura' })}
1240
+ </p>
1241
+ </div>
1242
+ )
1243
+ }
1244
+
1169
1245
  if (field.type === 'boolean') {
1170
1246
  return (
1171
1247
  <div className="flex items-center gap-2 py-1">
@@ -1202,7 +1278,18 @@ function EditField({ field, value, onChange }: {
1202
1278
  // option thumbnails and the inline-create "+" — against /api/options/<ref>.
1203
1279
  // Static inline `options` are handled by the enum <Select> branch below.
1204
1280
  if ((getFieldRef(field as ActionFieldDef) || field.widget === 'dynamic_select') && !field.options?.length) {
1205
- return <DynamicSelectField field={field as ActionFieldDef} value={value} onChange={onChange} />
1281
+ return (
1282
+ <DynamicSelectField
1283
+ field={field as ActionFieldDef}
1284
+ value={value}
1285
+ onChange={onChange}
1286
+ // Seed the trigger with the related record's NAME (from the
1287
+ // backend-injected FK sibling, key without `_id`) so an existing
1288
+ // selection shows the label, not the raw uuid — without waiting
1289
+ // for the popover to open and fetch a page.
1290
+ seedOption={fkSeedOption(field, value, record)}
1291
+ />
1292
+ )
1206
1293
  }
1207
1294
 
1208
1295
  if (field.type === 'search' && field.searchEndpoint) {
@@ -1180,6 +1180,7 @@ export function makeDefaultGetDynamicColumns(
1180
1180
  locale={currentLanguage}
1181
1181
  t={t}
1182
1182
  itemFields={col.itemFields ?? col.item_fields}
1183
+ getImageUrl={getImageUrl}
1183
1184
  />
1184
1185
  )
1185
1186
  }