@asteby/metacore-runtime-react 7.0.0 → 7.1.1
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 +21 -0
- package/dist/dialogs/dynamic-record.js +3 -3
- package/dist/dialogs/export.js +1 -1
- package/dist/dialogs/import.js +3 -3
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +35 -2
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +5 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +15 -3
- package/package.json +1 -1
- package/src/dialogs/dynamic-record.tsx +3 -3
- package/src/dialogs/export.tsx +1 -1
- package/src/dialogs/import.tsx +3 -3
- package/src/dynamic-columns.tsx +37 -2
- package/src/dynamic-crud-page.tsx +5 -1
- package/src/dynamic-table.tsx +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 7.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 76d4b58: Align SDK dialogs to the kernel's `/dynamic/:model` path namespace.
|
|
8
|
+
|
|
9
|
+
`<DynamicRecordDialog>`, `<ExportDialog>` and `<ImportDialog>` were posting to `/data/:model[/...]`, which had no kernel handler — apps that didn't ship their own `handlers/export.go` got 404s the moment a user clicked Export, Import or "Descargar plantilla".
|
|
10
|
+
|
|
11
|
+
Switches every hardcoded path from `/data/${model}` to `/dynamic/${model}`. Pairs with `metacore-kernel v0.5.0`, which exposes the canonical `/dynamic/:model/export`, `/dynamic/:model/export/template`, `/dynamic/:model/import`, `/dynamic/:model/import/validate` endpoints — so the SDK toolbar now wires straight to the kernel out of the box, no app glue.
|
|
12
|
+
|
|
13
|
+
## 7.1.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 0cd085c: Zero-config CRUD UX from a single column flag.
|
|
18
|
+
|
|
19
|
+
Three changes that move polish from each app's metadata into the SDK default behaviour, so a model only needs `enableCRUDActions: true` plus `filterable: true` on the columns it wants searchable to get the same UX link / ops / hub render today:
|
|
20
|
+
1. **Auto-derive filter chip type from column type.** A column flagged `filterable: true` without options or a `searchEndpoint` no longer falls back to "no filter" — it picks the FilterableColumnHeader variant that matches the column type: `text` for text/email/phone/tags, `number_range` for numeric columns, `boolean` for booleans, `select` when options/endpoint are present.
|
|
21
|
+
2. **Auto-render the row Actions column when `enableCRUDActions` is on.** If the host metadata already declares its own `actions[]`, those win. When it doesn't, the SDK falls back to the canonical View / Edit / Delete trio wired to DynamicTable's existing `view` / `edit` / `delete` handlers — no host-side glue.
|
|
22
|
+
3. **`<DynamicCRUDPage>` defaults `hideRefresh` to `true`.** The page-level Refresh button duplicated the one DynamicTable's internal toolbar already ships next to "View"; the page chrome now defers to it. Apps that want both back can pass `hideRefresh={false}`.
|
|
23
|
+
|
|
3
24
|
## 7.0.0
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
|
@@ -94,7 +94,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
94
94
|
else {
|
|
95
95
|
const recordEndpoint = endpoint
|
|
96
96
|
? `${endpoint}/${recordId}`
|
|
97
|
-
: `/
|
|
97
|
+
: `/dynamic/${model}/${recordId}`;
|
|
98
98
|
const recRes = await api.get(recordEndpoint);
|
|
99
99
|
if (cancelled)
|
|
100
100
|
return;
|
|
@@ -142,13 +142,13 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
142
142
|
try {
|
|
143
143
|
let res;
|
|
144
144
|
if (isCreate) {
|
|
145
|
-
const createEndpoint = endpoint || `/
|
|
145
|
+
const createEndpoint = endpoint || `/dynamic/${model}`;
|
|
146
146
|
res = await api.post(createEndpoint, formValues);
|
|
147
147
|
}
|
|
148
148
|
else {
|
|
149
149
|
const updateEndpoint = endpoint
|
|
150
150
|
? `${endpoint}/${recordId}`
|
|
151
|
-
: `/
|
|
151
|
+
: `/dynamic/${model}/${recordId}`;
|
|
152
152
|
res = await api.put(updateEndpoint, formValues);
|
|
153
153
|
}
|
|
154
154
|
if (res.data?.success !== false) {
|
package/dist/dialogs/export.js
CHANGED
|
@@ -110,7 +110,7 @@ export function ExportDialog({ open, onOpenChange, model, metadata, currentFilte
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
|
-
const response = await api.get(`/
|
|
113
|
+
const response = await api.get(`/dynamic/${model}/export`, {
|
|
114
114
|
params,
|
|
115
115
|
responseType: 'blob',
|
|
116
116
|
validateStatus: () => true,
|
package/dist/dialogs/import.js
CHANGED
|
@@ -36,7 +36,7 @@ export function ImportDialog({ open, onOpenChange, model, metadata, onImported,
|
|
|
36
36
|
};
|
|
37
37
|
const handleDownloadTemplate = async () => {
|
|
38
38
|
try {
|
|
39
|
-
const response = await api.get(`/
|
|
39
|
+
const response = await api.get(`/dynamic/${model}/export/template`, {
|
|
40
40
|
responseType: 'blob',
|
|
41
41
|
});
|
|
42
42
|
const url = window.URL.createObjectURL(response.data);
|
|
@@ -61,7 +61,7 @@ export function ImportDialog({ open, onOpenChange, model, metadata, onImported,
|
|
|
61
61
|
try {
|
|
62
62
|
const formData = new FormData();
|
|
63
63
|
formData.append('file', file);
|
|
64
|
-
const res = await api.post(`/
|
|
64
|
+
const res = await api.post(`/dynamic/${model}/import/validate`, formData, {
|
|
65
65
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
66
66
|
});
|
|
67
67
|
const data = res.data?.data ?? res.data;
|
|
@@ -87,7 +87,7 @@ export function ImportDialog({ open, onOpenChange, model, metadata, onImported,
|
|
|
87
87
|
try {
|
|
88
88
|
const formData = new FormData();
|
|
89
89
|
formData.append('file', file);
|
|
90
|
-
const res = await api.post(`/
|
|
90
|
+
const res = await api.post(`/dynamic/${model}/import`, formData, {
|
|
91
91
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
92
92
|
onUploadProgress: (progressEvent) => {
|
|
93
93
|
if (progressEvent.total) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAwHD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAwHD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAiXnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
package/dist/dynamic-columns.js
CHANGED
|
@@ -266,14 +266,47 @@ export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
|
266
266
|
enableHiding: true,
|
|
267
267
|
});
|
|
268
268
|
});
|
|
269
|
-
|
|
269
|
+
// Resolve which actions to surface in the row dropdown:
|
|
270
|
+
// 1. If the host metadata declares its own actions, use them as-is.
|
|
271
|
+
// 2. Otherwise, when enableCRUDActions is true, fall back to the
|
|
272
|
+
// canonical View / Edit / Delete trio so any model with CRUD on
|
|
273
|
+
// gets the same dropdown without the host having to declare it.
|
|
274
|
+
// The DynamicTable wires `view`/`edit`/`delete` to its own dialogs
|
|
275
|
+
// through onAction, so labels/icons are the only thing this needs to
|
|
276
|
+
// ship.
|
|
277
|
+
const explicitActions = metadata.actions ?? [];
|
|
278
|
+
const hasExplicitActions = (metadata.hasActions ?? explicitActions.length > 0) && explicitActions.length > 0;
|
|
279
|
+
const defaultCRUDActions = metadata.enableCRUDActions
|
|
280
|
+
? [
|
|
281
|
+
{
|
|
282
|
+
key: 'view',
|
|
283
|
+
name: 'view',
|
|
284
|
+
label: t ? t('datatable.view_record') : 'Ver',
|
|
285
|
+
icon: 'Eye',
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
key: 'edit',
|
|
289
|
+
name: 'edit',
|
|
290
|
+
label: t ? t('datatable.edit') : 'Editar',
|
|
291
|
+
icon: 'Pencil',
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
key: 'delete',
|
|
295
|
+
name: 'delete',
|
|
296
|
+
label: t ? t('datatable.delete') : 'Eliminar',
|
|
297
|
+
icon: 'Trash2',
|
|
298
|
+
},
|
|
299
|
+
]
|
|
300
|
+
: [];
|
|
301
|
+
const resolvedActions = hasExplicitActions ? explicitActions : defaultCRUDActions;
|
|
302
|
+
if (resolvedActions.length > 0) {
|
|
270
303
|
columns.push({
|
|
271
304
|
id: 'actions',
|
|
272
305
|
header: () => _jsx("div", { className: "text-right", children: t ? t('common.actions') : 'Acciones' }),
|
|
273
306
|
size: 80,
|
|
274
307
|
maxSize: 80,
|
|
275
308
|
meta: {},
|
|
276
|
-
cell: ({ row }) => (_jsx("div", { className: "flex items-center justify-end", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-8 w-8 p-0", children: [_jsx("span", { className: "sr-only", children: "Abrir men\u00FA" }), _jsx(MoreHorizontal, { className: "h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { align: "end", children:
|
|
309
|
+
cell: ({ row }) => (_jsx("div", { className: "flex items-center justify-end", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-8 w-8 p-0", children: [_jsx("span", { className: "sr-only", children: "Abrir men\u00FA" }), _jsx(MoreHorizontal, { className: "h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { align: "end", children: resolvedActions
|
|
277
310
|
.filter((action) => {
|
|
278
311
|
if (!action.condition)
|
|
279
312
|
return true;
|
|
@@ -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,
|
|
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"}
|
|
@@ -87,7 +87,11 @@ export function DynamicCRUDPage(props) {
|
|
|
87
87
|
const effectiveHideCreate = hideCreate || ext?.hideCreate;
|
|
88
88
|
const effectiveHideExport = hideExport || ext?.hideExport;
|
|
89
89
|
const effectiveHideImport = hideImport || ext?.hideImport;
|
|
90
|
-
|
|
90
|
+
// Refresh defaults to hidden in the page header — <DynamicTable> ships
|
|
91
|
+
// its own refresh icon next to the View / column-visibility toolbar, so
|
|
92
|
+
// showing one in the page chrome is just visual duplication. Apps that
|
|
93
|
+
// want it back can pass `hideRefresh={false}`.
|
|
94
|
+
const effectiveHideRefresh = hideRefresh ?? ext?.hideRefresh ?? true;
|
|
91
95
|
const showCreate = enableCRUD && !effectiveHideCreate;
|
|
92
96
|
const showImport = enableCRUD && !effectiveHideImport;
|
|
93
97
|
const showExport = !effectiveHideExport;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AASnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CACxC;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,GAC/C,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AA+B9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AASnF,UAAU,iBAAiB;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CACxC;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,aAAoB,EACpB,aAAkB,EAClB,QAAQ,EACR,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,GAC/C,EAAE,iBAAiB,2CAsrBnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -469,8 +469,20 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
469
469
|
continue;
|
|
470
470
|
const hasStaticOptions = (c.options?.length ?? 0) > 0;
|
|
471
471
|
const hasEndpoint = !!c.searchEndpoint;
|
|
472
|
-
|
|
473
|
-
|
|
472
|
+
// Pick the filter UI from column type:
|
|
473
|
+
// - explicit options or searchEndpoint → multi-select dropdown
|
|
474
|
+
// - boolean → boolean toggle (renders as select under the hood)
|
|
475
|
+
// - number / number_range / numeric → number range
|
|
476
|
+
// - everything else (text, email, phone, tags…) → text contains
|
|
477
|
+
let filterType = 'select';
|
|
478
|
+
if (hasStaticOptions || hasEndpoint)
|
|
479
|
+
filterType = 'select';
|
|
480
|
+
else if (c.type === 'boolean')
|
|
481
|
+
filterType = 'boolean';
|
|
482
|
+
else if (c.type === 'number')
|
|
483
|
+
filterType = 'number_range';
|
|
484
|
+
else
|
|
485
|
+
filterType = 'text';
|
|
474
486
|
const options = hasStaticOptions
|
|
475
487
|
? c.options.map(o => ({
|
|
476
488
|
label: o.label,
|
|
@@ -482,7 +494,7 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
|
|
|
482
494
|
? filterOptionsMap.get(c.searchEndpoint) || []
|
|
483
495
|
: [];
|
|
484
496
|
map.set(c.key, {
|
|
485
|
-
filterType
|
|
497
|
+
filterType,
|
|
486
498
|
filterKey: c.key,
|
|
487
499
|
options,
|
|
488
500
|
selectedValues: dynamicFilters[c.key] || [],
|
package/package.json
CHANGED
|
@@ -172,7 +172,7 @@ export function DynamicRecordDialog({
|
|
|
172
172
|
} else {
|
|
173
173
|
const recordEndpoint = endpoint
|
|
174
174
|
? `${endpoint}/${recordId}`
|
|
175
|
-
: `/
|
|
175
|
+
: `/dynamic/${model}/${recordId}`
|
|
176
176
|
|
|
177
177
|
const recRes = await api.get(recordEndpoint)
|
|
178
178
|
if (cancelled) return
|
|
@@ -223,12 +223,12 @@ export function DynamicRecordDialog({
|
|
|
223
223
|
try {
|
|
224
224
|
let res
|
|
225
225
|
if (isCreate) {
|
|
226
|
-
const createEndpoint = endpoint || `/
|
|
226
|
+
const createEndpoint = endpoint || `/dynamic/${model}`
|
|
227
227
|
res = await api.post(createEndpoint, formValues)
|
|
228
228
|
} else {
|
|
229
229
|
const updateEndpoint = endpoint
|
|
230
230
|
? `${endpoint}/${recordId}`
|
|
231
|
-
: `/
|
|
231
|
+
: `/dynamic/${model}/${recordId}`
|
|
232
232
|
res = await api.put(updateEndpoint, formValues)
|
|
233
233
|
}
|
|
234
234
|
|
package/src/dialogs/export.tsx
CHANGED
package/src/dialogs/import.tsx
CHANGED
|
@@ -87,7 +87,7 @@ export function ImportDialog({
|
|
|
87
87
|
|
|
88
88
|
const handleDownloadTemplate = async () => {
|
|
89
89
|
try {
|
|
90
|
-
const response = await api.get(`/
|
|
90
|
+
const response = await api.get(`/dynamic/${model}/export/template`, {
|
|
91
91
|
responseType: 'blob',
|
|
92
92
|
})
|
|
93
93
|
const url = window.URL.createObjectURL(response.data)
|
|
@@ -114,7 +114,7 @@ export function ImportDialog({
|
|
|
114
114
|
const formData = new FormData()
|
|
115
115
|
formData.append('file', file)
|
|
116
116
|
|
|
117
|
-
const res = await api.post(`/
|
|
117
|
+
const res = await api.post(`/dynamic/${model}/import/validate`, formData, {
|
|
118
118
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
119
119
|
})
|
|
120
120
|
|
|
@@ -143,7 +143,7 @@ export function ImportDialog({
|
|
|
143
143
|
const formData = new FormData()
|
|
144
144
|
formData.append('file', file)
|
|
145
145
|
|
|
146
|
-
const res = await api.post(`/
|
|
146
|
+
const res = await api.post(`/dynamic/${model}/import`, formData, {
|
|
147
147
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
148
148
|
onUploadProgress: (progressEvent: { loaded: number; total?: number }) => {
|
|
149
149
|
if (progressEvent.total) {
|
package/src/dynamic-columns.tsx
CHANGED
|
@@ -461,7 +461,42 @@ export function makeDefaultGetDynamicColumns(
|
|
|
461
461
|
})
|
|
462
462
|
})
|
|
463
463
|
|
|
464
|
-
|
|
464
|
+
// Resolve which actions to surface in the row dropdown:
|
|
465
|
+
// 1. If the host metadata declares its own actions, use them as-is.
|
|
466
|
+
// 2. Otherwise, when enableCRUDActions is true, fall back to the
|
|
467
|
+
// canonical View / Edit / Delete trio so any model with CRUD on
|
|
468
|
+
// gets the same dropdown without the host having to declare it.
|
|
469
|
+
// The DynamicTable wires `view`/`edit`/`delete` to its own dialogs
|
|
470
|
+
// through onAction, so labels/icons are the only thing this needs to
|
|
471
|
+
// ship.
|
|
472
|
+
const explicitActions = metadata.actions ?? []
|
|
473
|
+
const hasExplicitActions = (metadata.hasActions ?? explicitActions.length > 0) && explicitActions.length > 0
|
|
474
|
+
const defaultCRUDActions: typeof explicitActions =
|
|
475
|
+
metadata.enableCRUDActions
|
|
476
|
+
? [
|
|
477
|
+
{
|
|
478
|
+
key: 'view',
|
|
479
|
+
name: 'view',
|
|
480
|
+
label: t ? t('datatable.view_record') : 'Ver',
|
|
481
|
+
icon: 'Eye',
|
|
482
|
+
} as any,
|
|
483
|
+
{
|
|
484
|
+
key: 'edit',
|
|
485
|
+
name: 'edit',
|
|
486
|
+
label: t ? t('datatable.edit') : 'Editar',
|
|
487
|
+
icon: 'Pencil',
|
|
488
|
+
} as any,
|
|
489
|
+
{
|
|
490
|
+
key: 'delete',
|
|
491
|
+
name: 'delete',
|
|
492
|
+
label: t ? t('datatable.delete') : 'Eliminar',
|
|
493
|
+
icon: 'Trash2',
|
|
494
|
+
} as any,
|
|
495
|
+
]
|
|
496
|
+
: []
|
|
497
|
+
const resolvedActions = hasExplicitActions ? explicitActions : defaultCRUDActions
|
|
498
|
+
|
|
499
|
+
if (resolvedActions.length > 0) {
|
|
465
500
|
columns.push({
|
|
466
501
|
id: 'actions',
|
|
467
502
|
header: () => <div className="text-right">{t ? t('common.actions') : 'Acciones'}</div>,
|
|
@@ -478,7 +513,7 @@ export function makeDefaultGetDynamicColumns(
|
|
|
478
513
|
</Button>
|
|
479
514
|
</DropdownMenuTrigger>
|
|
480
515
|
<DropdownMenuContent align="end">
|
|
481
|
-
{
|
|
516
|
+
{resolvedActions
|
|
482
517
|
.filter((action) => {
|
|
483
518
|
if (!action.condition) return true
|
|
484
519
|
const { field, operator, value } = action.condition
|
|
@@ -156,7 +156,11 @@ export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
|
156
156
|
const effectiveHideCreate = hideCreate || ext?.hideCreate
|
|
157
157
|
const effectiveHideExport = hideExport || ext?.hideExport
|
|
158
158
|
const effectiveHideImport = hideImport || ext?.hideImport
|
|
159
|
-
|
|
159
|
+
// Refresh defaults to hidden in the page header — <DynamicTable> ships
|
|
160
|
+
// its own refresh icon next to the View / column-visibility toolbar, so
|
|
161
|
+
// showing one in the page chrome is just visual duplication. Apps that
|
|
162
|
+
// want it back can pass `hideRefresh={false}`.
|
|
163
|
+
const effectiveHideRefresh = hideRefresh ?? ext?.hideRefresh ?? true
|
|
160
164
|
const showCreate = enableCRUD && !effectiveHideCreate
|
|
161
165
|
const showImport = enableCRUD && !effectiveHideImport
|
|
162
166
|
const showExport = !effectiveHideExport
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -507,7 +507,17 @@ export function DynamicTable({
|
|
|
507
507
|
if (!c.filterable || map.has(c.key)) continue
|
|
508
508
|
const hasStaticOptions = (c.options?.length ?? 0) > 0
|
|
509
509
|
const hasEndpoint = !!c.searchEndpoint
|
|
510
|
-
|
|
510
|
+
// Pick the filter UI from column type:
|
|
511
|
+
// - explicit options or searchEndpoint → multi-select dropdown
|
|
512
|
+
// - boolean → boolean toggle (renders as select under the hood)
|
|
513
|
+
// - number / number_range / numeric → number range
|
|
514
|
+
// - everything else (text, email, phone, tags…) → text contains
|
|
515
|
+
let filterType: ColumnFilterConfig['filterType'] = 'select'
|
|
516
|
+
if (hasStaticOptions || hasEndpoint) filterType = 'select'
|
|
517
|
+
else if (c.type === 'boolean') filterType = 'boolean'
|
|
518
|
+
else if (c.type === 'number') filterType = 'number_range'
|
|
519
|
+
else filterType = 'text'
|
|
520
|
+
|
|
511
521
|
const options = hasStaticOptions
|
|
512
522
|
? c.options!.map(o => ({
|
|
513
523
|
label: o.label,
|
|
@@ -519,7 +529,7 @@ export function DynamicTable({
|
|
|
519
529
|
? filterOptionsMap.get(c.searchEndpoint!) || []
|
|
520
530
|
: []
|
|
521
531
|
map.set(c.key, {
|
|
522
|
-
filterType
|
|
532
|
+
filterType,
|
|
523
533
|
filterKey: c.key,
|
|
524
534
|
options,
|
|
525
535
|
selectedValues: dynamicFilters[c.key] || [],
|