@asteby/metacore-runtime-react 11.0.0 → 13.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 (51) hide show
  1. package/CHANGELOG.md +74 -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-crud-page.d.ts.map +1 -1
  17. package/dist/dynamic-crud-page.js +6 -2
  18. package/dist/dynamic-form-schema.d.ts +9 -0
  19. package/dist/dynamic-form-schema.d.ts.map +1 -1
  20. package/dist/dynamic-form-schema.js +22 -0
  21. package/dist/dynamic-form.d.ts +1 -0
  22. package/dist/dynamic-form.d.ts.map +1 -1
  23. package/dist/dynamic-form.js +12 -1
  24. package/dist/dynamic-line-items.d.ts +9 -0
  25. package/dist/dynamic-line-items.d.ts.map +1 -0
  26. package/dist/dynamic-line-items.js +64 -0
  27. package/dist/dynamic-table.d.ts.map +1 -1
  28. package/dist/dynamic-table.js +7 -1
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/model-action-toolbar.d.ts +27 -0
  33. package/dist/model-action-toolbar.d.ts.map +1 -0
  34. package/dist/model-action-toolbar.js +88 -0
  35. package/dist/types.d.ts +18 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +6 -6
  38. package/src/__tests__/dynamic-form.test.ts +56 -1
  39. package/src/action-modal-dispatcher.tsx +22 -2
  40. package/src/dialogs/create-record-dialog.tsx +46 -0
  41. package/src/dialogs/dynamic-record.tsx +111 -15
  42. package/src/dialogs/types.ts +119 -0
  43. package/src/dialogs/view-record-dialog.tsx +37 -0
  44. package/src/dynamic-crud-page.tsx +11 -1
  45. package/src/dynamic-form-schema.ts +25 -0
  46. package/src/dynamic-form.tsx +12 -1
  47. package/src/dynamic-line-items.tsx +221 -0
  48. package/src/dynamic-table.tsx +7 -1
  49. package/src/index.ts +16 -0
  50. package/src/model-action-toolbar.tsx +154 -0
  51. package/src/types.ts +18 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,79 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 13.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7ea7caa: Acciones con `placement` (`row` | `table` | `create`) y nuevo primitivo `<ModelActionToolbar>`.
8
+
9
+ `ActionMetadata`/`ActionDefinition` ganan `placement`, espejando `manifest/v3` Action.placement del kernel (v0.30.0):
10
+ - `row` (default) — acción por fila dentro de `<DynamicTable>` (comportamiento actual).
11
+ - `table` — botón en la toolbar de la página, sin contexto de record.
12
+ - `create` — botón en la toolbar que **reemplaza** el botón "crear" genérico, para addons que traen una experiencia de creación custom (p.ej. un asiento contable con líneas débito/crédito).
13
+
14
+ `<ModelActionToolbar>` (+ hook `useModelActions`) es el primitivo genérico que renderiza esos triggers de nivel página y monta el `ActionModalDispatcher` (record vacío para `create`). Resuelve tanto modales federados custom (vía el action registry) como el form declarativo genérico. `DynamicCRUDPage` lo consume internamente y suprime su botón crear cuando existe una acción `create`; `DynamicTable` excluye los placements `table`/`create` de la columna de acciones por fila. Los hosts ya no reimplementan el plumbing de botones de acción — montan `<ModelActionToolbar>` y listo.
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [7ea7caa]
19
+ - Updated dependencies [3b40ed5]
20
+ - @asteby/metacore-sdk@3.1.0
21
+ - @asteby/metacore-ui@2.1.0
22
+
23
+ ## 12.0.0
24
+
25
+ ### Minor Changes
26
+
27
+ - 5e17059: Add declarative line-items (repeatable group) support to the action
28
+ form renderer, pairing with the kernel v3 `ActionField.item_fields`
29
+ addition. Multi-line action modals (e.g. a "Recibir mercancía" modal
30
+ with N item rows, or a journal entry with N debit/credit lines) can now
31
+ be declared in the manifest instead of needing a custom federated modal.
32
+ - `ActionFieldDef` gains `itemFields?: ActionFieldDef[]` (mirrors the v3
33
+ `item_fields`). A field carrying item columns is a repeatable group;
34
+ its value is an array of row objects keyed by the item field keys.
35
+ - `buildZodSchema` now builds `z.array(z.object(...))` for line-items
36
+ fields, applying each column's per-cell rules per row; a required
37
+ group requires at least one row. New `isLineItemsField` / `getItemFields`
38
+ helpers tolerate both camelCase `itemFields` and raw snake_case
39
+ `item_fields` served by the kernel.
40
+ - New `DynamicLineItems` component renders a row grid (header from the
41
+ item field labels, add/remove row controls, each cell a widget via
42
+ `resolveWidget`, including `ref`-driven selects). It is wired into both
43
+ `DynamicForm` and `ActionModalDispatcher`'s declarative-fields path.
44
+
45
+ Additive only: existing flat-field rendering is unchanged.
46
+
47
+ - 212c6ab: Add `CreateRecordDialog` and `ViewRecordDialog` to
48
+ `@asteby/metacore-runtime-react` (Wave 2.5 cleanup).
49
+
50
+ Both components are thin, intent-specific wrappers over the existing
51
+ `DynamicRecordDialog`. They surface a narrower, callback-driven API so
52
+ addons can mount create/edit/view dialogs without having to pre-select
53
+ a `mode` and without coupling to product-specific affordances:
54
+ - `CreateRecordDialog` — opens in create mode by default; passing
55
+ `recordId` flips it to edit. Optional `onCreate` / `onUpdate`
56
+ callbacks override the default `useApi()` POST/PUT calls, and
57
+ `defaults` seeds the form on create.
58
+ - `ViewRecordDialog` — read-only viewer with optional `onEdit` /
59
+ `onDelete` affordances (footer buttons are hidden when the callback
60
+ is not provided).
61
+ - New shared types: `ModelKey`, `ModelSchema`, `RecordDialogProps`,
62
+ `CreateRecordDialogProps`, `ViewRecordDialogProps`, `CreateResult`.
63
+
64
+ `DynamicRecordDialog` itself gains the same optional props
65
+ (`onCreate`, `onUpdate`, `defaults`, `schema`, `onEdit`, `onDelete`)
66
+ so existing consumers keep working unchanged. The product-specific
67
+ dialogs that used to live in `ops/frontend` (with pricing rules, media
68
+ galleries, category-driven custom attributes) are intentionally NOT
69
+ promoted to the SDK — those stay in the host as they are product
70
+ domain concerns. Generic record CRUD lives here.
71
+
72
+ ### Patch Changes
73
+
74
+ - Updated dependencies [26063a4]
75
+ - @asteby/metacore-sdk@3.0.0
76
+
3
77
  ## 11.0.0
