@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 +44 -0
- package/dist/collection-cell.d.ts +7 -1
- package/dist/collection-cell.d.ts.map +1 -1
- package/dist/collection-cell.js +87 -25
- package/dist/dialogs/dynamic-record.d.ts +10 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +64 -4
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +1 -1
- package/package.json +3 -3
- package/src/__tests__/edit-field-polish.test.tsx +170 -0
- package/src/collection-cell.tsx +137 -18
- package/src/dialogs/dynamic-record.tsx +90 -3
- package/src/dynamic-columns.tsx +1 -0
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;
|
|
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"}
|
package/dist/collection-cell.js
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
22
|
-
if (field.ref)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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:
|
|
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}: ${
|
|
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":"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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"}
|
|
@@ -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,
|
|
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"}
|
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, 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.
|
|
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-
|
|
68
|
-
"@asteby/metacore-
|
|
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
|
+
})
|
package/src/collection-cell.tsx
CHANGED
|
@@ -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
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
103
|
+
function resolvedRefFor(
|
|
67
104
|
field: ItemField,
|
|
68
105
|
row: Record<string, unknown>,
|
|
69
|
-
):
|
|
70
|
-
if (field.ref)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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}: ${
|
|
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
|
|
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) {
|