@asteby/metacore-runtime-react 39.2.3 → 39.2.4
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 +6 -0
- package/dist/action-modal-dispatcher.d.ts +13 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +57 -0
- package/package.json +1 -1
- package/src/__tests__/prefill-from-relation.test.ts +42 -0
- package/src/action-modal-dispatcher.tsx +72 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 39.2.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c759e48: Los modales de acción que precargan renglones desde el registro (`$prefillFromRecord`, p. ej. "Recibir" de una orden de compra) ahora cargan las líneas desde la relación declarada del modelo cuando la fila de la lista no las trae. Antes el modal abría con "Sin renglones".
|
|
8
|
+
|
|
3
9
|
## 39.2.3
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -20,6 +20,19 @@ export interface PrefillSpec {
|
|
|
20
20
|
export declare function isPrefillSpec(v: unknown): v is PrefillSpec;
|
|
21
21
|
export declare function applyPrefillLock(field: ActionFieldDef): ActionFieldDef;
|
|
22
22
|
export declare function buildPrefillRows(spec: PrefillSpec, record: any): Array<Record<string, any>>;
|
|
23
|
+
export interface PrefillRelationRequest {
|
|
24
|
+
fieldKey: string;
|
|
25
|
+
spec: PrefillSpec;
|
|
26
|
+
endpoint: string;
|
|
27
|
+
params: Record<string, string | number>;
|
|
28
|
+
}
|
|
29
|
+
export declare function prefillRelationRequests(fields: ActionFieldDef[] | undefined, record: any, relations: Array<{
|
|
30
|
+
name?: string;
|
|
31
|
+
kind?: string;
|
|
32
|
+
through?: string;
|
|
33
|
+
foreign_key?: string;
|
|
34
|
+
scope?: Record<string, unknown>;
|
|
35
|
+
}>): PrefillRelationRequest[];
|
|
23
36
|
export declare function unwrapRecordScalar(value: unknown): unknown;
|
|
24
37
|
export declare function readRecordPath(record: any, path: string): unknown;
|
|
25
38
|
/** Scalar seed for one action field from the row being acted on. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AAwEA,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAiBhD,MAAM,WAAW,WAAW;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAM1D;AAsBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAStE;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAkB3F;AAYD,MAAM,WAAW,sBAAsB;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;CAC1C;AAED,wBAAgB,uBAAuB,CACnC,MAAM,EAAE,cAAc,EAAE,GAAG,SAAS,EACpC,MAAM,EAAE,GAAG,EACX,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,GAC5H,sBAAsB,EAAE,CAyB1B;AASD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAY1D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CASjE;AAUD,oEAAoE;AACpE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAgBnF;AAED,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCAkFlB"}
|
|
@@ -23,6 +23,7 @@ import { validateValues, bagHasErrors } from './validator';
|
|
|
23
23
|
import { clearFieldErrorTree, formatFieldErrorsDescription, labelsForValidationFields, labelForValidationPath, lineItemErrorsFor, } from './field-validation-ui';
|
|
24
24
|
import { validationCatalog } from './validation-catalog';
|
|
25
25
|
import { useApi } from './api-context';
|
|
26
|
+
import { buildRelationFilterParams } from './dynamic-relation-helpers';
|
|
26
27
|
import { DynamicIcon } from './dynamic-icon';
|
|
27
28
|
import { DynamicLineItems } from './dynamic-line-items';
|
|
28
29
|
import { DynamicRelations } from './dynamic-relations';
|
|
@@ -94,6 +95,36 @@ export function buildPrefillRows(spec, record) {
|
|
|
94
95
|
}
|
|
95
96
|
return rows;
|
|
96
97
|
}
|
|
98
|
+
export function prefillRelationRequests(fields, record, relations) {
|
|
99
|
+
const out = [];
|
|
100
|
+
const recordId = record?.id;
|
|
101
|
+
if (!fields || recordId === undefined || recordId === null || recordId === '')
|
|
102
|
+
return out;
|
|
103
|
+
for (const field of fields) {
|
|
104
|
+
if (!isLineItemsField(field))
|
|
105
|
+
continue;
|
|
106
|
+
const spec = lineItemsDefault(field);
|
|
107
|
+
if (!isPrefillSpec(spec))
|
|
108
|
+
continue;
|
|
109
|
+
if (Array.isArray(record?.[spec.$prefillFromRecord]))
|
|
110
|
+
continue; // already embedded
|
|
111
|
+
const rel = (relations || []).find((r) => r?.name === spec.$prefillFromRecord && (r.kind ?? 'one_to_many') === 'one_to_many');
|
|
112
|
+
if (!rel?.through || !rel.foreign_key)
|
|
113
|
+
continue;
|
|
114
|
+
const scope = {};
|
|
115
|
+
for (const [k, v] of Object.entries(rel.scope ?? {})) {
|
|
116
|
+
if (v !== undefined && v !== null)
|
|
117
|
+
scope[k] = String(v);
|
|
118
|
+
}
|
|
119
|
+
out.push({
|
|
120
|
+
fieldKey: field.key,
|
|
121
|
+
spec,
|
|
122
|
+
endpoint: `/data/${rel.through}`,
|
|
123
|
+
params: { ...buildRelationFilterParams(rel.foreign_key, recordId, scope), per_page: 200 },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
97
128
|
// ---- scalar prefill from the acted-on record --------------------------------
|
|
98
129
|
//
|
|
99
130
|
// Row actions (stamp / refactura / cancel-with-reason) should open with the
|
|
@@ -457,6 +488,32 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
457
488
|
setFieldErrors({});
|
|
458
489
|
}
|
|
459
490
|
}, [open, action.fields, record]);
|
|
491
|
+
// Lines the list row does not carry: load them from the declared relation
|
|
492
|
+
// (see prefillRelationRequests) and seed the line-items grid.
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
if (!open || relations.length === 0)
|
|
495
|
+
return;
|
|
496
|
+
const reqs = prefillRelationRequests(action.fields, record, relations);
|
|
497
|
+
if (reqs.length === 0)
|
|
498
|
+
return;
|
|
499
|
+
let cancelled = false;
|
|
500
|
+
for (const req of reqs) {
|
|
501
|
+
api.get(req.endpoint, { params: req.params })
|
|
502
|
+
.then((res) => {
|
|
503
|
+
if (cancelled)
|
|
504
|
+
return;
|
|
505
|
+
const rows = res?.data?.data ?? res?.data ?? [];
|
|
506
|
+
if (!Array.isArray(rows))
|
|
507
|
+
return;
|
|
508
|
+
const built = buildPrefillRows(req.spec, { [req.spec.$prefillFromRecord]: rows });
|
|
509
|
+
setFormData((prev) => ({ ...prev, [req.fieldKey]: built }));
|
|
510
|
+
})
|
|
511
|
+
.catch(() => { });
|
|
512
|
+
}
|
|
513
|
+
return () => {
|
|
514
|
+
cancelled = true;
|
|
515
|
+
};
|
|
516
|
+
}, [open, action.fields, record, relations, api]);
|
|
460
517
|
const updateField = (key, value) => {
|
|
461
518
|
setFormData((prev) => ({ ...prev, [key]: value }));
|
|
462
519
|
setFieldErrors((prev) => clearFieldErrorTree(prev, key));
|
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { prefillRelationRequests } from '../action-modal-dispatcher'
|
|
3
|
+
import type { ActionFieldDef } from '../types'
|
|
4
|
+
|
|
5
|
+
// QA 7Leguas: the purchase-order receive modal opened from the LIST showed
|
|
6
|
+
// "Sin renglones" — the list row carries no `items`, so the
|
|
7
|
+
// $prefillFromRecord grid had nothing to project. The modal must load the
|
|
8
|
+
// lines from the model's declared one_to_many relation.
|
|
9
|
+
const itemsField: ActionFieldDef = {
|
|
10
|
+
key: 'items',
|
|
11
|
+
label: 'Renglones',
|
|
12
|
+
type: 'array',
|
|
13
|
+
itemFields: [
|
|
14
|
+
{ key: 'product_id', label: 'Producto', type: 'dynamic_select', ref: 'Product' },
|
|
15
|
+
{ key: 'qty_received', label: 'Recibido', type: 'number' },
|
|
16
|
+
],
|
|
17
|
+
default: { $prefillFromRecord: 'items', map: { product_id: 'product_variant_id' } },
|
|
18
|
+
} as unknown as ActionFieldDef
|
|
19
|
+
|
|
20
|
+
const relations = [
|
|
21
|
+
{ name: 'items', kind: 'one_to_many', through: 'PurchaseOrderItem', foreign_key: 'purchase_order_id' },
|
|
22
|
+
{ name: 'goods_receipts', kind: 'one_to_many', through: 'GoodsReceipt', foreign_key: 'purchase_order_id' },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
describe('prefillRelationRequests', () => {
|
|
26
|
+
it('pide las líneas por la relación declarada cuando el registro no las trae', () => {
|
|
27
|
+
const reqs = prefillRelationRequests([itemsField], { id: 'po-1' }, relations)
|
|
28
|
+
expect(reqs).toHaveLength(1)
|
|
29
|
+
expect(reqs[0].fieldKey).toBe('items')
|
|
30
|
+
expect(reqs[0].endpoint).toBe('/data/PurchaseOrderItem')
|
|
31
|
+
expect(reqs[0].params).toMatchObject({ f_purchase_order_id: 'eq:po-1' })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('no pide nada si el registro ya trae las líneas embebidas', () => {
|
|
35
|
+
expect(prefillRelationRequests([itemsField], { id: 'po-1', items: [] }, relations)).toHaveLength(0)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('no pide nada sin id (acción de creación) o sin relación con ese nombre', () => {
|
|
39
|
+
expect(prefillRelationRequests([itemsField], {}, relations)).toHaveLength(0)
|
|
40
|
+
expect(prefillRelationRequests([itemsField], { id: 'po-1' }, [relations[1]])).toHaveLength(0)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
} from './field-validation-ui'
|
|
54
54
|
import { validationCatalog } from './validation-catalog'
|
|
55
55
|
import { useApi } from './api-context'
|
|
56
|
+
import { buildRelationFilterParams } from './dynamic-relation-helpers'
|
|
56
57
|
import { DynamicIcon } from './dynamic-icon'
|
|
57
58
|
import { DynamicLineItems } from './dynamic-line-items'
|
|
58
59
|
import { DynamicRelations } from './dynamic-relations'
|
|
@@ -167,6 +168,54 @@ export function buildPrefillRows(spec: PrefillSpec, record: any): Array<Record<s
|
|
|
167
168
|
return rows
|
|
168
169
|
}
|
|
169
170
|
|
|
171
|
+
// ---- prefill lines from the record's declared relation ----------------------
|
|
172
|
+
//
|
|
173
|
+
// A row action opened from a LIST gets the list row, which does not carry the
|
|
174
|
+
// document's lines: `record.items` is undefined and a receive modal opened
|
|
175
|
+
// with "Sin renglones" (QA 7Leguas: OC receive modal empty, the user could not
|
|
176
|
+
// see what was being received). When the prefill source key is missing from
|
|
177
|
+
// the record but the model declares a one_to_many relation with that name, the
|
|
178
|
+
// modal fetches the child rows the same way the relation panel does
|
|
179
|
+
// (`/data/<through>?f_<fk>=eq:<id>`) and builds the rows from them.
|
|
180
|
+
|
|
181
|
+
export interface PrefillRelationRequest {
|
|
182
|
+
fieldKey: string
|
|
183
|
+
spec: PrefillSpec
|
|
184
|
+
endpoint: string
|
|
185
|
+
params: Record<string, string | number>
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function prefillRelationRequests(
|
|
189
|
+
fields: ActionFieldDef[] | undefined,
|
|
190
|
+
record: any,
|
|
191
|
+
relations: Array<{ name?: string; kind?: string; through?: string; foreign_key?: string; scope?: Record<string, unknown> }>,
|
|
192
|
+
): PrefillRelationRequest[] {
|
|
193
|
+
const out: PrefillRelationRequest[] = []
|
|
194
|
+
const recordId = record?.id
|
|
195
|
+
if (!fields || recordId === undefined || recordId === null || recordId === '') return out
|
|
196
|
+
for (const field of fields) {
|
|
197
|
+
if (!isLineItemsField(field)) continue
|
|
198
|
+
const spec = lineItemsDefault(field)
|
|
199
|
+
if (!isPrefillSpec(spec)) continue
|
|
200
|
+
if (Array.isArray(record?.[spec.$prefillFromRecord])) continue // already embedded
|
|
201
|
+
const rel = (relations || []).find(
|
|
202
|
+
(r) => r?.name === spec.$prefillFromRecord && (r.kind ?? 'one_to_many') === 'one_to_many',
|
|
203
|
+
)
|
|
204
|
+
if (!rel?.through || !rel.foreign_key) continue
|
|
205
|
+
const scope: Record<string, string> = {}
|
|
206
|
+
for (const [k, v] of Object.entries(rel.scope ?? {})) {
|
|
207
|
+
if (v !== undefined && v !== null) scope[k] = String(v)
|
|
208
|
+
}
|
|
209
|
+
out.push({
|
|
210
|
+
fieldKey: field.key,
|
|
211
|
+
spec,
|
|
212
|
+
endpoint: `/data/${rel.through}`,
|
|
213
|
+
params: { ...buildRelationFilterParams(rel.foreign_key, recordId, scope), per_page: 200 },
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
return out
|
|
217
|
+
}
|
|
218
|
+
|
|
170
219
|
// ---- scalar prefill from the acted-on record --------------------------------
|
|
171
220
|
//
|
|
172
221
|
// Row actions (stamp / refactura / cancel-with-reason) should open with the
|
|
@@ -686,6 +735,29 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
686
735
|
}
|
|
687
736
|
}, [open, action.fields, record])
|
|
688
737
|
|
|
738
|
+
// Lines the list row does not carry: load them from the declared relation
|
|
739
|
+
// (see prefillRelationRequests) and seed the line-items grid.
|
|
740
|
+
useEffect(() => {
|
|
741
|
+
if (!open || relations.length === 0) return
|
|
742
|
+
const reqs = prefillRelationRequests(action.fields, record, relations)
|
|
743
|
+
if (reqs.length === 0) return
|
|
744
|
+
let cancelled = false
|
|
745
|
+
for (const req of reqs) {
|
|
746
|
+
api.get(req.endpoint, { params: req.params })
|
|
747
|
+
.then((res: any) => {
|
|
748
|
+
if (cancelled) return
|
|
749
|
+
const rows = res?.data?.data ?? res?.data ?? []
|
|
750
|
+
if (!Array.isArray(rows)) return
|
|
751
|
+
const built = buildPrefillRows(req.spec, { [req.spec.$prefillFromRecord]: rows })
|
|
752
|
+
setFormData((prev: Record<string, any>) => ({ ...prev, [req.fieldKey]: built }))
|
|
753
|
+
})
|
|
754
|
+
.catch(() => {})
|
|
755
|
+
}
|
|
756
|
+
return () => {
|
|
757
|
+
cancelled = true
|
|
758
|
+
}
|
|
759
|
+
}, [open, action.fields, record, relations, api])
|
|
760
|
+
|
|
689
761
|
const updateField = (key: string, value: any) => {
|
|
690
762
|
setFormData((prev: Record<string, any>) => ({ ...prev, [key]: value }))
|
|
691
763
|
setFieldErrors((prev) => clearFieldErrorTree(prev, key))
|