@asteby/metacore-runtime-react 18.24.0 → 18.26.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 +39 -0
- package/dist/activity-diff.d.ts.map +1 -1
- package/dist/activity-diff.js +8 -0
- package/dist/activity-value-renderer.d.ts.map +1 -1
- package/dist/activity-value-renderer.js +36 -0
- package/dist/collection-cell.d.ts.map +1 -1
- package/dist/collection-cell.js +50 -7
- package/package.json +1 -1
- package/src/activity-diff.tsx +7 -0
- package/src/activity-value-renderer.tsx +47 -0
- package/src/collection-cell.tsx +102 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.26.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d0056f1: Activity log (record history) renders jsonb line items as a table and localizes
|
|
8
|
+
relation field labels.
|
|
9
|
+
- **Line-items render as the shared `CollectionCell` mini-table** instead of raw
|
|
10
|
+
`JSON.stringify`. A jsonb array-of-objects value (e.g. a transfer's `items`,
|
|
11
|
+
directly or JSON-string-encoded) now shows a localized mini-table with
|
|
12
|
+
resolved relation chips (when the backend injects the `{value,label,image}`
|
|
13
|
+
siblings into the snapshot) — matching the detail view. Uses the column's
|
|
14
|
+
declared `item_fields` when present.
|
|
15
|
+
- **Relation field labels localize.** `resolveColumn` now matches the `*_id`
|
|
16
|
+
twin of a resolved relation key (`destination_warehouse` →
|
|
17
|
+
`destination_warehouse_id`), so the diff "Campo" uses the localized column
|
|
18
|
+
label ("Almacén destino") instead of humanizing the key in English
|
|
19
|
+
("Destination Warehouse").
|
|
20
|
+
|
|
21
|
+
## 18.25.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- adb1c52: `CollectionCell` renders resolved relation references as "pro" chips in EVERY
|
|
26
|
+
path — including the schema-less generic one.
|
|
27
|
+
|
|
28
|
+
Previously only the declared-`item_fields` path resolved a jsonb line-item ref
|
|
29
|
+
(e.g. `product_id`) to a relation chip. The generic path (used by the full-page
|
|
30
|
+
record detail and any jsonb without a declared schema) dumped the
|
|
31
|
+
backend-injected resolved sibling object as raw `"{…}"` AND showed the raw uuid
|
|
32
|
+
in a duplicate column. Now the generic path:
|
|
33
|
+
- detects the backend-injected `{ value, label, image }` ref siblings,
|
|
34
|
+
- renders them as the same relation chip (subtle tint + thumbnail or entity icon
|
|
35
|
+
- name) the FK table columns use, and
|
|
36
|
+
- hides the raw `<key>_id` twin column,
|
|
37
|
+
|
|
38
|
+
so an unconfigured jsonb line-items blob reads as first-class relations
|
|
39
|
+
(foto/nombre) instead of uuid soup. The shared chip is extracted as `RefChip`
|
|
40
|
+
and reused by the schema (`ItemFieldCell`) and generic paths.
|
|
41
|
+
|
|
3
42
|
## 18.24.0
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-diff.d.ts","sourceRoot":"","sources":["../src/activity-diff.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oCAAoC;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;
|
|
1
|
+
{"version":3,"file":"activity-diff.d.ts","sourceRoot":"","sources":["../src/activity-diff.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAO/C;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,iBAAiB;IAC9B,oCAAoC;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AA8GD;;;;GAIG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA4KpD,CAAA"}
|
package/dist/activity-diff.js
CHANGED
|
@@ -81,6 +81,14 @@ function resolveColumn(key, columns) {
|
|
|
81
81
|
const exact = columns.find((c) => c.key === key);
|
|
82
82
|
if (exact)
|
|
83
83
|
return exact;
|
|
84
|
+
// A resolved relation key is the FK column minus `_id` (the backend injects a
|
|
85
|
+
// `destination_warehouse` sibling next to `destination_warehouse_id`). The
|
|
86
|
+
// served metadata carries the LOCALIZED label on the `*_id` column, so match
|
|
87
|
+
// it — else the label falls back to humanizing the key in English
|
|
88
|
+
// ("Destination Warehouse" instead of "Almacén destino").
|
|
89
|
+
const fk = columns.find((c) => c.key === `${key}_id`);
|
|
90
|
+
if (fk)
|
|
91
|
+
return fk;
|
|
84
92
|
// A diff key is the physical column (created_by); the served metadata may
|
|
85
93
|
// only carry the dotted display column for it (created_by.avatar). Match on
|
|
86
94
|
// the base segment so the diff cell inherits its label and rich renderer.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-value-renderer.d.ts","sourceRoot":"","sources":["../src/activity-value-renderer.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAU9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"activity-value-renderer.d.ts","sourceRoot":"","sources":["../src/activity-value-renderer.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAU9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAqG/C,MAAM,WAAW,0BAA0B;IACvC,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAA;IACd,+EAA+E;IAC/E,GAAG,CAAC,EAAE,gBAAgB,CAAA;IACtB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAqStE,CAAA"}
|
|
@@ -17,6 +17,32 @@ import { Badge, Avatar, AvatarImage, AvatarFallback, } from '@asteby/metacore-ui
|
|
|
17
17
|
import { generateBadgeStyles, getInitials, optionColor, relationChipStyles } from '@asteby/metacore-ui/lib';
|
|
18
18
|
import { formatDateCell } from './dynamic-columns';
|
|
19
19
|
import { humanizeToken } from './dynamic-columns-helpers';
|
|
20
|
+
import { CollectionCell } from './collection-cell';
|
|
21
|
+
/**
|
|
22
|
+
* Parses a value into a jsonb line-items array (array of plain objects) when it
|
|
23
|
+
* is one — directly, or from a JSON-encoded string. Returns null otherwise, so
|
|
24
|
+
* scalars / resolved-entity objects keep their existing renderers.
|
|
25
|
+
*/
|
|
26
|
+
function asLineItemsArray(value) {
|
|
27
|
+
let arr = value;
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
const t = value.trim();
|
|
30
|
+
if (!t.startsWith('['))
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
arr = JSON.parse(t);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(arr) &&
|
|
40
|
+
arr.length > 0 &&
|
|
41
|
+
arr.every((v) => v !== null && typeof v === 'object' && !Array.isArray(v))) {
|
|
42
|
+
return arr;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
20
46
|
// ---------------------------------------------------------------------------
|
|
21
47
|
// Internal helpers (mirror dynamic-columns.tsx private helpers)
|
|
22
48
|
// ---------------------------------------------------------------------------
|
|
@@ -81,6 +107,16 @@ export const ActivityValueRenderer = ({ value, col, timeZone, currency, locale =
|
|
|
81
107
|
if (value === null || value === undefined || value === '') {
|
|
82
108
|
return _jsx("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
83
109
|
}
|
|
110
|
+
// jsonb line-items (array of objects, e.g. a transfer's `items`) → the shared
|
|
111
|
+
// CollectionCell mini-table (localized headers + resolved ref chips) instead
|
|
112
|
+
// of raw JSON. Uses the column's declared item_fields when present.
|
|
113
|
+
const lineItems = asLineItemsArray(value);
|
|
114
|
+
if (lineItems) {
|
|
115
|
+
return (_jsx(CollectionCell, { value: lineItems, variant: "inline", itemFields: col
|
|
116
|
+
?.itemFields ??
|
|
117
|
+
col
|
|
118
|
+
?.item_fields, locale: locale }));
|
|
119
|
+
}
|
|
84
120
|
// No column metadata → entity chip when the value is a resolved object,
|
|
85
121
|
// plain string otherwise.
|
|
86
122
|
if (!col) {
|
|
@@ -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;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;
|
|
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;AAoND;;;;;;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;AAiOD,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
|
@@ -80,18 +80,44 @@ function itemFieldText(field, row) {
|
|
|
80
80
|
return ref.label;
|
|
81
81
|
return formatScalar(row[field.key]);
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* A backend-resolved relation reference shaped `{ value, label, image? }` — the
|
|
85
|
+
* sibling the backend injects for a ref (FK column or jsonb item-field), the
|
|
86
|
+
* SAME shape the relation table columns use. Lets a jsonb line item read as a
|
|
87
|
+
* first-class relation, not a raw uuid.
|
|
88
|
+
*/
|
|
89
|
+
function resolvedRefObject(v) {
|
|
90
|
+
if (!isPlainObject(v))
|
|
91
|
+
return null;
|
|
92
|
+
const label = v.label;
|
|
93
|
+
if (label === undefined || label === null || label === '')
|
|
94
|
+
return null;
|
|
95
|
+
return {
|
|
96
|
+
label: String(label),
|
|
97
|
+
value: v.value,
|
|
98
|
+
image: typeof v.image === 'string' && v.image !== '' ? v.image : undefined,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The "pro" relation chip — subtle deterministic tint + the related record's
|
|
103
|
+
* thumbnail (product photo / logo / avatar) or a generic entity icon + the
|
|
104
|
+
* resolved name. The exact look the FK table columns use, so resolved relations
|
|
105
|
+
* read consistently everywhere (table popover, detail view, edit, line items).
|
|
106
|
+
*/
|
|
107
|
+
function RefChip({ label, image, getImageUrl, }) {
|
|
108
|
+
const isDark = useIsDarkTheme();
|
|
109
|
+
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(label, { isDark }), title: label, children: [image ? (_jsxs(Avatar, { className: "shrink-0 rounded-sm ring-1 ring-border/40", style: { width: 18, height: 18 }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(image) : image, alt: label, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-sm bg-primary/10 text-[8px] font-bold text-primary", children: getInitials(label) })] })) : (_jsx(Box, { className: "h-3 w-3 shrink-0 opacity-70" })), _jsx("span", { className: "truncate", children: label })] }));
|
|
110
|
+
}
|
|
83
111
|
/**
|
|
84
112
|
* Visual cell for one declared item-field. A resolved `ref` renders as a
|
|
85
|
-
* relation chip (
|
|
86
|
-
*
|
|
87
|
-
* line items read like first-class relations instead of raw uuids. Non-ref (or
|
|
113
|
+
* relation chip (foto/ícono + nombre) — the "pro" FK-column look — so jsonb line
|
|
114
|
+
* items read like first-class relations instead of raw uuids. Non-ref (or
|
|
88
115
|
* unresolved) fields render the plain scalar.
|
|
89
116
|
*/
|
|
90
117
|
function ItemFieldCell({ field, row, getImageUrl, }) {
|
|
91
|
-
const isDark = useIsDarkTheme();
|
|
92
118
|
const ref = resolvedRefFor(field, row);
|
|
93
119
|
if (ref?.label) {
|
|
94
|
-
return (
|
|
120
|
+
return (_jsx(RefChip, { label: ref.label, image: ref.image, getImageUrl: getImageUrl }));
|
|
95
121
|
}
|
|
96
122
|
return _jsx(_Fragment, { children: formatScalar(row[field.key]) });
|
|
97
123
|
}
|
|
@@ -265,11 +291,28 @@ function MiniTable({ rows, locale, t, itemFields, getImageUrl, }) {
|
|
|
265
291
|
if (itemFields && itemFields.length > 0) {
|
|
266
292
|
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))) })] }));
|
|
267
293
|
}
|
|
268
|
-
|
|
294
|
+
// Generic (no-schema) path — still relation-AWARE. The backend injects a
|
|
295
|
+
// resolved-ref sibling object (`{value,label,image}`) next to each FK id
|
|
296
|
+
// inside the items (e.g. `product` next to `product_id`). Without a declared
|
|
297
|
+
// schema we'd otherwise dump that object as "{…}" AND the raw uuid in a
|
|
298
|
+
// duplicate column. Instead: detect those sibling objects, render them as
|
|
299
|
+
// relation chips, and HIDE their raw `<key>_id` twin — so even an
|
|
300
|
+
// unconfigured jsonb blob reads "pro" (foto/nombre, no uuid soup).
|
|
301
|
+
const allKeys = unionKeys(rows);
|
|
302
|
+
const refKeys = new Set(allKeys.filter((k) => rows.some((r) => resolvedRefObject(r[k]) !== null)));
|
|
303
|
+
const hiddenTwins = new Set();
|
|
304
|
+
for (const rk of refKeys)
|
|
305
|
+
hiddenTwins.add(`${rk}_id`);
|
|
306
|
+
const keys = allKeys.filter((k) => !hiddenTwins.has(k));
|
|
269
307
|
if (keys.length === 0) {
|
|
270
308
|
return _jsx("div", { className: "p-3 text-xs text-muted-foreground", children: "-" });
|
|
271
309
|
}
|
|
272
|
-
return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: keys.map((key) => (_jsx(TableHead, { className: "text-xs whitespace-nowrap", children: prettifyKey(key, locale, t) }, key))) }) }), _jsx(TableBody, { children: rows.map((row, i) => (_jsx(TableRow, { children: keys.map((key) =>
|
|
310
|
+
return (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: keys.map((key) => (_jsx(TableHead, { className: "text-xs whitespace-nowrap", children: prettifyKey(key, locale, t) }, key))) }) }), _jsx(TableBody, { children: rows.map((row, i) => (_jsx(TableRow, { children: keys.map((key) => {
|
|
311
|
+
const ref = refKeys.has(key)
|
|
312
|
+
? resolvedRefObject(row[key])
|
|
313
|
+
: null;
|
|
314
|
+
return (_jsx(TableCell, { className: "text-xs whitespace-nowrap", children: ref ? (_jsx(RefChip, { label: ref.label, image: ref.image, getImageUrl: getImageUrl })) : (formatScalar(row[key])) }, key));
|
|
315
|
+
}) }, i))) })] }));
|
|
273
316
|
}
|
|
274
317
|
function ScalarList({ values }) {
|
|
275
318
|
return (_jsx("ul", { className: "p-3 space-y-1", children: values.map((v, i) => (_jsx("li", { className: "text-xs text-foreground", children: formatScalar(v) }, i))) }));
|
package/package.json
CHANGED
package/src/activity-diff.tsx
CHANGED
|
@@ -132,6 +132,13 @@ function resolveColumn(key: string, columns?: ColumnDefinition[]): ColumnDefinit
|
|
|
132
132
|
if (!columns?.length) return undefined
|
|
133
133
|
const exact = columns.find((c) => c.key === key)
|
|
134
134
|
if (exact) return exact
|
|
135
|
+
// A resolved relation key is the FK column minus `_id` (the backend injects a
|
|
136
|
+
// `destination_warehouse` sibling next to `destination_warehouse_id`). The
|
|
137
|
+
// served metadata carries the LOCALIZED label on the `*_id` column, so match
|
|
138
|
+
// it — else the label falls back to humanizing the key in English
|
|
139
|
+
// ("Destination Warehouse" instead of "Almacén destino").
|
|
140
|
+
const fk = columns.find((c) => c.key === `${key}_id`)
|
|
141
|
+
if (fk) return fk
|
|
135
142
|
// A diff key is the physical column (created_by); the served metadata may
|
|
136
143
|
// only carry the dotted display column for it (created_by.avatar). Match on
|
|
137
144
|
// the base segment so the diff cell inherits its label and rich renderer.
|
|
@@ -23,6 +23,33 @@ import { generateBadgeStyles, getInitials, optionColor, relationChipStyles } fro
|
|
|
23
23
|
import type { ColumnDefinition } from './types'
|
|
24
24
|
import { formatDateCell } from './dynamic-columns'
|
|
25
25
|
import { humanizeToken } from './dynamic-columns-helpers'
|
|
26
|
+
import { CollectionCell, type ItemField } from './collection-cell'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parses a value into a jsonb line-items array (array of plain objects) when it
|
|
30
|
+
* is one — directly, or from a JSON-encoded string. Returns null otherwise, so
|
|
31
|
+
* scalars / resolved-entity objects keep their existing renderers.
|
|
32
|
+
*/
|
|
33
|
+
function asLineItemsArray(value: unknown): Record<string, unknown>[] | null {
|
|
34
|
+
let arr: unknown = value
|
|
35
|
+
if (typeof value === 'string') {
|
|
36
|
+
const t = value.trim()
|
|
37
|
+
if (!t.startsWith('[')) return null
|
|
38
|
+
try {
|
|
39
|
+
arr = JSON.parse(t)
|
|
40
|
+
} catch {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (
|
|
45
|
+
Array.isArray(arr) &&
|
|
46
|
+
arr.length > 0 &&
|
|
47
|
+
arr.every((v) => v !== null && typeof v === 'object' && !Array.isArray(v))
|
|
48
|
+
) {
|
|
49
|
+
return arr as Record<string, unknown>[]
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
26
53
|
|
|
27
54
|
// ---------------------------------------------------------------------------
|
|
28
55
|
// Internal helpers (mirror dynamic-columns.tsx private helpers)
|
|
@@ -128,6 +155,26 @@ export const ActivityValueRenderer: React.FC<ActivityValueRendererProps> = ({
|
|
|
128
155
|
return <span className="text-muted-foreground">—</span>
|
|
129
156
|
}
|
|
130
157
|
|
|
158
|
+
// jsonb line-items (array of objects, e.g. a transfer's `items`) → the shared
|
|
159
|
+
// CollectionCell mini-table (localized headers + resolved ref chips) instead
|
|
160
|
+
// of raw JSON. Uses the column's declared item_fields when present.
|
|
161
|
+
const lineItems = asLineItemsArray(value)
|
|
162
|
+
if (lineItems) {
|
|
163
|
+
return (
|
|
164
|
+
<CollectionCell
|
|
165
|
+
value={lineItems}
|
|
166
|
+
variant="inline"
|
|
167
|
+
itemFields={
|
|
168
|
+
(col as { itemFields?: ItemField[]; item_fields?: ItemField[] })
|
|
169
|
+
?.itemFields ??
|
|
170
|
+
(col as { itemFields?: ItemField[]; item_fields?: ItemField[] })
|
|
171
|
+
?.item_fields
|
|
172
|
+
}
|
|
173
|
+
locale={locale}
|
|
174
|
+
/>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
131
178
|
// No column metadata → entity chip when the value is a resolved object,
|
|
132
179
|
// plain string otherwise.
|
|
133
180
|
if (!col) {
|
package/src/collection-cell.tsx
CHANGED
|
@@ -136,11 +136,74 @@ function itemFieldText(field: ItemField, row: Record<string, unknown>): string {
|
|
|
136
136
|
return formatScalar(row[field.key])
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* A backend-resolved relation reference shaped `{ value, label, image? }` — the
|
|
141
|
+
* sibling the backend injects for a ref (FK column or jsonb item-field), the
|
|
142
|
+
* SAME shape the relation table columns use. Lets a jsonb line item read as a
|
|
143
|
+
* first-class relation, not a raw uuid.
|
|
144
|
+
*/
|
|
145
|
+
function resolvedRefObject(
|
|
146
|
+
v: unknown,
|
|
147
|
+
): (ResolvedRef & { label: string }) | null {
|
|
148
|
+
if (!isPlainObject(v)) return null
|
|
149
|
+
const label = v.label
|
|
150
|
+
if (label === undefined || label === null || label === '') return null
|
|
151
|
+
return {
|
|
152
|
+
label: String(label),
|
|
153
|
+
value: v.value,
|
|
154
|
+
image:
|
|
155
|
+
typeof v.image === 'string' && v.image !== '' ? v.image : undefined,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The "pro" relation chip — subtle deterministic tint + the related record's
|
|
161
|
+
* thumbnail (product photo / logo / avatar) or a generic entity icon + the
|
|
162
|
+
* resolved name. The exact look the FK table columns use, so resolved relations
|
|
163
|
+
* read consistently everywhere (table popover, detail view, edit, line items).
|
|
164
|
+
*/
|
|
165
|
+
function RefChip({
|
|
166
|
+
label,
|
|
167
|
+
image,
|
|
168
|
+
getImageUrl,
|
|
169
|
+
}: {
|
|
170
|
+
label: string
|
|
171
|
+
image?: string
|
|
172
|
+
getImageUrl?: (path: string) => string
|
|
173
|
+
}): React.ReactElement {
|
|
174
|
+
const isDark = useIsDarkTheme()
|
|
175
|
+
return (
|
|
176
|
+
<span
|
|
177
|
+
className="inline-flex max-w-[220px] items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium"
|
|
178
|
+
style={relationChipStyles(label, { isDark })}
|
|
179
|
+
title={label}
|
|
180
|
+
>
|
|
181
|
+
{image ? (
|
|
182
|
+
<Avatar
|
|
183
|
+
className="shrink-0 rounded-sm ring-1 ring-border/40"
|
|
184
|
+
style={{ width: 18, height: 18 }}
|
|
185
|
+
>
|
|
186
|
+
<AvatarImage
|
|
187
|
+
src={getImageUrl ? getImageUrl(image) : image}
|
|
188
|
+
alt={label}
|
|
189
|
+
className="object-cover"
|
|
190
|
+
/>
|
|
191
|
+
<AvatarFallback className="rounded-sm bg-primary/10 text-[8px] font-bold text-primary">
|
|
192
|
+
{getInitials(label)}
|
|
193
|
+
</AvatarFallback>
|
|
194
|
+
</Avatar>
|
|
195
|
+
) : (
|
|
196
|
+
<Box className="h-3 w-3 shrink-0 opacity-70" />
|
|
197
|
+
)}
|
|
198
|
+
<span className="truncate">{label}</span>
|
|
199
|
+
</span>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
139
203
|
/**
|
|
140
204
|
* Visual cell for one declared item-field. A resolved `ref` renders as a
|
|
141
|
-
* relation chip (
|
|
142
|
-
*
|
|
143
|
-
* line items read like first-class relations instead of raw uuids. Non-ref (or
|
|
205
|
+
* relation chip (foto/ícono + nombre) — the "pro" FK-column look — so jsonb line
|
|
206
|
+
* items read like first-class relations instead of raw uuids. Non-ref (or
|
|
144
207
|
* unresolved) fields render the plain scalar.
|
|
145
208
|
*/
|
|
146
209
|
function ItemFieldCell({
|
|
@@ -152,34 +215,10 @@ function ItemFieldCell({
|
|
|
152
215
|
row: Record<string, unknown>
|
|
153
216
|
getImageUrl?: (path: string) => string
|
|
154
217
|
}): React.ReactElement {
|
|
155
|
-
const isDark = useIsDarkTheme()
|
|
156
218
|
const ref = resolvedRefFor(field, row)
|
|
157
219
|
if (ref?.label) {
|
|
158
220
|
return (
|
|
159
|
-
<
|
|
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>
|
|
221
|
+
<RefChip label={ref.label} image={ref.image} getImageUrl={getImageUrl} />
|
|
183
222
|
)
|
|
184
223
|
}
|
|
185
224
|
return <>{formatScalar(row[field.key])}</>
|
|
@@ -412,7 +451,20 @@ function MiniTable({
|
|
|
412
451
|
)
|
|
413
452
|
}
|
|
414
453
|
|
|
415
|
-
|
|
454
|
+
// Generic (no-schema) path — still relation-AWARE. The backend injects a
|
|
455
|
+
// resolved-ref sibling object (`{value,label,image}`) next to each FK id
|
|
456
|
+
// inside the items (e.g. `product` next to `product_id`). Without a declared
|
|
457
|
+
// schema we'd otherwise dump that object as "{…}" AND the raw uuid in a
|
|
458
|
+
// duplicate column. Instead: detect those sibling objects, render them as
|
|
459
|
+
// relation chips, and HIDE their raw `<key>_id` twin — so even an
|
|
460
|
+
// unconfigured jsonb blob reads "pro" (foto/nombre, no uuid soup).
|
|
461
|
+
const allKeys = unionKeys(rows)
|
|
462
|
+
const refKeys = new Set(
|
|
463
|
+
allKeys.filter((k) => rows.some((r) => resolvedRefObject(r[k]) !== null))
|
|
464
|
+
)
|
|
465
|
+
const hiddenTwins = new Set<string>()
|
|
466
|
+
for (const rk of refKeys) hiddenTwins.add(`${rk}_id`)
|
|
467
|
+
const keys = allKeys.filter((k) => !hiddenTwins.has(k))
|
|
416
468
|
if (keys.length === 0) {
|
|
417
469
|
return <div className="p-3 text-xs text-muted-foreground">-</div>
|
|
418
470
|
}
|
|
@@ -430,14 +482,27 @@ function MiniTable({
|
|
|
430
482
|
<TableBody>
|
|
431
483
|
{rows.map((row, i) => (
|
|
432
484
|
<TableRow key={i}>
|
|
433
|
-
{keys.map((key) =>
|
|
434
|
-
|
|
435
|
-
key
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
485
|
+
{keys.map((key) => {
|
|
486
|
+
const ref = refKeys.has(key)
|
|
487
|
+
? resolvedRefObject(row[key])
|
|
488
|
+
: null
|
|
489
|
+
return (
|
|
490
|
+
<TableCell
|
|
491
|
+
key={key}
|
|
492
|
+
className="text-xs whitespace-nowrap"
|
|
493
|
+
>
|
|
494
|
+
{ref ? (
|
|
495
|
+
<RefChip
|
|
496
|
+
label={ref.label}
|
|
497
|
+
image={ref.image}
|
|
498
|
+
getImageUrl={getImageUrl}
|
|
499
|
+
/>
|
|
500
|
+
) : (
|
|
501
|
+
formatScalar(row[key])
|
|
502
|
+
)}
|
|
503
|
+
</TableCell>
|
|
504
|
+
)
|
|
505
|
+
})}
|
|
441
506
|
</TableRow>
|
|
442
507
|
))}
|
|
443
508
|
</TableBody>
|