4
78
 
5
79
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AAyCA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAEhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,kDAiDlB"}
1
+ {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA4CA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAEhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,kDAiDlB"}
@@ -14,6 +14,8 @@ import { Loader2 } from 'lucide-react';
14
14
  import { toast } from 'sonner';
15
15
  import { useApi } from './api-context';
16
16
  import { DynamicIcon } from './dynamic-icon';
17
+ import { DynamicLineItems } from './dynamic-line-items';
18
+ import { isLineItemsField } from './dynamic-form-schema';
17
19
  // Canonical registry lives in @asteby/metacore-sdk
18
20
  import { getActionComponent, } from '@asteby/metacore-sdk';
19
21
  export function ActionModalDispatcher({ open, onOpenChange, action, model, record, endpoint, onSuccess, }) {
@@ -68,6 +70,10 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
68
70
  if (open && action.fields) {
69
71
  const defaults = {};
70
72
  for (const field of action.fields) {
73
+ if (isLineItemsField(field)) {
74
+ defaults[field.key] = field.defaultValue ?? [];
75
+ continue;
76
+ }
71
77
  defaults[field.key] = field.defaultValue ?? (field.type === 'boolean' ? false : '');
72
78
  }
73
79
  setFormData(defaults);
@@ -77,7 +83,17 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
77
83
  const execute = async () => {
78
84
  if (action.fields) {
79
85
  for (const field of action.fields) {
80
- if (field.required && !formData[field.key] && formData[field.key] !== false) {
86
+ if (!field.required)
87
+ continue;
88
+ if (isLineItemsField(field)) {
89
+ const rows = formData[field.key];
90
+ if (!Array.isArray(rows) || rows.length === 0) {
91
+ toast.error(`${field.label} requiere al menos un renglón`);
92
+ return;
93
+ }
94
+ continue;
95
+ }
96
+ if (!formData[field.key] && formData[field.key] !== false) {
81
97
  toast.error(`${field.label} es requerido`);
82
98
  return;
83
99
  }
@@ -106,6 +122,10 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
106
122
  return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), action.label] }), action.confirmMessage && _jsx(DialogDescription, { children: action.confirmMessage })] }), _jsx("div", { className: "grid gap-4 py-4", children: action.fields?.map((field) => (_jsxs("div", { className: "grid gap-2", children: [_jsxs(Label, { htmlFor: field.key, children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }), renderField(field, formData[field.key], (v) => updateField(field.key, v))] }, field.key))) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), action.label] })] })] }) }));
107
123
  }
