@asteby/metacore-runtime-react 13.6.0 → 13.8.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 13.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ab80937: feat(dynamic-select): inline "+" to create the referenced record. A dynamic_select with a `ref` now shows a "+" button that opens the referenced model's OWN create modal (via a decoupled `metacore:create-record` window event the host handles) and auto-selects the newly created record. Lets users add a missing Category/Brand/etc. without leaving the form.
8
+
9
+ ## 13.7.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 33165d2: feat(dynamic-table): card-per-row layout on mobile. On phones the multi-column data table forced a wide horizontal scroll; below the `sm` breakpoint the table is now replaced by a stacked card list (one card per row, columns shown as label : value pairs, row actions pinned at the bottom). Desktop keeps the classic table.
14
+
3
15
  ## 13.6.0
4
16
 
5
17
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAW7C,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,2CA0GrF;AAED,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAW7C,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,2CAgJrF;AAED,eAAe,kBAAkB,CAAA"}
@@ -24,7 +24,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
24
24
  // case — start empty and never hit this.
25
25
  import { useEffect, useState } from 'react';
26
26
  import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives';
27
- import { Check, ChevronsUpDown, Loader2 } from 'lucide-react';
27
+ import { Check, ChevronsUpDown, Loader2, Plus } from 'lucide-react';
28
28
  import { useOptionsResolver } from './use-options-resolver';
29
29
  function useDebounced(value, ms) {
30
30
  const [debounced, setDebounced] = useState(value);
@@ -63,12 +63,33 @@ export function DynamicSelectField({ field, value, onChange }) {
63
63
  setOpen(false);
64
64
  setSearch('');
65
65
  };
66
- return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "w-full justify-between font-normal", "data-empty": !value, children: [_jsx("span", { className: 'truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || field.placeholder || 'Buscar…' }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
67
- // Match the trigger width without an arbitrary Tailwind class
68
- // (those don't always survive a consuming app's Tailwind scan).
69
- style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: field.placeholder || 'Buscar…', value: search, onValueChange: setSearch }), _jsxs(CommandList, { children: [loading && (_jsxs("div", { className: "text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm", children: [_jsx(Loader2, { className: "size-4 animate-spin" }), "Buscando\u2026"] })), !loading && options.length === 0 && (_jsx(CommandEmpty, { children: debounced ? 'Sin resultados' : 'Escribí para buscar…' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
70
- const isSel = String(opt.id) === String(value);
71
- return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 ' + (isSel ? 'opacity-100' : 'opacity-0') }), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] })] }, String(opt.id)));
72
- }) }))] })] }) })] }));
66
+ // Inline-create: the "+" opens the REFERENCED model's own create modal (the
67
+ // real one the host renders for that model full fields, not a duplicate),
68
+ // via a decoupled window event the host listens for. On success the host
69
+ // hands back the new record and we select it immediately. No host import
70
+ // no circular dependency; works for ANY dynamic_select with a `ref`.
71
+ const openCreate = () => {
72
+ if (!field.ref || typeof window === 'undefined')
73
+ return;
74
+ window.dispatchEvent(new CustomEvent('metacore:create-record', {
75
+ detail: {
76
+ model: field.ref,
77
+ onCreated: (rec) => {
78
+ if (rec && rec.id != null) {
79
+ const id = String(rec.id);
80
+ const label = String(rec.name ?? rec.label ?? rec.title ?? rec.id);
81
+ handlePick({ id, value: id, label, name: label });
82
+ }
83
+ },
84
+ },
85
+ }));
86
+ };
87
+ return (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "w-full justify-between font-normal", "data-empty": !value, children: [_jsx("span", { className: 'truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || field.placeholder || 'Buscar…' }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
88
+ // Match the trigger width without an arbitrary Tailwind class
89
+ // (those don't always survive a consuming app's Tailwind scan).
90
+ style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: field.placeholder || 'Buscar…', value: search, onValueChange: setSearch }), _jsxs(CommandList, { children: [loading && (_jsxs("div", { className: "text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm", children: [_jsx(Loader2, { className: "size-4 animate-spin" }), "Buscando\u2026"] })), !loading && options.length === 0 && (_jsx(CommandEmpty, { children: debounced ? 'Sin resultados' : 'Escribí para buscar…' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
91
+ const isSel = String(opt.id) === String(value);
92
+ return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 ' + (isSel ? 'opacity-100' : 'opacity-0') }), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] })] }, String(opt.id)));
93
+ }) }))] })] }) })] }), field.ref && (_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: openCreate, title: `Crear ${field.label ?? field.ref}`, "aria-label": `Crear ${field.label ?? field.ref}`, children: _jsx(Plus, { className: "size-4" }) }))] }));
73
94
  }
