@asteby/metacore-runtime-react 11.0.0 → 12.0.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 +54 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +21 -1
- package/dist/dialogs/create-record-dialog.d.ts +3 -0
- package/dist/dialogs/create-record-dialog.d.ts.map +1 -0
- package/dist/dialogs/create-record-dialog.js +20 -0
- package/dist/dialogs/dynamic-record.d.ts +38 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +50 -12
- package/dist/dialogs/types.d.ts +115 -0
- package/dist/dialogs/types.d.ts.map +1 -0
- package/dist/dialogs/types.js +15 -0
- package/dist/dialogs/view-record-dialog.d.ts +3 -0
- package/dist/dialogs/view-record-dialog.d.ts.map +1 -0
- package/dist/dialogs/view-record-dialog.js +15 -0
- package/dist/dynamic-form-schema.d.ts +9 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +22 -0
- package/dist/dynamic-form.d.ts +1 -0
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +12 -1
- package/dist/dynamic-line-items.d.ts +9 -0
- package/dist/dynamic-line-items.d.ts.map +1 -0
- package/dist/dynamic-line-items.js +64 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/dynamic-form.test.ts +56 -1
- package/src/action-modal-dispatcher.tsx +22 -2
- package/src/dialogs/create-record-dialog.tsx +46 -0
- package/src/dialogs/dynamic-record.tsx +111 -15
- package/src/dialogs/types.ts +119 -0
- package/src/dialogs/view-record-dialog.tsx +37 -0
- package/src/dynamic-form-schema.ts +25 -0
- package/src/dynamic-form.tsx +12 -1
- package/src/dynamic-line-items.tsx +221 -0
- package/src/index.ts +10 -0
- package/src/types.ts +10 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// DynamicLineItems — renders a repeatable line-items group: a table/grid of
|
|
3
|
+
// rows where each column is one of the field's `itemFields` (the v3
|
|
4
|
+
// `item_fields`). Powers declarative multi-line action modals (e.g. the item
|
|
5
|
+
// rows of a "Recibir mercancía" modal, or the debit/credit lines of a journal
|
|
6
|
+
// entry) without needing a custom federated modal.
|
|
7
|
+
//
|
|
8
|
+
// The value is an array of row objects keyed by the item field keys. Add/remove
|
|
9
|
+
// row controls mutate the array; each cell is a widget resolved via
|
|
10
|
+
// `resolveWidget`, matching the flat-field renderer in dynamic-form.tsx.
|
|
11
|
+
import { Input, Textarea, Switch, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@asteby/metacore-ui/primitives';
|
|
12
|
+
import { Plus, Trash2 } from 'lucide-react';
|
|
13
|
+
import { resolveWidget, getItemFields } from './dynamic-form-schema';
|
|
14
|
+
import { useOptionsResolver } from './use-options-resolver';
|
|
15
|
+
function emptyRow(itemFields) {
|
|
16
|
+
const row = {};
|
|
17
|
+
for (const f of itemFields) {
|
|
18
|
+
row[f.key] = f.defaultValue ?? (f.type === 'boolean' ? false : '');
|
|
19
|
+
}
|
|
20
|
+
return row;
|
|
21
|
+
}
|
|
22
|
+
export function DynamicLineItems({ field, value, onChange, disabled = false }) {
|
|
23
|
+
const itemFields = getItemFields(field);
|
|
24
|
+
const rows = Array.isArray(value) ? value : [];
|
|
25
|
+
const addRow = () => onChange([...rows, emptyRow(itemFields)]);
|
|
26
|
+
const removeRow = (idx) => onChange(rows.filter((_, i) => i !== idx));
|
|
27
|
+
const updateCell = (idx, key, cellValue) => onChange(rows.map((r, i) => (i === idx ? { ...r, [key]: cellValue } : r)));
|
|
28
|
+
return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsx("div", { className: "overflow-x-auto rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: "px-3 py-2 text-left font-medium", children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + 1, className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => (_jsx("td", { className: "px-2 py-1.5", children: _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => updateCell(idx, col.key, v), disabled: disabled }) }, col.key))), _jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) })] }, idx)))] })] }) }), _jsx("div", { children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addRow, disabled: disabled, children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Agregar rengl\u00F3n"] }) })] }));
|
|
29
|
+
}
|
|
30
|
+
// Per-cell widget. Mirrors the flat FieldRenderer in dynamic-form.tsx but
|
|
31
|
+
// without the per-field Label (the column header is the label) and sized for a
|
|
32
|
+
// table cell. Nested line-items inside a row are not supported (a row column is
|
|
33
|
+
// a scalar widget).
|
|
34
|
+
function CellRenderer({ field, value, onChange, disabled }) {
|
|
35
|
+
const widget = resolveWidget(field);
|
|
36
|
+
if (widget === 'select' && field.ref) {
|
|
37
|
+
return _jsx(RefCell, { field: field, value: value, onChange: onChange, disabled: disabled });
|
|
38
|
+
}
|
|
39
|
+
switch (widget) {
|
|
40
|
+
case 'textarea':
|
|
41
|
+
case 'richtext':
|
|
42
|
+
return (_jsx(Textarea, { value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: disabled, rows: 2 }));
|
|
43
|
+
case 'color':
|
|
44
|
+
return (_jsx(Input, { type: "color", value: value || '#000000', onChange: (e) => onChange(e.target.value), disabled: disabled }));
|
|
45
|
+
case 'select':
|
|
46
|
+
return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: disabled, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
|
|
47
|
+
case 'switch':
|
|
48
|
+
return _jsx(Switch, { checked: !!value, onCheckedChange: onChange, disabled: disabled });
|
|
49
|
+
case 'number':
|
|
50
|
+
return (_jsx(Input, { type: "number", value: value ?? '', onChange: (e) => onChange(e.target.valueAsNumber || ''), placeholder: field.placeholder, disabled: disabled }));
|
|
51
|
+
case 'date':
|
|
52
|
+
return (_jsx(Input, { type: "date", value: value || '', onChange: (e) => onChange(e.target.value), disabled: disabled }));
|
|
53
|
+
default:
|
|
54
|
+
return (_jsx(Input, { type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: disabled }));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function RefCell({ field, value, onChange, disabled }) {
|
|
58
|
+
const { options, loading } = useOptionsResolver({
|
|
59
|
+
modelKey: '',
|
|
60
|
+
fieldKey: 'id',
|
|
61
|
+
ref: field.ref,
|
|
62
|
+
});
|
|
63
|
+
return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: disabled || loading, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: loading ? 'Cargando…' : field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: options.map((opt) => (_jsx(SelectItem, { value: String(opt.id), children: opt.label }, String(opt.id)))) })] }));
|
|
64
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ export * from './dynamic-icon';
|
|
|
17
17
|
export type { ColumnFilterConfig, FilterOption as DynamicColumnFilterOption, GetDynamicColumns, DynamicIconComponent, } from './dynamic-columns-shim';
|
|
18
18
|
export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, type DynamicColumnsHelpers, } from './dynamic-columns';
|
|
19
19
|
export { DynamicRecordDialog } from './dialogs/dynamic-record';
|
|
20
|
+
export { CreateRecordDialog } from './dialogs/create-record-dialog';
|
|
21
|
+
export { ViewRecordDialog } from './dialogs/view-record-dialog';
|
|
22
|
+
export type { ModelKey, ModelSchema, CreateResult, RecordDialogProps, CreateRecordDialogProps, ViewRecordDialogProps, } from './dialogs/types';
|
|
20
23
|
export { ExportDialog } from './dialogs/export';
|
|
21
24
|
export { ImportDialog } from './dialogs/import';
|
|
22
25
|
export { DynamicCRUDPage, type DynamicCRUDPageProps, type DynamicCRUDPageStrings, type DynamicCRUDPageClasses, } from './dynamic-crud-page';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,WAAW,EAChB,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,GACtC,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACH,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC9B,MAAM,yBAAyB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GAC3B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACH,sBAAsB,EACtB,uBAAuB,GAC1B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,kBAAkB,EAClB,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,qBAAqB,EACrB,KAAK,gBAAgB,GACxB,MAAM,2BAA2B,CAAA;AAClC,cAAc,gBAAgB,CAAA;AAC9B,OAAO,EACH,mBAAmB,EACnB,cAAc,EACd,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,WAAW,EAChB,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,cAAc,QAAQ,CAAA;AACtB,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,OAAO,EACH,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,KAAK,2BAA2B,EAChC,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,GACtC,MAAM,+BAA+B,CAAA;AACtC,OAAO,EACH,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC9B,MAAM,yBAAyB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,YAAY,EACR,kBAAkB,EAClB,YAAY,IAAI,yBAAyB,EACzC,iBAAiB,EACjB,oBAAoB,GACvB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,wBAAwB,EACxB,4BAA4B,EAC5B,KAAK,qBAAqB,GAC7B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAC/D,YAAY,EACR,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,GACxB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC9B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,GACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACH,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GAC3B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACH,sBAAsB,EACtB,uBAAuB,GAC1B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACH,kBAAkB,EAClB,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,GAChC,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACH,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,eAAe,GACvB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederation
|
|
|
21
21
|
export * from './dynamic-icon';
|
|
22
22
|
export { defaultGetDynamicColumns, makeDefaultGetDynamicColumns, } from './dynamic-columns';
|
|
23
23
|
export { DynamicRecordDialog } from './dialogs/dynamic-record';
|
|
24
|
+
export { CreateRecordDialog } from './dialogs/create-record-dialog';
|
|
25
|
+
export { ViewRecordDialog } from './dialogs/view-record-dialog';
|
|
24
26
|
export { ExportDialog } from './dialogs/export';
|
|
25
27
|
export { ImportDialog } from './dialogs/import';
|
|
26
28
|
export { DynamicCRUDPage, } from './dynamic-crud-page';
|
package/dist/types.d.ts
CHANGED
|
@@ -121,6 +121,16 @@ export interface ActionFieldDef {
|
|
|
121
121
|
* `useOptionsResolver` against `/api/options/<ref>?field=id`.
|
|
122
122
|
*/
|
|
123
123
|
ref?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Columns of a repeatable line-items group. Mirrors the kernel v3
|
|
126
|
+
* `ActionField.item_fields` (json `item_fields`). Present on a field
|
|
127
|
+
* with `type: "array"` — the multi-row container (e.g. the item rows
|
|
128
|
+
* of a "Recibir mercancía" modal, or the debit/credit lines of a
|
|
129
|
+
* journal entry). Each entry is itself an ActionFieldDef describing
|
|
130
|
+
* one column's cell widget. The field value is an array of objects
|
|
131
|
+
* keyed by these item field keys. Rendered by `DynamicLineItems`.
|
|
132
|
+
*/
|
|
133
|
+
itemFields?: ActionFieldDef[];
|
|
124
134
|
}
|
|
125
135
|
export interface ActionDefinition {
|
|
126
136
|
key: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"lucide-react": ">=0.460",
|
|
33
33
|
"date-fns": ">=3",
|
|
34
34
|
"react-day-picker": ">=8",
|
|
35
|
-
"@asteby/metacore-sdk": "^
|
|
36
|
-
"@asteby/metacore-ui": "^2.0.
|
|
35
|
+
"@asteby/metacore-sdk": "^3.0.0",
|
|
36
|
+
"@asteby/metacore-ui": "^2.0.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependenciesMeta": {
|
|
39
39
|
"@tanstack/react-router": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"i18next": "^26.0.0",
|
|
53
53
|
"lucide-react": "^1.0.0",
|
|
54
54
|
"react": "^19.2.4",
|
|
55
|
-
"react-day-picker": "^
|
|
55
|
+
"react-day-picker": "^10.0.0",
|
|
56
56
|
"react-dom": "^19.2.4",
|
|
57
57
|
"react-i18next": "^17.0.0",
|
|
58
58
|
"sonner": "^2.0.0",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"typescript": "^6.0.0",
|
|
61
61
|
"vitest": "^4.0.0",
|
|
62
62
|
"zustand": "^5.0.0",
|
|
63
|
-
"@asteby/metacore-sdk": "
|
|
64
|
-
"@asteby/metacore-ui": "2.0.
|
|
63
|
+
"@asteby/metacore-sdk": "3.0.0",
|
|
64
|
+
"@asteby/metacore-ui": "2.0.1"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"build": "tsc -p tsconfig.json",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { buildZodSchema, resolveWidget } from '../dynamic-form-schema'
|
|
2
|
+
import { buildZodSchema, resolveWidget, isLineItemsField, getItemFields } from '../dynamic-form-schema'
|
|
3
3
|
import type { ActionFieldDef } from '../types'
|
|
4
4
|
|
|
5
5
|
describe('buildZodSchema', () => {
|
|
@@ -102,3 +102,58 @@ describe('resolveWidget', () => {
|
|
|
102
102
|
expect(resolveWidget({ key: 'k', label: 'L', type: 'email' })).toBe('text')
|
|
103
103
|
})
|
|
104
104
|
})
|
|
105
|
+
|
|
106
|
+
describe('line-items (repeatable group)', () => {
|
|
107
|
+
const lineItemsField: ActionFieldDef = {
|
|
108
|
+
key: 'lines',
|
|
109
|
+
label: 'Renglones',
|
|
110
|
+
type: 'array',
|
|
111
|
+
itemFields: [
|
|
112
|
+
{ key: 'product_id', label: 'Producto', type: 'select', ref: 'product' },
|
|
113
|
+
{ key: 'quantity', label: 'Cantidad', type: 'number', required: true },
|
|
114
|
+
],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
it('detecta un campo line-items por sus itemFields', () => {
|
|
118
|
+
expect(isLineItemsField(lineItemsField)).toBe(true)
|
|
119
|
+
expect(isLineItemsField({ key: 'name', label: 'Nombre', type: 'string' })).toBe(false)
|
|
120
|
+
expect(getItemFields(lineItemsField)).toHaveLength(2)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('tolera item_fields snake_case crudo del kernel', () => {
|
|
124
|
+
const raw = {
|
|
125
|
+
key: 'lines',
|
|
126
|
+
label: 'Renglones',
|
|
127
|
+
type: 'array',
|
|
128
|
+
item_fields: [{ key: 'sku', label: 'SKU', type: 'string' }],
|
|
129
|
+
} as unknown as ActionFieldDef
|
|
130
|
+
expect(isLineItemsField(raw)).toBe(true)
|
|
131
|
+
expect(getItemFields(raw)).toHaveLength(1)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('valida como array de objetos por renglón', () => {
|
|
135
|
+
const schema = buildZodSchema([lineItemsField])
|
|
136
|
+
const ok = schema.safeParse({ lines: [{ product_id: 'p1', quantity: 3 }] })
|
|
137
|
+
expect(ok.success).toBe(true)
|
|
138
|
+
// No es un array → inválido (el valor del grupo debe ser una lista de renglones)
|
|
139
|
+
expect(schema.safeParse({ lines: 'nope' }).success).toBe(false)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('aplica reglas por columna dentro de cada renglón', () => {
|
|
143
|
+
const withBound: ActionFieldDef = {
|
|
144
|
+
key: 'lines',
|
|
145
|
+
label: 'Renglones',
|
|
146
|
+
type: 'array',
|
|
147
|
+
itemFields: [{ key: 'qty', label: 'Cantidad', type: 'number', required: true, validation: { min: 1 } }],
|
|
148
|
+
}
|
|
149
|
+
const schema = buildZodSchema([withBound])
|
|
150
|
+
expect(schema.safeParse({ lines: [{ qty: 5 }] }).success).toBe(true)
|
|
151
|
+
expect(schema.safeParse({ lines: [{ qty: 0 }] }).success).toBe(false)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('un grupo requerido exige al menos un renglón', () => {
|
|
155
|
+
const schema = buildZodSchema([{ ...lineItemsField, required: true }])
|
|
156
|
+
expect(schema.safeParse({ lines: [] }).success).toBe(false)
|
|
157
|
+
expect(schema.safeParse({ lines: [{ product_id: 'p1', quantity: 1 }] }).success).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -38,6 +38,9 @@ import { Loader2 } from 'lucide-react'
|
|
|
38
38
|
import { toast } from 'sonner'
|
|
39
39
|
import { useApi } from './api-context'
|
|
40
40
|
import { DynamicIcon } from './dynamic-icon'
|
|
41
|
+
import { DynamicLineItems } from './dynamic-line-items'
|
|
42
|
+
import { isLineItemsField } from './dynamic-form-schema'
|
|
43
|
+
import type { ActionFieldDef } from './types'
|
|
41
44
|
// Canonical registry lives in @asteby/metacore-sdk
|
|
42
45
|
import {
|
|
43
46
|
type ActionMetadata,
|
|
@@ -172,6 +175,10 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
172
175
|
if (open && action.fields) {
|
|
173
176
|
const defaults: Record<string, any> = {}
|
|
174
177
|
for (const field of action.fields) {
|
|
178
|
+
if (isLineItemsField(field)) {
|
|
179
|
+
defaults[field.key] = field.defaultValue ?? []
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
175
182
|
defaults[field.key] = field.defaultValue ?? (field.type === 'boolean' ? false : '')
|
|
176
183
|
}
|
|
177
184
|
setFormData(defaults)
|
|
@@ -183,7 +190,16 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
183
190
|
const execute = async () => {
|
|
184
191
|
if (action.fields) {
|
|
185
192
|
for (const field of action.fields) {
|
|
186
|
-
if (field.required
|
|
193
|
+
if (!field.required) continue
|
|
194
|
+
if (isLineItemsField(field)) {
|
|
195
|
+
const rows = formData[field.key]
|
|
196
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
197
|
+
toast.error(`${field.label} requiere al menos un renglón`)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
if (!formData[field.key] && formData[field.key] !== false) {
|
|
187
203
|
toast.error(`${field.label} es requerido`)
|
|
188
204
|
return
|
|
189
205
|
}
|
|
@@ -247,10 +263,14 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
247
263
|
}
|
|
248
264
|
|
|
249
265
|
function renderField(
|
|
250
|
-
field:
|
|
266
|
+
field: ActionFieldDef,
|
|
251
267
|
value: any,
|
|
252
268
|
onChange: (value: any) => void,
|
|
253
269
|
) {
|
|
270
|
+
// Repeatable line-items group → row grid (value is an array of row objects).
|
|
271
|
+
if (isLineItemsField(field)) {
|
|
272
|
+
return <DynamicLineItems field={field} value={value} onChange={onChange} />
|
|
273
|
+
}
|
|
254
274
|
switch (field.type) {
|
|
255
275
|
case 'textarea':
|
|
256
276
|
return <Textarea id={field.key} value={value || ''} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)} placeholder={field.placeholder} />
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreateRecordDialog — generic record create/edit dialog.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around the underlying `DynamicRecordDialog` that exposes a
|
|
5
|
+
* narrower, intent-specific API (`onCreate` / `onUpdate` callbacks, `defaults`)
|
|
6
|
+
* matching the Wave 2.5 cleanup spec. Callers that need the full mode-switched
|
|
7
|
+
* dialog (including the read-only `view` mode) should reach for
|
|
8
|
+
* `DynamicRecordDialog` directly.
|
|
9
|
+
*
|
|
10
|
+
* When `recordId` is supplied, the dialog operates in edit mode; otherwise it
|
|
11
|
+
* starts in create mode. Callbacks (`onCreate`, `onUpdate`) are optional —
|
|
12
|
+
* when omitted, the dialog falls back to the configured `useApi()` transport
|
|
13
|
+
* with the default `/dynamic/${modelKey}` endpoint convention.
|
|
14
|
+
*/
|
|
15
|
+
import { DynamicRecordDialog } from './dynamic-record'
|
|
16
|
+
import type { CreateRecordDialogProps } from './types'
|
|
17
|
+
|
|
18
|
+
export function CreateRecordDialog({
|
|
19
|
+
modelKey,
|
|
20
|
+
open,
|
|
21
|
+
onOpenChange,
|
|
22
|
+
recordId,
|
|
23
|
+
endpoint,
|
|
24
|
+
schema,
|
|
25
|
+
defaults,
|
|
26
|
+
onCreate,
|
|
27
|
+
onUpdate,
|
|
28
|
+
onSaved,
|
|
29
|
+
}: CreateRecordDialogProps) {
|
|
30
|
+
const mode = recordId ? 'edit' : 'create'
|
|
31
|
+
return (
|
|
32
|
+
<DynamicRecordDialog
|
|
33
|
+
open={open}
|
|
34
|
+
onOpenChange={onOpenChange}
|
|
35
|
+
mode={mode}
|
|
36
|
+
model={modelKey}
|
|
37
|
+
recordId={recordId}
|
|
38
|
+
endpoint={endpoint}
|
|
39
|
+
schema={schema}
|
|
40
|
+
defaults={defaults}
|
|
41
|
+
onCreate={onCreate}
|
|
42
|
+
onUpdate={onUpdate}
|
|
43
|
+
onSaved={onSaved}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// starter. Host-owned infra that was referenced by alias (axios client,
|
|
4
4
|
// branch store) now flows through <ApiProvider> from runtime-react.
|
|
5
5
|
import { createContext, useContext, useEffect, useRef, useState } from 'react'
|
|
6
|
+
import type { ModelSchema } from './types'
|
|
6
7
|
import {
|
|
7
8
|
Dialog,
|
|
8
9
|
DialogContent,
|
|
@@ -59,11 +60,14 @@ interface FieldDef {
|
|
|
59
60
|
filterBy?: string
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// Permissive shape: the wire payload may omit some fields (e.g. `title` is
|
|
64
|
+
// optional on legacy backends). Keep field types loose so a host-supplied
|
|
65
|
+
// `ModelSchema` (see ./types.ts) is structurally assignable here.
|
|
62
66
|
interface ModalMetadata {
|
|
63
|
-
title
|
|
64
|
-
createTitle
|
|
65
|
-
editTitle
|
|
66
|
-
fields
|
|
67
|
+
title?: string
|
|
68
|
+
createTitle?: string
|
|
69
|
+
editTitle?: string
|
|
70
|
+
fields?: FieldDef[]
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
export interface DynamicRecordDialogProps {
|
|
@@ -74,6 +78,38 @@ export interface DynamicRecordDialogProps {
|
|
|
74
78
|
recordId?: string | null
|
|
75
79
|
endpoint?: string
|
|
76
80
|
onSaved?: () => void
|
|
81
|
+
/**
|
|
82
|
+
* Optional override invoked instead of the default `POST` when the dialog
|
|
83
|
+
* is in `create` mode. Hosts may use this to route writes through custom
|
|
84
|
+
* mutations (optimistic updates, audit hooks, etc.). The dialog still
|
|
85
|
+
* closes and fires `onSaved` on success.
|
|
86
|
+
*/
|
|
87
|
+
onCreate?: (data: Record<string, any>) => Promise<{ id?: string | number } | void>
|
|
88
|
+
/**
|
|
89
|
+
* Optional override invoked instead of the default `PUT` when the dialog
|
|
90
|
+
* is in `edit` mode. Receives the record id and the form payload.
|
|
91
|
+
*/
|
|
92
|
+
onUpdate?: (recordId: string, data: Record<string, any>) => Promise<{ id?: string | number } | void>
|
|
93
|
+
/**
|
|
94
|
+
* Optional default values seeded into the form on `create`. Ignored when
|
|
95
|
+
* `mode` is `'edit'` or `'view'` (those fetch from the record endpoint).
|
|
96
|
+
*/
|
|
97
|
+
defaults?: Record<string, any>
|
|
98
|
+
/**
|
|
99
|
+
* Optional pre-fetched metadata. When provided the dialog skips the
|
|
100
|
+
* `/metadata/modal/:model` request and uses this shape directly.
|
|
101
|
+
*/
|
|
102
|
+
schema?: ModelSchema
|
|
103
|
+
/**
|
|
104
|
+
* Optional handler shown as a "Delete" action in `view` mode. The dialog
|
|
105
|
+
* awaits the promise and closes on success. Omit to hide the action.
|
|
106
|
+
*/
|
|
107
|
+
onDelete?: () => Promise<void>
|
|
108
|
+
/**
|
|
109
|
+
* Optional handler shown as an "Edit" action in `view` mode. Omit to hide
|
|
110
|
+
* the action.
|
|
111
|
+
*/
|
|
112
|
+
onEdit?: () => void
|
|
77
113
|
}
|
|
78
114
|
|
|
79
115
|
function resolvePath(obj: any, path: string): any {
|
|
@@ -136,15 +172,25 @@ export function DynamicRecordDialog({
|
|
|
136
172
|
recordId,
|
|
137
173
|
endpoint,
|
|
138
174
|
onSaved,
|
|
175
|
+
onCreate,
|
|
176
|
+
onUpdate,
|
|
177
|
+
defaults,
|
|
178
|
+
schema,
|
|
179
|
+
onDelete,
|
|
180
|
+
onEdit,
|
|
139
181
|
}: DynamicRecordDialogProps) {
|
|
140
182
|
const api = useApi()
|
|
141
|
-
const [modalMeta, setModalMeta] = useState<ModalMetadata | null>(
|
|
183
|
+
const [modalMeta, setModalMeta] = useState<ModalMetadata | null>(
|
|
184
|
+
schema ? (schema as ModalMetadata) : null,
|
|
185
|
+
)
|
|
142
186
|
const [record, setRecord] = useState<any | null>(null)
|
|
143
187
|
const [formValues, setFormValues] = useState<Record<string, any>>({})
|
|
144
188
|
const [loading, setLoading] = useState(false)
|
|
145
189
|
const [saving, setSaving] = useState(false)
|
|
190
|
+
const [deleting, setDeleting] = useState(false)
|
|
146
191
|
|
|
147
192
|
const isCreate = mode === 'create'
|
|
193
|
+
const isView = mode === 'view'
|
|
148
194
|
const isEditable = mode === 'create' || mode === 'edit'
|
|
149
195
|
const config = MODE_CONFIG[mode]
|
|
150
196
|
|
|
@@ -157,16 +203,21 @@ export function DynamicRecordDialog({
|
|
|
157
203
|
const load = async () => {
|
|
158
204
|
setLoading(true)
|
|
159
205
|
try {
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
206
|
+
let meta: ModalMetadata | null = schema ? (schema as ModalMetadata) : null
|
|
207
|
+
if (!meta) {
|
|
208
|
+
const metaRes = await api.get(`/metadata/modal/${model}`)
|
|
209
|
+
if (cancelled) return
|
|
210
|
+
meta = metaRes.data?.data ?? metaRes.data
|
|
211
|
+
}
|
|
164
212
|
setModalMeta(meta)
|
|
165
213
|
|
|
166
214
|
if (isCreate) {
|
|
167
215
|
const initial: Record<string, any> = {}
|
|
168
|
-
for (const field of meta
|
|
169
|
-
initial[field.key] =
|
|
216
|
+
for (const field of meta?.fields ?? []) {
|
|
217
|
+
initial[field.key] =
|
|
218
|
+
(defaults && Object.prototype.hasOwnProperty.call(defaults, field.key)
|
|
219
|
+
? defaults[field.key]
|
|
220
|
+
: field.defaultValue) ?? ''
|
|
170
221
|
}
|
|
171
222
|
setFormValues(initial)
|
|
172
223
|
} else {
|
|
@@ -181,7 +232,7 @@ export function DynamicRecordDialog({
|
|
|
181
232
|
setRecord(rec)
|
|
182
233
|
|
|
183
234
|
const initial: Record<string, any> = {}
|
|
184
|
-
for (const field of meta
|
|
235
|
+
for (const field of meta?.fields ?? []) {
|
|
185
236
|
initial[field.key] = resolvePath(rec, field.key) ?? field.defaultValue ?? ''
|
|
186
237
|
}
|
|
187
238
|
setFormValues(initial)
|
|
@@ -196,7 +247,7 @@ export function DynamicRecordDialog({
|
|
|
196
247
|
|
|
197
248
|
load()
|
|
198
249
|
return () => { cancelled = true }
|
|
199
|
-
}, [open, recordId, model, endpoint, isCreate])
|
|
250
|
+
}, [open, recordId, model, endpoint, isCreate, schema, defaults])
|
|
200
251
|
|
|
201
252
|
useEffect(() => {
|
|
202
253
|
if (!open) {
|
|
@@ -211,7 +262,7 @@ export function DynamicRecordDialog({
|
|
|
211
262
|
if (!modalMeta) return
|
|
212
263
|
|
|
213
264
|
if (isEditable) {
|
|
214
|
-
for (const field of modalMeta.fields) {
|
|
265
|
+
for (const field of modalMeta.fields ?? []) {
|
|
215
266
|
if (field.required && !formValues[field.key] && formValues[field.key] !== 0 && formValues[field.key] !== false) {
|
|
216
267
|
toast.error(`El campo "${field.label}" es obligatorio`)
|
|
217
268
|
return
|
|
@@ -221,6 +272,22 @@ export function DynamicRecordDialog({
|
|
|
221
272
|
|
|
222
273
|
setSaving(true)
|
|
223
274
|
try {
|
|
275
|
+
if (isCreate && onCreate) {
|
|
276
|
+
await onCreate(formValues)
|
|
277
|
+
toast.success('Registro creado correctamente')
|
|
278
|
+
onSaved?.()
|
|
279
|
+
onOpenChange(false)
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!isCreate && recordId && onUpdate) {
|
|
284
|
+
await onUpdate(String(recordId), formValues)
|
|
285
|
+
toast.success('Guardado correctamente')
|
|
286
|
+
onSaved?.()
|
|
287
|
+
onOpenChange(false)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
224
291
|
let res
|
|
225
292
|
if (isCreate) {
|
|
226
293
|
const createEndpoint = endpoint || `/dynamic/${model}`
|
|
@@ -246,6 +313,20 @@ export function DynamicRecordDialog({
|
|
|
246
313
|
}
|
|
247
314
|
}
|
|
248
315
|
|
|
316
|
+
const handleDelete = async () => {
|
|
317
|
+
if (!onDelete) return
|
|
318
|
+
setDeleting(true)
|
|
319
|
+
try {
|
|
320
|
+
await onDelete()
|
|
321
|
+
onOpenChange(false)
|
|
322
|
+
} catch (err: any) {
|
|
323
|
+
console.error('[DynamicRecordDialog] delete error:', err)
|
|
324
|
+
toast.error(err?.response?.data?.message || err?.message || 'Error al eliminar')
|
|
325
|
+
} finally {
|
|
326
|
+
setDeleting(false)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
249
330
|
const title = modalMeta ? config.getTitle(modalMeta) : ''
|
|
250
331
|
|
|
251
332
|
const visibleFields = modalMeta?.fields?.filter(f => {
|
|
@@ -311,9 +392,24 @@ export function DynamicRecordDialog({
|
|
|
311
392
|
</div>
|
|
312
393
|
|
|
313
394
|
<DialogFooter className="p-4 border-t shrink-0">
|
|
314
|
-
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
|
|
395
|
+
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving || deleting}>
|
|
315
396
|
{config.cancelLabel}
|
|
316
397
|
</Button>
|
|
398
|
+
{isView && onDelete && (
|
|
399
|
+
<Button
|
|
400
|
+
variant="destructive"
|
|
401
|
+
onClick={handleDelete}
|
|
402
|
+
disabled={deleting || loading}
|
|
403
|
+
>
|
|
404
|
+
{deleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
405
|
+
{deleting ? 'Eliminando...' : 'Eliminar'}
|
|
406
|
+
</Button>
|
|
407
|
+
)}
|
|
408
|
+
{isView && onEdit && (
|
|
409
|
+
<Button onClick={onEdit} disabled={deleting || loading}>
|
|
410
|
+
Editar
|
|
411
|
+
</Button>
|
|
412
|
+
)}
|
|
317
413
|
{isEditable && (
|
|
318
414
|
<Button
|
|
319
415
|
type="submit"
|