108
124
  function renderField(field, value, onChange) {
125
+ // Repeatable line-items group → row grid (value is an array of row objects).
126
+ if (isLineItemsField(field)) {
127
+ return _jsx(DynamicLineItems, { field: field, value: value, onChange: onChange });
128
+ }
109
129
  switch (field.type) {
110
130
  case 'textarea':
111
131
  return _jsx(Textarea, { id: field.key, value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
@@ -0,0 +1,3 @@
1
+ import type { CreateRecordDialogProps } from './types';
2
+ export declare function CreateRecordDialog({ modelKey, open, onOpenChange, recordId, endpoint, schema, defaults, onCreate, onUpdate, onSaved, }: CreateRecordDialogProps): import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=create-record-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-record-dialog.d.ts","sourceRoot":"","sources":["../../src/dialogs/create-record-dialog.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAEtD,wBAAgB,kBAAkB,CAAC,EAC/B,QAAQ,EACR,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,OAAO,GACV,EAAE,uBAAuB,2CAiBzB"}
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * CreateRecordDialog — generic record create/edit dialog.
4
+ *
5
+ * Thin wrapper around the underlying `DynamicRecordDialog` that exposes a
6
+ * narrower, intent-specific API (`onCreate` / `onUpdate` callbacks, `defaults`)
7
+ * matching the Wave 2.5 cleanup spec. Callers that need the full mode-switched
8
+ * dialog (including the read-only `view` mode) should reach for
9
+ * `DynamicRecordDialog` directly.
10
+ *
11
+ * When `recordId` is supplied, the dialog operates in edit mode; otherwise it
12
+ * starts in create mode. Callbacks (`onCreate`, `onUpdate`) are optional —
13
+ * when omitted, the dialog falls back to the configured `useApi()` transport
14
+ * with the default `/dynamic/${modelKey}` endpoint convention.
15
+ */
16
+ import { DynamicRecordDialog } from './dynamic-record';
17
+ export function CreateRecordDialog({ modelKey, open, onOpenChange, recordId, endpoint, schema, defaults, onCreate, onUpdate, onSaved, }) {
18
+ const mode = recordId ? 'edit' : 'create';
19
+ return (_jsx(DynamicRecordDialog, { open: open, onOpenChange: onOpenChange, mode: mode, model: modelKey, recordId: recordId, endpoint: endpoint, schema: schema, defaults: defaults, onCreate: onCreate, onUpdate: onUpdate, onSaved: onSaved }));
20
+ }
@@ -1,3 +1,4 @@
1
+ import type { ModelSchema } from './types';
1
2
  export interface DynamicRecordDialogProps {
2
3
  open: boolean;
3
4
  onOpenChange: (open: boolean) => void;
@@ -6,6 +7,42 @@ export interface DynamicRecordDialogProps {
6
7
  recordId?: string | null;
7
8
  endpoint?: string;
8
9
  onSaved?: () => void;
10
+ /**
11
+ * Optional override invoked instead of the default `POST` when the dialog
12
+ * is in `create` mode. Hosts may use this to route writes through custom
13
+ * mutations (optimistic updates, audit hooks, etc.). The dialog still
14
+ * closes and fires `onSaved` on success.
15
+ */
16
+ onCreate?: (data: Record<string, any>) => Promise<{
17
+ id?: string | number;
18
+ } | void>;
19
+ /**
20
+ * Optional override invoked instead of the default `PUT` when the dialog
21
+ * is in `edit` mode. Receives the record id and the form payload.
22
+ */
23
+ onUpdate?: (recordId: string, data: Record<string, any>) => Promise<{
24
+ id?: string | number;
25
+ } | void>;
26
+ /**
27
+ * Optional default values seeded into the form on `create`. Ignored when
28
+ * `mode` is `'edit'` or `'view'` (those fetch from the record endpoint).
29
+ */
30
+ defaults?: Record<string, any>;
31
+ /**
32
+ * Optional pre-fetched metadata. When provided the dialog skips the
33
+ * `/metadata/modal/:model` request and uses this shape directly.
34
+ */
35
+ schema?: ModelSchema;
36
+ /**
37
+ * Optional handler shown as a "Delete" action in `view` mode. The dialog
38
+ * awaits the promise and closes on success. Omit to hide the action.
39
+ */
40
+ onDelete?: () => Promise<void>;
41
+ /**
42
+ * Optional handler shown as an "Edit" action in `view` mode. Omit to hide
43
+ * the action.
44
+ */
45
+ onEdit?: () => void;
9
46
  }
10
- export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, }: DynamicRecordDialogProps): import("react/jsx-runtime").JSX.Element;
47
+ export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, }: DynamicRecordDialogProps): import("react/jsx-runtime").JSX.Element;
11
48
  //# sourceMappingURL=dynamic-record.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAoEA,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAsDD,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,GACV,EAAE,wBAAwB,2CAgM1B"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAmE1C,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAsDD,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,MAAM,GACT,EAAE,wBAAwB,2CAsP1B"}
@@ -60,14 +60,16 @@ const MODE_CONFIG = {
60
60
  },
61
61
  };
62
62
  const ModelContext = createContext('');