74
95
  export default DynamicSelectField;
@@ -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;AAUnF,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,2CAqtBnB"}
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;AAUnF,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,2CAixBnB"}
@@ -574,11 +574,20 @@ export function DynamicTable({ model, endpoint, enableUrlSync = true, hiddenColu
574
574
  if (!metadata) {
575
575
  return _jsx("div", { className: "text-center text-muted-foreground py-8", children: "Error al cargar la configuraci\u00F3n de la tabla." });
576
576
  }
577
- return (_jsxs(OptionsContext.Provider, { value: { optionsMap }, children: [_jsxs("div", { className: 'flex flex-col h-full min-h-0 w-full', children: [_jsx("div", { className: 'pb-4 shrink-0', children: _jsx(DataTableToolbar, { table: table, searchPlaceholder: metadata.searchPlaceholder || 'Buscar...', filters: filters, activeFilters: dynamicFilters, onDynamicFilterChange: handleDynamicFilterChange, dateFilter: { value: dateRange, onChange: setDateRange, placeholder: 'Filtrar por fecha' }, perPageOptions: metadata.perPageOptions, onRefresh: handleRefresh, isLoading: loadingData, selectedCount: Object.keys(rowSelection).length, onBulkDelete: () => setShowBulkDeleteConfirm(true), extraActions: _jsxs(_Fragment, { children: [metadata.canExport && (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => setExportOpen(true), children: [_jsx(Download, { className: "h-4 w-4 mr-1" }), " Exportar"] })), metadata.canImport && (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => setImportOpen(true), children: [_jsx(Upload, { className: "h-4 w-4 mr-1" }), " Importar"] }))] }) }) }), _jsx("div", { className: 'flex-1 min-h-0 overflow-auto border rounded-md bg-card', children: _jsxs(Table, { noWrapper: true, className: "min-w-max w-full", children: [_jsx(TableHeader, { className: 'sticky top-0 z-10', children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { className: 'border-b-0 hover:bg-transparent', children: headerGroup.headers.map((header) => {
577
+ return (_jsxs(OptionsContext.Provider, { value: { optionsMap }, children: [_jsxs("div", { className: 'flex flex-col h-full min-h-0 w-full', children: [_jsx("div", { className: 'pb-4 shrink-0', children: _jsx(DataTableToolbar, { table: table, searchPlaceholder: metadata.searchPlaceholder || 'Buscar...', filters: filters, activeFilters: dynamicFilters, onDynamicFilterChange: handleDynamicFilterChange, dateFilter: { value: dateRange, onChange: setDateRange, placeholder: 'Filtrar por fecha' }, perPageOptions: metadata.perPageOptions, onRefresh: handleRefresh, isLoading: loadingData, selectedCount: Object.keys(rowSelection).length, onBulkDelete: () => setShowBulkDeleteConfirm(true), extraActions: _jsxs(_Fragment, { children: [metadata.canExport && (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => setExportOpen(true), children: [_jsx(Download, { className: "h-4 w-4 mr-1" }), " Exportar"] })), metadata.canImport && (_jsxs(Button, { variant: "outline", size: "sm", className: "h-8", onClick: () => setImportOpen(true), children: [_jsx(Upload, { className: "h-4 w-4 mr-1" }), " Importar"] }))] }) }) }), _jsx("div", { className: 'hidden sm:block flex-1 min-h-0 overflow-auto border rounded-md bg-card', children: _jsxs(Table, { noWrapper: true, className: "min-w-max w-full", children: [_jsx(TableHeader, { className: 'sticky top-0 z-10', children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { className: 'border-b-0 hover:bg-transparent', children: headerGroup.headers.map((header) => {
578
578
  const isActionsColumn = header.id === 'actions';
579
579
  return (_jsx(TableHead, { colSpan: header.colSpan, style: header.column.columnDef.size ? { width: header.column.columnDef.size } : undefined, className: cn('bg-card border-b h-10', isActionsColumn && 'sticky right-0 z-20 bg-card shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]'), children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
580
580
  }) }, headerGroup.id))) }), _jsx(TableBody, { children: loadingData && data.length === 0 ? (_jsx(TableSkeleton, {})) : table.getRowModel().rows?.length ? (table.getRowModel().rows.map((row) => (_jsx(TableRow, { "data-state": row.getIsSelected() && 'selected', children: row.getVisibleCells().map((cell) => {
581
581
  const isActionsColumn = cell.column.id === 'actions';
582
582
  return (_jsx(TableCell, { style: cell.column.columnDef.size ? { width: cell.column.columnDef.size } : undefined, className: cn('py-2', isActionsColumn && 'sticky right-0 bg-card shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]'), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
583
- }) }, row.id)))) : (_jsx(TableRow, { className: 'border-b-0 hover:bg-transparent', children: _jsx(TableCell, { colSpan: columns.length, className: 'h-full p-0', children: _jsxs("div", { className: "flex h-full py-12 flex-col items-center justify-center gap-2 text-muted-foreground", children: [_jsx("div", { className: "flex h-20 w-20 items-center justify-center rounded-full bg-muted/50", children: _jsx(Inbox, { className: "h-10 w-10" }) }), _jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsx("h3", { className: "text-lg font-semibold text-foreground", children: "No se encontraron resultados" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "No hay datos para mostrar en este momento." })] })] }) }) })) })] }) }), _jsx("div", { className: 'shrink-0 pt-4', children: _jsx(DataTablePagination, { table: table, pageSizeOptions: metadata.perPageOptions }) })] }), _jsx(AlertDialog, { open: !!rowToDelete, onOpenChange: (open) => !open && setRowToDelete(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "\u00BFEst\u00E1 absolutamente seguro?" }), _jsx(AlertDialogDescription, { children: "Esta acci\u00F3n no se puede deshacer. Esto eliminar\u00E1 permanentemente el registro seleccionado de nuestros servidores." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: isDeleting, children: t('common.cancel') }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); confirmDelete(); }, className: "bg-red-600 hover:bg-red-700", disabled: isDeleting, children: isDeleting ? 'Eliminando...' : 'Eliminar' })] })] }) }), _jsx(AlertDialog, { open: showBulkDeleteConfirm, onOpenChange: (open) => !open && !isBulkDeleting && setShowBulkDeleteConfirm(false), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: isBulkDeleting ? 'Eliminando registros...' : '¿Eliminar múltiples registros?' }), _jsx(AlertDialogDescription, { children: isBulkDeleting ? (_jsxs("div", { className: "space-y-4 mt-4", children: [_jsx(Progress, { value: (bulkDeleteProgress / bulkDeleteTotal) * 100 }), _jsxs("p", { className: "text-center text-sm", children: ["Procesando ", bulkDeleteProgress, " de ", bulkDeleteTotal, " registros..."] })] })) : (_jsxs(_Fragment, { children: ["Esta acci\u00F3n no se puede deshacer. Se eliminar\u00E1n permanentemente ", _jsx("strong", { children: Object.keys(rowSelection).length }), " registro(s) de nuestros servidores."] })) })] }), !isBulkDeleting && (_jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: t('common.cancel') }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); confirmBulkDelete(); }, className: "bg-red-600 hover:bg-red-700", children: "Eliminar todos" })] }))] }) }), _jsx(DynamicRecordDialog, { open: recordDialog.open, onOpenChange: (open) => setRecordDialog((prev) => ({ ...prev, open })), mode: recordDialog.mode, model: model, recordId: recordDialog.recordId, endpoint: endpoint, onSaved: handleRefresh }), metadata.canExport && (_jsx(ExportDialog, { open: exportOpen, onOpenChange: setExportOpen, model: model, metadata: metadata, currentFilters: buildFilterParams(), hasActiveFilters: hasActiveFilters })), metadata.canImport && (_jsx(ImportDialog, { open: importOpen, onOpenChange: setImportOpen, model: model, metadata: metadata, onImported: handleRefresh })), actionModal.action && (_jsx(ActionModalDispatcher, { open: actionModal.open, onOpenChange: (open) => setActionModal((prev) => ({ ...prev, open })), action: actionModal.action, model: model, record: actionModal.record, endpoint: endpoint, onSuccess: handleRefresh })), _jsx(DataTableBulkActions, { table: table, entityName: "registro", children: _jsxs(Button, { variant: "destructive", size: "sm", className: "h-8", onClick: () => setShowBulkDeleteConfirm(true), children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5" }), " Eliminar"] }) })] }));
583
+ }) }, row.id)))) : (_jsx(TableRow, { className: 'border-b-0 hover:bg-transparent', children: _jsx(TableCell, { colSpan: columns.length, className: 'h-full p-0', children: _jsxs("div", { className: "flex h-full py-12 flex-col items-center justify-center gap-2 text-muted-foreground", children: [_jsx("div", { className: "flex h-20 w-20 items-center justify-center rounded-full bg-muted/50", children: _jsx(Inbox, { className: "h-10 w-10" }) }), _jsxs("div", { className: "flex flex-col items-center gap-1", children: [_jsx("h3", { className: "text-lg font-semibold text-foreground", children: "No se encontraron resultados" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "No hay datos para mostrar en este momento." })] })] }) }) })) })] }) }), _jsx("div", { className: 'flex flex-1 min-h-0 flex-col gap-2 overflow-y-auto sm:hidden', children: loadingData && data.length === 0 ? (Array.from({ length: 5 }).map((_, i) => (_jsxs("div", { className: 'rounded-lg border bg-card p-3', children: [_jsx(Skeleton, { className: 'h-4 w-24' }), _jsx(Skeleton, { className: 'mt-2 h-4 w-40' }), _jsx(Skeleton, { className: 'mt-2 h-4 w-32' })] }, i)))) : table.getRowModel().rows?.length ? (table.getRowModel().rows.map((row) => {
584
+ const cells = row.getVisibleCells();
585
+ const actionsCell = cells.find((c) => c.column.id === 'actions');
586
+ const dataCells = cells.filter((c) => c.column.id !== 'actions' && c.column.id !== 'select');
587
+ return (_jsxs("div", { "data-state": row.getIsSelected() && 'selected', className: 'flex flex-col gap-1.5 rounded-lg border bg-card p-3 data-[state=selected]:border-primary/40', children: [dataCells.map((cell) => {
588
+ const header = cell.column.columnDef.header;
589
+ const label = typeof header === 'string' ? header : cell.column.id;
590
+ return (_jsxs("div", { className: 'flex items-start justify-between gap-3 text-sm', children: [_jsx("span", { className: 'shrink-0 text-muted-foreground', children: label }), _jsx("span", { className: 'min-w-0 break-words text-right font-medium', children: flexRender(cell.column.columnDef.cell, cell.getContext()) })] }, cell.id));
591
+ }), actionsCell && (_jsx("div", { className: 'flex justify-end border-t pt-2', children: flexRender(actionsCell.column.columnDef.cell, actionsCell.getContext()) }))] }, row.id));
592
+ })) : (_jsxs("div", { className: 'flex flex-col items-center justify-center gap-2 rounded-lg border bg-card py-12 text-muted-foreground', children: [_jsx("div", { className: 'flex h-16 w-16 items-center justify-center rounded-full bg-muted/50', children: _jsx(Inbox, { className: 'h-8 w-8' }) }), _jsx("h3", { className: 'text-base font-semibold text-foreground', children: "No se encontraron resultados" }), _jsx("p", { className: 'text-sm text-muted-foreground', children: "No hay datos para mostrar en este momento." })] })) }), _jsx("div", { className: 'shrink-0 pt-4', children: _jsx(DataTablePagination, { table: table, pageSizeOptions: metadata.perPageOptions }) })] }), _jsx(AlertDialog, { open: !!rowToDelete, onOpenChange: (open) => !open && setRowToDelete(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "\u00BFEst\u00E1 absolutamente seguro?" }), _jsx(AlertDialogDescription, { children: "Esta acci\u00F3n no se puede deshacer. Esto eliminar\u00E1 permanentemente el registro seleccionado de nuestros servidores." })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: isDeleting, children: t('common.cancel') }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); confirmDelete(); }, className: "bg-red-600 hover:bg-red-700", disabled: isDeleting, children: isDeleting ? 'Eliminando...' : 'Eliminar' })] })] }) }), _jsx(AlertDialog, { open: showBulkDeleteConfirm, onOpenChange: (open) => !open && !isBulkDeleting && setShowBulkDeleteConfirm(false), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: isBulkDeleting ? 'Eliminando registros...' : '¿Eliminar múltiples registros?' }), _jsx(AlertDialogDescription, { children: isBulkDeleting ? (_jsxs("div", { className: "space-y-4 mt-4", children: [_jsx(Progress, { value: (bulkDeleteProgress / bulkDeleteTotal) * 100 }), _jsxs("p", { className: "text-center text-sm", children: ["Procesando ", bulkDeleteProgress, " de ", bulkDeleteTotal, " registros..."] })] })) : (_jsxs(_Fragment, { children: ["Esta acci\u00F3n no se puede deshacer. Se eliminar\u00E1n permanentemente ", _jsx("strong", { children: Object.keys(rowSelection).length }), " registro(s) de nuestros servidores."] })) })] }), !isBulkDeleting && (_jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: t('common.cancel') }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); confirmBulkDelete(); }, className: "bg-red-600 hover:bg-red-700", children: "Eliminar todos" })] }))] }) }), _jsx(DynamicRecordDialog, { open: recordDialog.open, onOpenChange: (open) => setRecordDialog((prev) => ({ ...prev, open })), mode: recordDialog.mode, model: model, recordId: recordDialog.recordId, endpoint: endpoint, onSaved: handleRefresh }), metadata.canExport && (_jsx(ExportDialog, { open: exportOpen, onOpenChange: setExportOpen, model: model, metadata: metadata, currentFilters: buildFilterParams(), hasActiveFilters: hasActiveFilters })), metadata.canImport && (_jsx(ImportDialog, { open: importOpen, onOpenChange: setImportOpen, model: model, metadata: metadata, onImported: handleRefresh })), actionModal.action && (_jsx(ActionModalDispatcher, { open: actionModal.open, onOpenChange: (open) => setActionModal((prev) => ({ ...prev, open })), action: actionModal.action, model: model, record: actionModal.record, endpoint: endpoint, onSuccess: handleRefresh })), _jsx(DataTableBulkActions, { table: table, entityName: "registro", children: _jsxs(Button, { variant: "destructive", size: "sm", className: "h-8", onClick: () => setShowBulkDeleteConfirm(true), children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5" }), " Eliminar"] }) })] }));
584
593
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "13.6.0",
3
+ "version": "13.8.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "date-fns": ">=3",
35
35
  "react-day-picker": ">=8",
36
36
  "@asteby/metacore-sdk": "^3.1.0",
37
- "@asteby/metacore-ui": "^2.1.0"
37
+ "@asteby/metacore-ui": "^2.1.1"
38
38
  },
39
39
  "peerDependenciesMeta": {
40
40
  "@tanstack/react-router": {
@@ -62,7 +62,7 @@
62
62
  "vitest": "^4.0.0",
63
63
  "zustand": "^5.0.0",
64
64
  "@asteby/metacore-sdk": "3.1.0",
65
- "@asteby/metacore-ui": "2.1.0"
65
+ "@asteby/metacore-ui": "2.1.1"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc -p tsconfig.json",
@@ -34,7 +34,7 @@ import {
34
34
  PopoverContent,
35
35
  PopoverTrigger,
36
36
  } from '@asteby/metacore-ui/primitives'
37
- import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
37
+ import { Check, ChevronsUpDown, Loader2, Plus } from 'lucide-react'
38
38
  import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
39
39
  import type { ActionFieldDef } from './types'
40
40
 
@@ -87,8 +87,32 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
87
87
  setSearch('')
88
88
  }
89
89
 
90
+ // Inline-create: the "+" opens the REFERENCED model's own create modal (the
91
+ // real one the host renders for that model — full fields, not a duplicate),
92
+ // via a decoupled window event the host listens for. On success the host
93
+ // hands back the new record and we select it immediately. No host import →
94
+ // no circular dependency; works for ANY dynamic_select with a `ref`.
95
+ const openCreate = () => {
96
+ if (!field.ref || typeof window === 'undefined') return
97
+ window.dispatchEvent(
98
+ new CustomEvent('metacore:create-record', {
99
+ detail: {
100
+ model: field.ref,
101
+ onCreated: (rec: any) => {
102
+ if (rec && rec.id != null) {
103
+ const id = String(rec.id)
104
+ const label = String(rec.name ?? rec.label ?? rec.title ?? rec.id)
105
+ handlePick({ id, value: id, label, name: label })
106
+ }
107
+ },
108
+ },
109
+ }),
110
+ )
111
+ }
112
+
90
113
  return (
91
- <Popover open={open} onOpenChange={setOpen}>
114
+ <div className="flex items-center gap-1.5">
115
+ <Popover open={open} onOpenChange={setOpen}>
92
116
  <PopoverTrigger asChild>
93
117
  <Button
94
118
  type="button"
@@ -157,7 +181,21 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
157
181
  </CommandList>
158
182
  </Command>
159
183
  </PopoverContent>
160
- </Popover>
184
+ </Popover>
185
+ {field.ref && (
186
+ <Button
187
+ type="button"
188
+ variant="outline"
189
+ size="icon"
190
+ className="size-9 shrink-0"
191
+ onClick={openCreate}
192
+ title={`Crear ${field.label ?? field.ref}`}
193
+ aria-label={`Crear ${field.label ?? field.ref}`}
194
+ >
195
+ <Plus className="size-4" />
196
+ </Button>
197
+ )}
198
+ </div>
161
199
  )
162
200
  }
163
201
 
@@ -687,7 +687,10 @@ export function DynamicTable({
687
687
  }
688
688
  />
689
689
  </div>
690
- <div className='flex-1 min-h-0 overflow-auto border rounded-md bg-card'>
690
+ {/* Desktop: classic horizontal-scroll table. Hidden on phones —
691
+ a 7-column table forces a wide horizontal scroll there, so we
692
+ render a card-per-row list instead (see MobileCards below). */}
693
+ <div className='hidden sm:block flex-1 min-h-0 overflow-auto border rounded-md bg-card'>
691
694
  <Table noWrapper className="min-w-max w-full">
692
695
  <TableHeader className='sticky top-0 z-10'>
693
696
  {table.getHeaderGroups().map((headerGroup: HeaderGroup<any>) => (
@@ -746,6 +749,63 @@ export function DynamicTable({
746
749
  </TableBody>
747
750
  </Table>
748
751
  </div>
752
+
753
+ {/* Mobile: one card per row — no horizontal scroll. Each card
754
+ stacks its columns as label : value pairs with the row actions
755
+ pinned at the bottom. */}
756
+ <div className='flex flex-1 min-h-0 flex-col gap-2 overflow-y-auto sm:hidden'>
757
+ {loadingData && data.length === 0 ? (
758
+ Array.from({ length: 5 }).map((_, i) => (
759
+ <div key={i} className='rounded-lg border bg-card p-3'>
760
+ <Skeleton className='h-4 w-24' />
761
+ <Skeleton className='mt-2 h-4 w-40' />
762
+ <Skeleton className='mt-2 h-4 w-32' />
763
+ </div>
764
+ ))
765
+ ) : table.getRowModel().rows?.length ? (
766
+ table.getRowModel().rows.map((row: Row<any>) => {
767
+ const cells = row.getVisibleCells()
768
+ const actionsCell = cells.find((c: Cell<any, unknown>) => c.column.id === 'actions')
769
+ const dataCells = cells.filter(
770
+ (c: Cell<any, unknown>) => c.column.id !== 'actions' && c.column.id !== 'select',
771
+ )
772
+ return (
773
+ <div
774
+ key={row.id}
775
+ data-state={row.getIsSelected() && 'selected'}
776
+ className='flex flex-col gap-1.5 rounded-lg border bg-card p-3 data-[state=selected]:border-primary/40'
777
+ >
778
+ {dataCells.map((cell: Cell<any, unknown>) => {
779
+ const header = cell.column.columnDef.header
780
+ const label = typeof header === 'string' ? header : cell.column.id
781
+ return (
782
+ <div key={cell.id} className='flex items-start justify-between gap-3 text-sm'>
783
+ <span className='shrink-0 text-muted-foreground'>{label}</span>
784
+ <span className='min-w-0 break-words text-right font-medium'>
785
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
786
+ </span>
787
+ </div>
788
+ )
789
+ })}
790
+ {actionsCell && (
791
+ <div className='flex justify-end border-t pt-2'>
792
+ {flexRender(actionsCell.column.columnDef.cell, actionsCell.getContext())}
793
+ </div>
794
+ )}
795
+ </div>
796
+ )
797
+ })
798
+ ) : (
799
+ <div className='flex flex-col items-center justify-center gap-2 rounded-lg border bg-card py-12 text-muted-foreground'>
800
+ <div className='flex h-16 w-16 items-center justify-center rounded-full bg-muted/50'>
801
+ <Inbox className='h-8 w-8' />
802
+ </div>
803
+ <h3 className='text-base font-semibold text-foreground'>No se encontraron resultados</h3>
804
+ <p className='text-sm text-muted-foreground'>No hay datos para mostrar en este momento.</p>
805
+ </div>
806
+ )}
807
+ </div>
808
+
749
809
  <div className='shrink-0 pt-4'>
750
810
  <DataTablePagination
751
811
  table={table}