@asteby/metacore-runtime-react 10.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  3. package/dist/action-modal-dispatcher.js +21 -1
  4. package/dist/dialogs/create-record-dialog.d.ts +3 -0
  5. package/dist/dialogs/create-record-dialog.d.ts.map +1 -0
  6. package/dist/dialogs/create-record-dialog.js +20 -0
  7. package/dist/dialogs/dynamic-record.d.ts +38 -1
  8. package/dist/dialogs/dynamic-record.d.ts.map +1 -1
  9. package/dist/dialogs/dynamic-record.js +50 -12
  10. package/dist/dialogs/types.d.ts +115 -0
  11. package/dist/dialogs/types.d.ts.map +1 -0
  12. package/dist/dialogs/types.js +15 -0
  13. package/dist/dialogs/view-record-dialog.d.ts +3 -0
  14. package/dist/dialogs/view-record-dialog.d.ts.map +1 -0
  15. package/dist/dialogs/view-record-dialog.js +15 -0
  16. package/dist/dynamic-form-schema.d.ts +9 -0
  17. package/dist/dynamic-form-schema.d.ts.map +1 -1
  18. package/dist/dynamic-form-schema.js +22 -0
  19. package/dist/dynamic-form.d.ts +1 -0
  20. package/dist/dynamic-form.d.ts.map +1 -1
  21. package/dist/dynamic-form.js +12 -1
  22. package/dist/dynamic-line-items.d.ts +9 -0
  23. package/dist/dynamic-line-items.d.ts.map +1 -0
  24. package/dist/dynamic-line-items.js +64 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/slot.d.ts.map +1 -1
  29. package/dist/slot.js +2 -0
  30. package/dist/types.d.ts +10 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/package.json +6 -6
  33. package/src/__tests__/dynamic-form.test.ts +56 -1
  34. package/src/__tests__/slot.test.ts +70 -0
  35. package/src/action-modal-dispatcher.tsx +22 -2
  36. package/src/dialogs/create-record-dialog.tsx +46 -0
  37. package/src/dialogs/dynamic-record.tsx +111 -15
  38. package/src/dialogs/types.ts +119 -0
  39. package/src/dialogs/view-record-dialog.tsx +37 -0
  40. package/src/dynamic-form-schema.ts +25 -0
  41. package/src/dynamic-form.tsx +12 -1
  42. package/src/dynamic-line-items.tsx +221 -0
  43. package/src/index.ts +10 -0
  44. package/src/slot.tsx +2 -0
  45. 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';