63
- export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, }) {
63
+ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, }) {
64
64
  const api = useApi();
65
- const [modalMeta, setModalMeta] = useState(null);
65
+ const [modalMeta, setModalMeta] = useState(schema ? schema : null);
66
66
  const [record, setRecord] = useState(null);
67
67
  const [formValues, setFormValues] = useState({});
68
68
  const [loading, setLoading] = useState(false);
69
69
  const [saving, setSaving] = useState(false);
70
+ const [deleting, setDeleting] = useState(false);
70
71
  const isCreate = mode === 'create';
72
+ const isView = mode === 'view';
71
73
  const isEditable = mode === 'create' || mode === 'edit';
72
74
  const config = MODE_CONFIG[mode];
73
75
  useEffect(() => {
@@ -79,15 +81,21 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
79
81
  const load = async () => {
80
82
  setLoading(true);
81
83
  try {
82
- const metaRes = await api.get(`/metadata/modal/${model}`);
83
- if (cancelled)
84
- return;
85
- const meta = metaRes.data?.data ?? metaRes.data;
84
+ let meta = schema ? schema : null;
85
+ if (!meta) {
86
+ const metaRes = await api.get(`/metadata/modal/${model}`);
87
+ if (cancelled)
88
+ return;
89
+ meta = metaRes.data?.data ?? metaRes.data;
90
+ }
86
91
  setModalMeta(meta);
87
92
  if (isCreate) {
88
93
  const initial = {};
89
- for (const field of meta.fields ?? []) {
90
- initial[field.key] = field.defaultValue ?? '';
94
+ for (const field of meta?.fields ?? []) {
95
+ initial[field.key] =
96
+ (defaults && Object.prototype.hasOwnProperty.call(defaults, field.key)
97
+ ? defaults[field.key]
98
+ : field.defaultValue) ?? '';
91
99
  }
92
100
  setFormValues(initial);
93
101
  }
@@ -101,7 +109,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
101
109
  const rec = recRes.data?.data ?? recRes.data;
102
110
  setRecord(rec);
103
111
  const initial = {};
104
- for (const field of meta.fields ?? []) {
112
+ for (const field of meta?.fields ?? []) {
105
113
  initial[field.key] = resolvePath(rec, field.key) ?? field.defaultValue ?? '';
106
114
  }
107
115
  setFormValues(initial);
@@ -118,7 +126,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
118
126
  };
119
127
  load();
120
128
  return () => { cancelled = true; };
121
- }, [open, recordId, model, endpoint, isCreate]);
129
+ }, [open, recordId, model, endpoint, isCreate, schema, defaults]);
122
130
  useEffect(() => {
123
131
  if (!open) {
124
132
  setModalMeta(null);
@@ -131,7 +139,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
131
139
  if (!modalMeta)
132
140
  return;
133
141
  if (isEditable) {
134
- for (const field of modalMeta.fields) {
142
+ for (const field of modalMeta.fields ?? []) {
135
143
  if (field.required && !formValues[field.key] && formValues[field.key] !== 0 && formValues[field.key] !== false) {
136
144
  toast.error(`El campo "${field.label}" es obligatorio`);
137
145
  return;
@@ -140,6 +148,20 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
140
148
  }
141
149
  setSaving(true);
142
150
  try {
151
+ if (isCreate && onCreate) {
152
+ await onCreate(formValues);
153
+ toast.success('Registro creado correctamente');
154
+ onSaved?.();
155
+ onOpenChange(false);
156
+ return;
157
+ }
158
+ if (!isCreate && recordId && onUpdate) {
159
+ await onUpdate(String(recordId), formValues);
160
+ toast.success('Guardado correctamente');
161
+ onSaved?.();
162
+ onOpenChange(false);
163
+ return;
164
+ }
143
165
  let res;
144
166
  if (isCreate) {
145
167
  const createEndpoint = endpoint || `/dynamic/${model}`;
@@ -167,6 +189,22 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
167
189
  setSaving(false);
168
190
  }
169
191
  };
192
+ const handleDelete = async () => {
193
+ if (!onDelete)
194
+ return;
195
+ setDeleting(true);
196
+ try {
197
+ await onDelete();
198
+ onOpenChange(false);
199
+ }
200
+ catch (err) {
201
+ console.error('[DynamicRecordDialog] delete error:', err);
202
+ toast.error(err?.response?.data?.message || err?.message || 'Error al eliminar');
203
+ }
204
+ finally {
205
+ setDeleting(false);
206
+ }
207
+ };
170
208
  const title = modalMeta ? config.getTitle(modalMeta) : '';
171
209
  const visibleFields = modalMeta?.fields?.filter(f => {
172
210
  if (f.hidden)
@@ -178,7 +216,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
178
216
  return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
179
217
  const isFullWidth = field.type === 'textarea';
180
218
  return (_jsx("div", { className: isFullWidth ? 'sm:col-span-2' : '', children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
181
- }), record?.external_url && (_jsx("div", { className: "sm:col-span-2", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }))] }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving, children: config.cancelLabel }), isEditable && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] }) }));
219
+ }), record?.external_url && (_jsx("div", { className: "sm:col-span-2", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }))] }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel }), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] }) }));
182
220
  }
