@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.
- package/CHANGELOG.md +74 -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-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +6 -2
- 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/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +7 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/model-action-toolbar.d.ts +27 -0
- package/dist/model-action-toolbar.d.ts.map +1 -0
- package/dist/model-action-toolbar.js +88 -0
- package/dist/types.d.ts +18 -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-crud-page.tsx +11 -1
- 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/dynamic-table.tsx +7 -1
- package/src/index.ts +16 -0
- package/src/model-action-toolbar.tsx +154 -0
- 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":"
|
|
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
|
|
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":"
|
|
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
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
90
|
-
initial[field.key] =
|
|
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
|
|
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;
|
|
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
|
-
|
|
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;
|
|
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';
|
package/dist/dynamic-form.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|