@asteby/metacore-runtime-react 18.25.0 → 18.27.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 +24 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +48 -2
- 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/package.json +3 -3
- package/src/action-modal-dispatcher.tsx +71 -2
- package/src/activity-diff.tsx +7 -0
- package/src/activity-value-renderer.tsx +47 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.27.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b71dcc0: feat(action-modal): line-items fields can prefill their rows from the acted-on record. A field whose `default` is a `PrefillSpec` (`{$prefillFromRecord, map, remaining}`) seeds one row per record array entry, copying mapped keys and computing a remaining quantity (`of - minus`), dropping fully-satisfied rows. Enables receive-goods/partial-reception modals (e.g. inventory transfers) to open pre-loaded with the pending lines instead of empty. Decoupled: the SDK only projects a record array into the field's item_fields.
|
|
8
|
+
|
|
9
|
+
## 18.26.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- d0056f1: Activity log (record history) renders jsonb line items as a table and localizes
|
|
14
|
+
relation field labels.
|
|
15
|
+
- **Line-items render as the shared `CollectionCell` mini-table** instead of raw
|
|
16
|
+
`JSON.stringify`. A jsonb array-of-objects value (e.g. a transfer's `items`,
|
|
17
|
+
directly or JSON-string-encoded) now shows a localized mini-table with
|
|
18
|
+
resolved relation chips (when the backend injects the `{value,label,image}`
|
|
19
|
+
siblings into the snapshot) — matching the detail view. Uses the column's
|
|
20
|
+
declared `item_fields` when present.
|
|
21
|
+
- **Relation field labels localize.** `resolveColumn` now matches the `*_id`
|
|
22
|
+
twin of a resolved relation key (`destination_warehouse` →
|
|
23
|
+
`destination_warehouse_id`), so the diff "Campo" uses the localized column
|
|
24
|
+
label ("Almacén destino") instead of humanizing the key in English
|
|
25
|
+
("Destination Warehouse").
|
|
26
|
+
|
|
3
27
|
## 18.25.0
|
|
4
28
|
|
|
5
29
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA+CA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA+CA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAkEhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCAiDlB"}
|
|
@@ -21,6 +21,47 @@ import { UploadField } from './upload-field';
|
|
|
21
21
|
import { isLineItemsField, resolveWidget, resolveDependsValue, getDependsOn } from './dynamic-form-schema';
|
|
22
22
|
// Canonical registry lives in @asteby/metacore-sdk
|
|
23
23
|
import { getActionComponent, } from '@asteby/metacore-sdk';
|
|
24
|
+
function isPrefillSpec(v) {
|
|
25
|
+
return (typeof v === 'object' &&
|
|
26
|
+
v !== null &&
|
|
27
|
+
typeof v.$prefillFromRecord === 'string');
|
|
28
|
+
}
|
|
29
|
+
// lineItemsDefault reads the field's declared default tolerating BOTH the
|
|
30
|
+
// camelCase `defaultValue` the host serves and the snake/legacy `default` the
|
|
31
|
+
// raw manifest carries (the kernel maps action-field `default` through without
|
|
32
|
+
// renaming it to `defaultValue`).
|
|
33
|
+
function lineItemsDefault(field) {
|
|
34
|
+
const f = field;
|
|
35
|
+
return f.defaultValue ?? f.default;
|
|
36
|
+
}
|
|
37
|
+
function toNum(v) {
|
|
38
|
+
const n = typeof v === 'number' ? v : parseFloat(String(v ?? ''));
|
|
39
|
+
return Number.isFinite(n) ? n : 0;
|
|
40
|
+
}
|
|
41
|
+
// buildPrefillRows projects record[spec.$prefillFromRecord] into modal rows.
|
|
42
|
+
function buildPrefillRows(spec, record) {
|
|
43
|
+
const src = record?.[spec.$prefillFromRecord];
|
|
44
|
+
if (!Array.isArray(src))
|
|
45
|
+
return [];
|
|
46
|
+
const rows = [];
|
|
47
|
+
for (const item of src) {
|
|
48
|
+
if (!item || typeof item !== 'object')
|
|
49
|
+
continue;
|
|
50
|
+
const row = {};
|
|
51
|
+
if (spec.map) {
|
|
52
|
+
for (const [target, from] of Object.entries(spec.map))
|
|
53
|
+
row[target] = item[from];
|
|
54
|
+
}
|
|
55
|
+
if (spec.remaining) {
|
|
56
|
+
const remaining = toNum(item[spec.remaining.of]) - (spec.remaining.minus ? toNum(item[spec.remaining.minus]) : 0);
|
|
57
|
+
if (remaining <= 0)
|
|
58
|
+
continue; // line already fully satisfied → omit
|
|
59
|
+
row[spec.remaining.target] = remaining;
|
|
60
|
+
}
|
|
61
|
+
rows.push(row);
|
|
62
|
+
}
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
24
65
|
export function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }) {
|
|
25
66
|
const CustomComponent = useMemo(() => getActionComponent(model, action.key), [model, action.key]);
|
|
26
67
|
if (CustomComponent) {
|
|
@@ -83,14 +124,19 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
83
124
|
const defaults = {};
|
|
84
125
|
for (const field of action.fields) {
|
|
85
126
|
if (isLineItemsField(field)) {
|
|
86
|
-
|
|
127
|
+
const dv = lineItemsDefault(field);
|
|
128
|
+
defaults[field.key] = isPrefillSpec(dv)
|
|
129
|
+
? buildPrefillRows(dv, record)
|
|
130
|
+
: Array.isArray(dv)
|
|
131
|
+
? dv
|
|
132
|
+
: [];
|
|
87
133
|
continue;
|
|
88
134
|
}
|
|
89
135
|
defaults[field.key] = field.defaultValue ?? (field.type === 'boolean' ? false : '');
|
|
90
136
|
}
|
|
91
137
|
setFormData(defaults);
|
|
92
138
|
}
|
|
93
|
-
}, [open, action.fields]);
|
|
139
|
+
}, [open, action.fields, record]);
|
|
94
140
|
const updateField = (key, value) => setFormData((prev) => ({ ...prev, [key]: value }));
|
|
95
141
|
const execute = async () => {
|
|
96
142
|
if (action.fields) {
|
|
@@ -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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.27.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"react-i18next": ">=13",
|
|
35
35
|
"sonner": ">=1.7",
|
|
36
36
|
"zustand": ">=5",
|
|
37
|
-
"@asteby/metacore-
|
|
38
|
-
"@asteby/metacore-
|
|
37
|
+
"@asteby/metacore-sdk": "^3.2.0",
|
|
38
|
+
"@asteby/metacore-ui": "^2.5.2"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
41
41
|
"@tanstack/react-router": {
|
|
@@ -53,6 +53,70 @@ import {
|
|
|
53
53
|
|
|
54
54
|
export type { ActionMetadata, ActionModalProps }
|
|
55
55
|
|
|
56
|
+
// ---- line-items prefill from the acted-on record ----------------------------
|
|
57
|
+
//
|
|
58
|
+
// A line-items action field can seed its rows from the record being acted on,
|
|
59
|
+
// instead of opening empty. The manifest declares this by setting the field's
|
|
60
|
+
// `default`/`defaultValue` to a PrefillSpec object: the modal reads the record's
|
|
61
|
+
// `$prefillFromRecord` array, copies the mapped keys, and (for a receive-style
|
|
62
|
+
// flow) computes a `remaining` quantity = of - minus, dropping fully-satisfied
|
|
63
|
+
// rows. Decoupled + generic: the SDK knows nothing about transfers, only how to
|
|
64
|
+
// project a record array into the field's item_fields. Example (receive goods):
|
|
65
|
+
//
|
|
66
|
+
// "default": {
|
|
67
|
+
// "$prefillFromRecord": "items",
|
|
68
|
+
// "map": { "product_id": "product_id" },
|
|
69
|
+
// "remaining": { "target": "qty_received", "of": "quantity", "minus": "received" }
|
|
70
|
+
// }
|
|
71
|
+
interface PrefillSpec {
|
|
72
|
+
$prefillFromRecord: string
|
|
73
|
+
map?: Record<string, string>
|
|
74
|
+
remaining?: { target: string; of: string; minus?: string }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isPrefillSpec(v: unknown): v is PrefillSpec {
|
|
78
|
+
return (
|
|
79
|
+
typeof v === 'object' &&
|
|
80
|
+
v !== null &&
|
|
81
|
+
typeof (v as { $prefillFromRecord?: unknown }).$prefillFromRecord === 'string'
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// lineItemsDefault reads the field's declared default tolerating BOTH the
|
|
86
|
+
// camelCase `defaultValue` the host serves and the snake/legacy `default` the
|
|
87
|
+
// raw manifest carries (the kernel maps action-field `default` through without
|
|
88
|
+
// renaming it to `defaultValue`).
|
|
89
|
+
function lineItemsDefault(field: ActionFieldDef): unknown {
|
|
90
|
+
const f = field as { defaultValue?: unknown; default?: unknown }
|
|
91
|
+
return f.defaultValue ?? f.default
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toNum(v: unknown): number {
|
|
95
|
+
const n = typeof v === 'number' ? v : parseFloat(String(v ?? ''))
|
|
96
|
+
return Number.isFinite(n) ? n : 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// buildPrefillRows projects record[spec.$prefillFromRecord] into modal rows.
|
|
100
|
+
function buildPrefillRows(spec: PrefillSpec, record: any): Array<Record<string, any>> {
|
|
101
|
+
const src = record?.[spec.$prefillFromRecord]
|
|
102
|
+
if (!Array.isArray(src)) return []
|
|
103
|
+
const rows: Array<Record<string, any>> = []
|
|
104
|
+
for (const item of src) {
|
|
105
|
+
if (!item || typeof item !== 'object') continue
|
|
106
|
+
const row: Record<string, any> = {}
|
|
107
|
+
if (spec.map) {
|
|
108
|
+
for (const [target, from] of Object.entries(spec.map)) row[target] = item[from]
|
|
109
|
+
}
|
|
110
|
+
if (spec.remaining) {
|
|
111
|
+
const remaining = toNum(item[spec.remaining.of]) - (spec.remaining.minus ? toNum(item[spec.remaining.minus]) : 0)
|
|
112
|
+
if (remaining <= 0) continue // line already fully satisfied → omit
|
|
113
|
+
row[spec.remaining.target] = remaining
|
|
114
|
+
}
|
|
115
|
+
rows.push(row)
|
|
116
|
+
}
|
|
117
|
+
return rows
|
|
118
|
+
}
|
|
119
|
+
|
|
56
120
|
export function ActionModalDispatcher({
|
|
57
121
|
open,
|
|
58
122
|
onOpenChange,
|
|
@@ -188,14 +252,19 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
188
252
|
const defaults: Record<string, any> = {}
|
|
189
253
|
for (const field of action.fields) {
|
|
190
254
|
if (isLineItemsField(field)) {
|
|
191
|
-
|
|
255
|
+
const dv = lineItemsDefault(field)
|
|
256
|
+
defaults[field.key] = isPrefillSpec(dv)
|
|
257
|
+
? buildPrefillRows(dv, record)
|
|
258
|
+
: Array.isArray(dv)
|
|
259
|
+
? dv
|
|
260
|
+
: []
|
|
192
261
|
continue
|
|
193
262
|
}
|
|
194
263
|
defaults[field.key] = field.defaultValue ?? (field.type === 'boolean' ? false : '')
|
|
195
264
|
}
|
|
196
265
|
setFormData(defaults)
|
|
197
266
|
}
|
|
198
|
-
}, [open, action.fields])
|
|
267
|
+
}, [open, action.fields, record])
|
|
199
268
|
|
|
200
269
|
const updateField = (key: string, value: any) => setFormData((prev: Record<string, any>) => ({ ...prev, [key]: value }))
|
|
201
270
|
|
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) {
|