183
221
  function LoadingSkeleton() {
184
222
  return (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-9 w-full" })] }, i))) }));
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Shared types for record-oriented dialogs in `@asteby/metacore-runtime-react`.
3
+ *
4
+ * These dialogs are intentionally generic: any addon that drives a metadata-
5
+ * backed CRUD surface can render them by passing a `modelKey`. The dialogs
6
+ * resolve field shape, labels, and validation hints from the metadata server
7
+ * (default: `/metadata/modal/:modelKey`) via the configured `useApi()`
8
+ * transport. Hosts may override transport behaviour by supplying explicit
9
+ * `endpoint` overrides and/or `onCreate` / `onDelete` callbacks.
10
+ *
11
+ * Wave 2.5 cleanup — replaces the product-specific dialogs that used to live
12
+ * in `ops/frontend/src/components/dynamic/` for any record kind that does not
13
+ * need pricing rules, media galleries, or category-driven custom fields.
14
+ */
15
+ /**
16
+ * Identifier for a model registered with the metadata service. The wire format
17
+ * is a plain string (e.g. `"products"`, `"organizations"`, `"users"`). The
18
+ * dialogs do not interpret this value; it is forwarded to the API transport.
19
+ */
20
+ export type ModelKey = string;
21
+ /**
22
+ * Optional schema hint, accepted only as a passthrough for hosts that want to
23
+ * supply pre-fetched metadata instead of waiting for the dialog to call
24
+ * `/metadata/modal/:modelKey`. The runtime intentionally keeps the shape loose
25
+ * because metadata is owned by each backend addon.
26
+ */
27
+ export interface ModelSchema {
28
+ title?: string;
29
+ createTitle?: string;
30
+ editTitle?: string;
31
+ fields?: Array<{
32
+ key: string;
33
+ label: string;
34
+ type: string;
35
+ required?: boolean;
36
+ defaultValue?: unknown;
37
+ placeholder?: string;
38
+ readonly?: boolean;
39
+ hidden?: boolean;
40
+ [extra: string]: unknown;
41
+ }>;
42
+ }
43
+ /**
44
+ * Result shape returned by `onCreate` callbacks. Hosts may return `void` if
45
+ * the resulting record id is not needed downstream.
46
+ */
47
+ export type CreateResult = {
48
+ id: string;
49
+ } | void;
50
+ /**
51
+ * Base props shared by every record dialog.
52
+ */
53
+ export interface RecordDialogProps {
54
+ /** Model key forwarded to the metadata service and transport calls. */
55
+ modelKey: ModelKey;
56
+ /** Controlled open state. */
57
+ open: boolean;
58
+ /** Open-state controller. */
59
+ onOpenChange: (open: boolean) => void;
60
+ /**
61
+ * Optional custom endpoint base. When provided, the dialog uses
62
+ * `${endpoint}` (for create/list) and `${endpoint}/${recordId}` (for
63
+ * fetch/update/delete). When omitted, the dialog uses
64
+ * `/dynamic/${modelKey}` / `/dynamic/${modelKey}/${recordId}`.
65
+ */
66
+ endpoint?: string;
67
+ /**
68
+ * Optional pre-fetched schema. When omitted, the dialog fetches metadata
69
+ * from `/metadata/modal/${modelKey}` via the configured API transport.
70
+ */
71
+ schema?: ModelSchema;
72
+ }
73
+ /**
74
+ * Props for the create / edit dialog.
75
+ *
76
+ * When `recordId` is provided the dialog operates in edit mode; otherwise it
77
+ * starts in create mode. Supplying `onCreate` overrides the default transport
78
+ * call so that hosts may route writes through custom mutations (optimistic
79
+ * updates, audit hooks, etc.). The default behaviour POSTs/PUTs through the
80
+ * configured `useApi()` transport.
81
+ */
82
+ export interface CreateRecordDialogProps extends RecordDialogProps {
83
+ /** When set, the dialog operates as an editor for this record id. */
84
+ recordId?: string | null;
85
+ /**
86
+ * Optional override invoked instead of the default POST. The dialog still
87
+ * closes and calls `onSaved` on success. Hosts that need to support both
88
+ * create and edit through callbacks should also pass `onUpdate`.
89
+ */
90
+ onCreate?: (data: Record<string, unknown>) => Promise<CreateResult>;
91
+ /**
92
+ * Optional override invoked instead of the default PUT when `recordId`
93
+ * is provided.
94
+ */
95
+ onUpdate?: (recordId: string, data: Record<string, unknown>) => Promise<CreateResult>;
96
+ /** Default values seeded into the form on create. */
97
+ defaults?: Record<string, unknown>;
98
+ /** Notification when a create or update succeeds. */
99
+ onSaved?: () => void;
100
+ }
101
+ /**
102
+ * Props for the read-only viewer dialog.
103
+ */
104
+ export interface ViewRecordDialogProps extends RecordDialogProps {
105
+ /** Identifier of the record to display. */
106
+ recordId: string;
107
+ /** Optional handler triggered by the "Edit" affordance. */
108
+ onEdit?: () => void;
109
+ /**
110
+ * Optional handler triggered by the "Delete" affordance. When omitted the
111
+ * delete button is hidden. The dialog awaits the promise before closing.
112
+ */
113
+ onDelete?: () => Promise<void>;
114
+ }
115
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/dialogs/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAA;AAE7B;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,KAAK,CAAC;QACX,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,YAAY,CAAC,EAAE,OAAO,CAAA;QACtB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,MAAM,CAAC,EAAE,OAAO,CAAA;QAEhB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAC3B,CAAC,CAAA;CACL;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAA;AAEhD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAC9B,uEAAuE;IACvE,QAAQ,EAAE,QAAQ,CAAA;IAClB,6BAA6B;IAC7B,IAAI,EAAE,OAAO,CAAA;IACb,6BAA6B;IAC7B,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;CACvB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAwB,SAAQ,iBAAiB;IAC9D,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;IACnE;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;IACrF,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC5D,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAA;IAChB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACjC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Shared types for record-oriented dialogs in `@asteby/metacore-runtime-react`.
3
+ *
4
+ * These dialogs are intentionally generic: any addon that drives a metadata-
5
+ * backed CRUD surface can render them by passing a `modelKey`. The dialogs
6
+ * resolve field shape, labels, and validation hints from the metadata server
7
+ * (default: `/metadata/modal/:modelKey`) via the configured `useApi()`
8
+ * transport. Hosts may override transport behaviour by supplying explicit
9
+ * `endpoint` overrides and/or `onCreate` / `onDelete` callbacks.
10
+ *
11
+ * Wave 2.5 cleanup — replaces the product-specific dialogs that used to live
12
+ * in `ops/frontend/src/components/dynamic/` for any record kind that does not
13
+ * need pricing rules, media galleries, or category-driven custom fields.
14
+ */
15
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { ViewRecordDialogProps } from './types';
2
+ export declare function ViewRecordDialog({ modelKey, open, onOpenChange, recordId, endpoint, schema, onEdit, onDelete, }: ViewRecordDialogProps): import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=view-record-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-record-dialog.d.ts","sourceRoot":"","sources":["../../src/dialogs/view-record-dialog.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAEpD,wBAAgB,gBAAgB,CAAC,EAC7B,QAAQ,EACR,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,MAAM,EACN,QAAQ,GACX,EAAE,qBAAqB,2CAcvB"}
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * ViewRecordDialog — read-only record viewer with optional edit/delete
4
+ * affordances.
5
+ *
6
+ * Thin wrapper around `DynamicRecordDialog` (mode = `'view'`) that exposes a
7
+ * narrower, intent-specific API matching the Wave 2.5 cleanup spec.
8
+ * The "Edit" and "Delete" footer buttons are only rendered when the host
9
+ * supplies `onEdit` / `onDelete`, so the dialog gracefully degrades to a
10
+ * pure viewer when those affordances are not needed.
11
+ */
12
+ import { DynamicRecordDialog } from './dynamic-record';
13
+ export function ViewRecordDialog({ modelKey, open, onOpenChange, recordId, endpoint, schema, onEdit, onDelete, }) {
14
+ return (_jsx(DynamicRecordDialog, { open: open, onOpenChange: onOpenChange, mode: "view", model: modelKey, recordId: recordId, endpoint: endpoint, schema: schema, onEdit: onEdit, onDelete: onDelete }));
15
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-crud-page.d.ts","sourceRoot":"","sources":["../src/dynamic-crud-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAKN,MAAM,OAAO,CAAA;AAWd,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;mDAC+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AASD,MAAM,WAAW,sBAAsB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACjC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,sDAAsD;IACtD,OAAO,CAAC,EAAE,sBAAsB,CAAA;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAsL1D"}
1
+ {"version":3,"file":"dynamic-crud-page.d.ts","sourceRoot":"","sources":["../src/dynamic-crud-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAKN,MAAM,OAAO,CAAA;AAYd,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;mDAC+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AASD,MAAM,WAAW,sBAAsB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACjC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,sDAAsD;IACtD,OAAO,CAAC,EAAE,sBAAsB,CAAA;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CA+L1D"}
@@ -32,6 +32,7 @@ import { DynamicRecordDialog } from './dialogs/dynamic-record';
32
32
  import { ExportDialog } from './dialogs/export';
33
33
  import { ImportDialog } from './dialogs/import';
34
34
  import { getModelExtension } from './model-extension-registry';
35
+ import { ModelActionToolbar } from './model-action-toolbar';
35
36
  const defaultStrings = {
36
37
  refresh: 'Refresh',
37
38
  export: 'Export',
@@ -84,7 +85,10 @@ export function DynamicCRUDPage(props) {
84
85
  return t.charAt(0).toUpperCase() + t.slice(1);
85
86
  }, [title]);
86
87
  const enableCRUD = metadata?.enableCRUDActions ?? false;
87
- const effectiveHideCreate = hideCreate || ext?.hideCreate;
88
+ // A "create"-placement action ships a custom create experience — it
89
+ // replaces the generic create button (the ModelActionToolbar renders it).
90
+ const hasCreateAction = metadata?.actions?.some((a) => a.placement === 'create') ?? false;
91
+ const effectiveHideCreate = hideCreate || ext?.hideCreate || hasCreateAction;
88
92
  const effectiveHideExport = hideExport || ext?.hideExport;
89
93
  const effectiveHideImport = hideImport || ext?.hideImport;
90
94
  // Refresh defaults to hidden in the page header — <DynamicTable> ships
@@ -106,5 +110,5 @@ export function DynamicCRUDPage(props) {
106
110
  const titleCls = classes?.title ?? 'text-2xl font-bold tracking-tight';
107
111
  const toolbarCls = classes?.toolbar ?? 'flex items-center gap-2';
108
112
  const tableWrapperCls = classes?.tableWrapper ?? 'flex-1 min-h-0';
109
- return (_jsxs("div", { className: rootCls, children: [_jsxs("div", { className: containerCls, children: [ext?.headerExtras && _jsx(ext.headerExtras, { model: model, onRefresh: handleRefresh }), headerExtras, _jsxs("div", { className: headerCls, children: [metadata ? (_jsx("h1", { className: titleCls, children: title })) : (_jsx("div", { className: 'h-8 w-48 bg-muted rounded animate-pulse' })), _jsxs("div", { className: toolbarCls, children: [showRefresh && (_jsx("button", { type: 'button', onClick: handleRefresh, "aria-label": strings.refresh, className: 'inline-flex items-center justify-center size-9 rounded-md border border-border bg-background hover:bg-accent text-foreground', children: _jsx(RefreshCw, { className: 'size-4' }) })), metadata && showExport && (_jsxs("button", { type: 'button', onClick: () => setOpenExport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Download, { className: 'size-4' }), strings.export] })), metadata && showImport && (_jsxs("button", { type: 'button', onClick: () => setOpenImport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Upload, { className: 'size-4' }), strings.import] })), ext?.toolbarExtras && _jsx(ext.toolbarExtras, { model: model, onRefresh: handleRefresh }), toolbarExtras, showCreate && (_jsxs("button", { type: 'button', onClick: () => setOpenCreate(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md bg-primary text-primary-foreground hover:opacity-90 text-sm font-medium', children: [_jsx(Plus, { className: 'size-4' }), resolvedNewLabel ?? `${strings.newPrefix} ${singular}`] }))] })] }), _jsx("div", { className: tableWrapperCls, children: _jsx(DynamicTable, { model: model, endpoint: dataEndpoint, refreshTrigger: refreshKey }, model) })] }), showCreate && (_jsx(DynamicRecordDialog, { open: openCreate, onOpenChange: setOpenCreate, mode: 'create', model: model, endpoint: dataEndpoint, onSaved: handleRefresh })), metadata && showExport && (_jsx(ExportDialog, { open: openExport, onOpenChange: setOpenExport, model: model, metadata: metadata })), metadata && showImport && (_jsx(ImportDialog, { open: openImport, onOpenChange: setOpenImport, model: model, metadata: metadata, onImported: handleRefresh }))] }));
113
+ return (_jsxs("div", { className: rootCls, children: [_jsxs("div", { className: containerCls, children: [ext?.headerExtras && _jsx(ext.headerExtras, { model: model, onRefresh: handleRefresh }), headerExtras, _jsxs("div", { className: headerCls, children: [metadata ? (_jsx("h1", { className: titleCls, children: title })) : (_jsx("div", { className: 'h-8 w-48 bg-muted rounded animate-pulse' })), _jsxs("div", { className: toolbarCls, children: [showRefresh && (_jsx("button", { type: 'button', onClick: handleRefresh, "aria-label": strings.refresh, className: 'inline-flex items-center justify-center size-9 rounded-md border border-border bg-background hover:bg-accent text-foreground', children: _jsx(RefreshCw, { className: 'size-4' }) })), metadata && showExport && (_jsxs("button", { type: 'button', onClick: () => setOpenExport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Download, { className: 'size-4' }), strings.export] })), metadata && showImport && (_jsxs("button", { type: 'button', onClick: () => setOpenImport(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground', children: [_jsx(Upload, { className: 'size-4' }), strings.import] })), ext?.toolbarExtras && _jsx(ext.toolbarExtras, { model: model, onRefresh: handleRefresh }), toolbarExtras, _jsx(ModelActionToolbar, { model: model, endpoint: dataEndpoint, actions: metadata?.actions, onChange: handleRefresh }), showCreate && (_jsxs("button", { type: 'button', onClick: () => setOpenCreate(true), className: 'inline-flex items-center gap-2 h-9 px-3 rounded-md bg-primary text-primary-foreground hover:opacity-90 text-sm font-medium', children: [_jsx(Plus, { className: 'size-4' }), resolvedNewLabel ?? `${strings.newPrefix} ${singular}`] }))] })] }), _jsx("div", { className: tableWrapperCls, children: _jsx(DynamicTable, { model: model, endpoint: dataEndpoint, refreshTrigger: refreshKey }, model) })] }), showCreate && (_jsx(DynamicRecordDialog, { open: openCreate, onOpenChange: setOpenCreate, mode: 'create', model: model, endpoint: dataEndpoint, onSaved: handleRefresh })), metadata && showExport && (_jsx(ExportDialog, { open: openExport, onOpenChange: setOpenExport, model: model, metadata: metadata })), metadata && showImport && (_jsx(ImportDialog, { open: openImport, onOpenChange: setOpenImport, model: model, metadata: metadata, onImported: handleRefresh }))] }));
110
114
  }
@@ -8,5 +8,14 @@ export declare function registerValidator(slug: string, fn: (s: z.ZodString) =>
8
8
  export declare function buildZodSchema(fields: ActionFieldDef[]): z.ZodObject<{
9
9
  [x: string]: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
10
10
  }, z.core.$strip>;
11
+ /**
12
+ * Returns the line-items columns of a repeatable-group field, tolerating both
13
+ * the camelCase `itemFields` (the authored SDK shape) and the raw snake_case
14
+ * `item_fields` that the kernel serves in action metadata. Empty when the
15
+ * field is not a line-items group.
16
+ */
17
+ export declare function getItemFields(field: ActionFieldDef): ActionFieldDef[];
18
+ /** A field is a repeatable line-items group when it declares item columns. */
19
+ export declare function isLineItemsField(field: ActionFieldDef): boolean;
11
20
  export declare function resolveWidget(field: ActionFieldDef): string;
12
21
  //# sourceMappingURL=dynamic-form-schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,SAAS,CAAA;AAiB9D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,CAEzF;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;;kBAMtD;AA4CD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAU3D"}
1
+ {"version":3,"file":"dynamic-form-schema.d.ts","sourceRoot":"","sources":["../src/dynamic-form-schema.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAmB,MAAM,KAAK,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,SAAS,CAAA;AAiB9D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,SAAS,GAAG,IAAI,CAEzF;AAcD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;;kBAMtD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,EAAE,CAGrE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAE/D;AAqDD,wBAAgB,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAU3D"}
@@ -43,7 +43,29 @@ export function buildZodSchema(fields) {
43
43
  }
44
44
  return z.object(shape);
45
45
  }
46
+ /**
47
+ * Returns the line-items columns of a repeatable-group field, tolerating both
48
+ * the camelCase `itemFields` (the authored SDK shape) and the raw snake_case
49
+ * `item_fields` that the kernel serves in action metadata. Empty when the
50
+ * field is not a line-items group.
51
+ */
52
+ export function getItemFields(field) {
53
+ const raw = field.itemFields ?? field.item_fields;
54
+ return Array.isArray(raw) ? raw : [];
55
+ }
56
+ /** A field is a repeatable line-items group when it declares item columns. */
57
+ export function isLineItemsField(field) {
58
+ return getItemFields(field).length > 0;
59
+ }
46
60
  function fieldToZod(field) {
61
+ // Repeatable line-items group → array of row objects, each row built from
62
+ // the item field columns. Required keeps at least one row.
63
+ const itemFields = getItemFields(field);
64
+ if (itemFields.length > 0) {
65
+ const row = buildZodSchema(itemFields);
66
+ const arr = z.array(row);
67
+ return field.required ? arr.min(1, `${field.label} requiere al menos un renglón`) : arr;
68
+ }
47
69
  const v = field.validation ?? {};
48
70
  const isNumeric = field.type === 'number';
49
71
  const isBool = field.type === 'boolean';
@@ -1,6 +1,7 @@
1
1
  import type { ActionFieldDef } from './types';
2
2
  import { buildZodSchema, resolveWidget } from './dynamic-form-schema';
3
3
  export { buildZodSchema, resolveWidget };
4
+ export { DynamicLineItems } from './dynamic-line-items';
4
5
  export interface DynamicFormProps {
5
6
  fields: ActionFieldDef[];
6
7
  initialValues?: Record<string, any>;
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAGrE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AAExC,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,2CAgElB"}
1
+ {"version":3,"file":"dynamic-form.d.ts","sourceRoot":"","sources":["../src/dynamic-form.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,aAAa,EAAoB,MAAM,uBAAuB,CAAA;AAIvF,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAEvD,MAAM,WAAW,gBAAgB;IAC7B,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,WAAW,CAAC,EACxB,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,WAAuB,EACvB,WAAwB,EACxB,QAAgB,GACnB,EAAE,gBAAgB,2CAoElB"}