@@ -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';
@@ -1 +1 @@
1
- {"version":3,"file":"slot.d.ts","sourceRoot":"","sources":["../src/slot.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA+B,MAAM,OAAO,CAAA;AAEnD,MAAM,MAAM,aAAa,CAAC,CAAC,GAAG,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAA;AAE3D,UAAU,SAAS;IACf,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,aAAa,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAA;AAE1B,cAAM,gBAAgB;IAClB,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,SAAS,CAAsB;IAEvC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,IAAI;IAkB7G,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE;IAIhC,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI;IAKzC,OAAO,CAAC,IAAI;CACf;AAED,eAAO,MAAM,YAAY,kBAAyB,CAAA;AAElD,MAAM,WAAW,SAAS;IACtB,eAAe;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAED,wBAAgB,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAe,EAAE,EAAE,SAAS,2CAe/D"}
1
+ {"version":3,"file":"slot.d.ts","sourceRoot":"","sources":["../src/slot.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA+B,MAAM,OAAO,CAAA;AAEnD,MAAM,MAAM,aAAa,CAAC,CAAC,GAAG,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAA;AAE3D,UAAU,SAAS;IACf,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,aAAa,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAA;AAE1B,cAAM,gBAAgB;IAClB,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,SAAS,CAAsB;IAEvC,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,IAAI;IAoB7G,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE;IAIhC,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,IAAI;IAKzC,OAAO,CAAC,IAAI;CACf;AAED,eAAO,MAAM,YAAY,kBAAyB,CAAA;AAElD,MAAM,WAAW,SAAS;IACtB,eAAe;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC7B;AAED,wBAAgB,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAe,EAAE,EAAE,SAAS,2CAe/D"}
package/dist/slot.js CHANGED
@@ -10,6 +10,8 @@ class SlotRegistryImpl {
10
10
  const entry = { id: slotId, component, priority: opts?.priority ?? 0, source: opts?.source };
11
11
  const list = this.slots.get(slotId) ?? [];
12
12
  list.push(entry);
13
+ // Higher priority renders first — canonical across SDK and runtime-react.
14
+ // See docs/slot-priority.md.
13
15
  list.sort((a, b) => b.priority - a.priority);
14
16
  this.slots.set(slotId, list);
15
17
  this.emit();
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;
@@ -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": "10.0.0",
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": "^2.5.0",
36
- "@asteby/metacore-ui": "^2.0.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": "^9.0.0",
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": "2.5.0",
64
- "@asteby/metacore-ui": "2.0.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
+ })
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { ComponentType } from 'react'
3
+ import { slotRegistry } from '../slot'
4
+
5
+ // Dummy components — slot registry stores+sorts, doesn't render.
6
+ const A: ComponentType = () => null
7
+ const B: ComponentType = () => null
8
+ const C: ComponentType = () => null
9
+ const D: ComponentType = () => null
10
+
11
+ describe('slotRegistry priority ordering', () => {
12
+ it('renders higher priority first (DESC) — canonical contract', () => {
13
+ const slot = `test.desc.${Math.random()}`
14
+ const off1 = slotRegistry.register(slot, A, { priority: 1 })
15
+ const off2 = slotRegistry.register(slot, B, { priority: 5 })
16
+ const off3 = slotRegistry.register(slot, C, { priority: 3 })
17
+
18
+ const items = slotRegistry.get(slot)
19
+ expect(items.map((i) => i.priority)).toEqual([5, 3, 1])
20
+ expect(items.map((i) => i.component)).toEqual([B, C, A])
21
+
22
+ off1()
23
+ off2()
24
+ off3()
25
+ })
26
+
27
+ it('treats missing priority as 0', () => {
28
+ const slot = `test.zero.${Math.random()}`
29
+ const off1 = slotRegistry.register(slot, A)
30
+ const off2 = slotRegistry.register(slot, B, { priority: 10 })
31
+ const off3 = slotRegistry.register(slot, C, { priority: -5 })
32
+
33
+ const items = slotRegistry.get(slot)
34
+ expect(items.map((i) => i.component)).toEqual([B, A, C])
35
+
36
+ off1()
37
+ off2()
38
+ off3()
39
+ })
40
+
41
+ it('preserves insertion order on ties', () => {
42
+ const slot = `test.ties.${Math.random()}`
43
+ const off1 = slotRegistry.register(slot, A, { priority: 1 })
44
+ const off2 = slotRegistry.register(slot, B, { priority: 1 })
45
+ const off3 = slotRegistry.register(slot, C, { priority: 2 })
46
+ const off4 = slotRegistry.register(slot, D, { priority: 1 })
47
+
48
+ const items = slotRegistry.get(slot)
49
+ expect(items.map((i) => i.component)).toEqual([C, A, B, D])
50
+
51
+ off1()
52
+ off2()
53
+ off3()
54
+ off4()
55
+ })
56
+
57
+ it('unregister removes the entry and notifies subscribers', () => {
58
+ const slot = `test.unreg.${Math.random()}`
59
+ let notifications = 0
60
+ const unsubscribe = slotRegistry.subscribe(() => { notifications++ })
61
+ const off = slotRegistry.register(slot, A, { priority: 1 })
62
+ expect(slotRegistry.get(slot)).toHaveLength(1)
63
+ expect(notifications).toBeGreaterThanOrEqual(1)
64
+ const before = notifications
65
+ off()
66
+ expect(slotRegistry.get(slot)).toHaveLength(0)
67
+ expect(notifications).toBeGreaterThan(before)
68
+ unsubscribe()
69
+ })
70
+ })
@@ -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 && !formData[field.key] && formData[field.key] !== false) {
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: { key: string; type: string; options?: { value: string; label: string }[]; placeholder?: string },
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: string
64
- createTitle: string
65
- editTitle: string
66
- fields: FieldDef[]
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>(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
- const metaRes = await api.get(`/metadata/modal/${model}`)
161
- if (cancelled) return
162
-
163
- const meta: ModalMetadata = metaRes.data?.data ?? metaRes.data
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.fields ?? []) {
169
- initial[field.key] = field.defaultValue ?? ''
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.fields ?? []) {
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"