@asteby/metacore-runtime-react 29.0.0 → 29.0.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 +14 -0
- package/dist/dynamic-relation.d.ts.map +1 -1
- package/dist/dynamic-relation.js +43 -4
- package/package.json +1 -1
- package/src/dynamic-relation.tsx +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 29.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- be357ff: fix(runtime-react): las sub-tablas de relación resuelven las columnas `ref` al
|
|
8
|
+
nombre del registro en vez del uuid crudo.
|
|
9
|
+
|
|
10
|
+
`OneToManyRelation` construía sus columnas sin precargar las opciones de las
|
|
11
|
+
columnas `ref` (a diferencia de `<DynamicTable>`, que las mete en `OptionsContext`),
|
|
12
|
+
así que una celda `ref` (p.ej. el Producto de un renglón de orden de compra)
|
|
13
|
+
mostraba el uuid. Ahora la sub-tabla precarga `/api/options/<Model>` de sus
|
|
14
|
+
columnas `useOptions && searchEndpoint` y las provee vía `OptionsContext.Provider`,
|
|
15
|
+
igual que la tabla principal — el chip muestra "Nombre / SKU".
|
|
16
|
+
|
|
3
17
|
## 29.0.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AAmEA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,OAAO,EACH,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,aAAa,EACb,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,cAAc,GACjB,MAAM,4BAA4B,CAAA;AAEnC,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,wBAAwB,EAAE,MAAM,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;IACzB,uBAAuB,EAAE,MAAM,CAAA;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,oDAAoD;IACpD,iBAAiB,EAAE,MAAM,CAAA;IACzB,sDAAsD;IACtD,UAAU,EAAE,MAAM,CAAA;CACrB;AAwBD,UAAU,WAAW;IACjB,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,2BAA2B;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAA;IACzC,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED,MAAM,WAAW,6BAA8B,SAAQ,WAAW;IAC9D,IAAI,EAAE,aAAa,CAAA;IACnB,yFAAyF;IACzF,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,8BAA+B,SAAQ,WAAW;IAC/D,IAAI,EAAE,cAAc,CAAA;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,6BAA6B;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,uEAAuE;IACvE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC1B,6BAA6B,GAC7B,8BAA8B,CAAA;AAEpC,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAK1D"}
|
package/dist/dynamic-relation.js
CHANGED
|
@@ -12,6 +12,7 @@ import { Button, Skeleton, AlertDialog, AlertDialogAction, AlertDialogCancel, Al
|
|
|
12
12
|
import { Plus, Trash2, Pencil, Search } from 'lucide-react';
|
|
13
13
|
import { useApi } from './api-context';
|
|
14
14
|
import { useMetadataCache } from './metadata-cache';
|
|
15
|
+
import { OptionsContext } from './options-context';
|
|
15
16
|
import { DynamicForm } from './dynamic-form';
|
|
16
17
|
import { useImageUrl } from './image-url-context';
|
|
17
18
|
import { useTimeZone, useCurrency } from './org-runtime-context';
|
|
@@ -65,6 +66,11 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
|
|
|
65
66
|
const cachedMeta = getMetadata(model);
|
|
66
67
|
const labels = { ...DEFAULT_STRINGS, ...(strings || {}) };
|
|
67
68
|
const [metadata, setMetadata] = useState(cachedMeta || null);
|
|
69
|
+
// Options for the ref/relation columns so their cells render the record's
|
|
70
|
+
// NAME (+ SKU subtitle) instead of the raw uuid. The main <DynamicTable>
|
|
71
|
+
// preloads these into OptionsContext; a sub-table skipped it, so a `ref`
|
|
72
|
+
// column (e.g. a purchase order line's Product) showed the bare uuid.
|
|
73
|
+
const [optionsMap, setOptionsMap] = useState(new Map());
|
|
68
74
|
const [rows, setRows] = useState([]);
|
|
69
75
|
const [loading, setLoading] = useState(true);
|
|
70
76
|
// Paginación server-side + scroll infinito, igual que <DynamicTable>.
|
|
@@ -94,6 +100,39 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
|
|
|
94
100
|
const id = setTimeout(() => setDebouncedSearch(search.trim()), 300);
|
|
95
101
|
return () => clearTimeout(id);
|
|
96
102
|
}, [search]);
|
|
103
|
+
// Preload the options of the ref/relation columns (same shape <DynamicTable>
|
|
104
|
+
// uses: `useOptions && searchEndpoint`) so their cells resolve the referenced
|
|
105
|
+
// record's name via OptionsContext instead of printing the raw uuid.
|
|
106
|
+
const refEndpointsKey = useMemo(() => {
|
|
107
|
+
const eps = (metadata?.columns || [])
|
|
108
|
+
.filter((c) => c.useOptions && c.searchEndpoint)
|
|
109
|
+
.map((c) => c.searchEndpoint);
|
|
110
|
+
return Array.from(new Set(eps)).sort().join('|');
|
|
111
|
+
}, [metadata]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!refEndpointsKey) {
|
|
114
|
+
setOptionsMap((prev) => (prev.size ? new Map() : prev));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const endpoints = refEndpointsKey.split('|');
|
|
118
|
+
let cancelled = false;
|
|
119
|
+
void Promise.all(endpoints.map(async (ep) => {
|
|
120
|
+
try {
|
|
121
|
+
const res = (await api.get(ep));
|
|
122
|
+
return [ep, res.data?.success ? res.data.data || [] : []];
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return [ep, []];
|
|
126
|
+
}
|
|
127
|
+
})).then((entries) => {
|
|
128
|
+
if (!cancelled)
|
|
129
|
+
setOptionsMap(new Map(entries));
|
|
130
|
+
});
|
|
131
|
+
return () => {
|
|
132
|
+
cancelled = true;
|
|
133
|
+
};
|
|
134
|
+
}, [api, refEndpointsKey]);
|
|
135
|
+
const optionsCtx = useMemo(() => ({ optionsMap }), [optionsMap]);
|
|
97
136
|
// fetchPage REEMPLAZA (página 1) o ANEXA (scroll infinito). Antes esta lista
|
|
98
137
|
// pedía el modelo hijo entero sin page/per_page.
|
|
99
138
|
const fetchPage = useCallback(async (nextPage, append) => {
|
|
@@ -261,10 +300,10 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
|
|
|
261
300
|
setSubmitting(false);
|
|
262
301
|
}
|
|
263
302
|
}, [api, dataEndpoint, fetchAll, onChange, rowToDelete]);
|
|
264
|
-
return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate || showSearch) && (_jsxs("div", { className: "flex items-center justify-between gap-2 pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [showSearch && (_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute start-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: search, onChange: (e) => setSearch(e.target.value), placeholder: labels.searchPlaceholder, className: "h-8 w-40 ps-7 sm:w-56" })] })), canCreate && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => { setEditingRow(null); setFormOpen(true); }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), labels.addLabel] }))] })] })), loading ? (_jsx("div", { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-10 w-full" }, `rel-skeleton-${i}`))) })) : rows.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsxs("div", { ref: rootRef, className: "overflow-auto max-h-[60vh] border rounded-md bg-card", children: [_jsxs(Table, { noWrapper: true, className: "w-full", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { className: "border-b-0 hover:bg-transparent", children: headerGroup.headers.map((header) => {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
303
|
+
return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate || showSearch) && (_jsxs("div", { className: "flex items-center justify-between gap-2 pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [showSearch && (_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute start-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: search, onChange: (e) => setSearch(e.target.value), placeholder: labels.searchPlaceholder, className: "h-8 w-40 ps-7 sm:w-56" })] })), canCreate && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => { setEditingRow(null); setFormOpen(true); }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), labels.addLabel] }))] })] })), loading ? (_jsx("div", { className: "space-y-2", children: Array.from({ length: 3 }).map((_, i) => (_jsx(Skeleton, { className: "h-10 w-full" }, `rel-skeleton-${i}`))) })) : rows.length === 0 ? (_jsx("div", { className: "text-center text-sm text-muted-foreground py-8 border rounded-md bg-muted/30", children: labels.emptyState })) : (_jsx(OptionsContext.Provider, { value: optionsCtx, children: _jsxs("div", { ref: rootRef, className: "overflow-auto max-h-[60vh] border rounded-md bg-card", children: [_jsxs(Table, { noWrapper: true, className: "w-full", children: [_jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => (_jsx(TableRow, { className: "border-b-0 hover:bg-transparent", children: headerGroup.headers.map((header) => {
|
|
304
|
+
const isActions = header.id === 'actions';
|
|
305
|
+
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', isActions && 'text-right'), children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
|
|
306
|
+
}) }, headerGroup.id))) }), _jsx(TableBody, { children: table.getRowModel().rows.map((row, idx) => (_jsx(TableRow, { children: row.getVisibleCells().map((cell) => (_jsx(TableCell, { style: cell.column.columnDef.size ? { width: cell.column.columnDef.size } : undefined, className: "py-2", children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, relationRowKey(row.original, idx, foreignKey)))) })] }), loadingMore && (_jsx("div", { className: "p-2", children: _jsx(Skeleton, { className: "h-8 w-full" }) })), _jsx("div", { ref: sentinelRef, "aria-hidden": true, className: "h-px w-full" })] }) })), !loading && rows.length > 0 && total > rows.length && (_jsx("div", { className: "pt-2 text-end text-xs text-muted-foreground", children: labels.countLabel
|
|
268
307
|
.replace('{{loaded}}', String(rows.length))
|
|
269
308
|
.replace('{{total}}', String(total)) })), _jsx(Dialog, { open: formOpen, onOpenChange: (open) => { setFormOpen(open); if (!open)
|
|
270
309
|
setEditingRow(null); }, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: editingRow ? labels.editLabel : labels.addLabel }) }), _jsx(DynamicForm, { fields: formFields, initialValues: editingRow || undefined, onSubmit: handleSubmit, onCancel: () => { setFormOpen(false); setEditingRow(null); }, submitLabel: labels.saveLabel, cancelLabel: labels.cancelLabel, disabled: submitting })] }) }), _jsx(AlertDialog, { open: !!rowToDelete, onOpenChange: (open) => !open && setRowToDelete(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: labels.confirmRemoveTitle }), _jsx(AlertDialogDescription, { children: labels.confirmRemoveDescription })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: submitting, children: labels.cancelLabel }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); handleDelete(); }, className: "bg-red-600 hover:bg-red-700", disabled: submitting, children: labels.removeLabel })] })] }) })] }));
|
package/package.json
CHANGED
package/src/dynamic-relation.tsx
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
import { Plus, Trash2, Pencil, Search } from 'lucide-react'
|
|
44
44
|
import { useApi } from './api-context'
|
|
45
45
|
import { useMetadataCache } from './metadata-cache'
|
|
46
|
+
import { OptionsContext } from './options-context'
|
|
46
47
|
import { DynamicForm } from './dynamic-form'
|
|
47
48
|
import { useImageUrl } from './image-url-context'
|
|
48
49
|
import { useTimeZone, useCurrency } from './org-runtime-context'
|
|
@@ -244,6 +245,11 @@ function OneToManyRelation({
|
|
|
244
245
|
const labels = { ...DEFAULT_STRINGS, ...(strings || {}) }
|
|
245
246
|
|
|
246
247
|
const [metadata, setMetadata] = useState<TableMetadata | null>(cachedMeta || null)
|
|
248
|
+
// Options for the ref/relation columns so their cells render the record's
|
|
249
|
+
// NAME (+ SKU subtitle) instead of the raw uuid. The main <DynamicTable>
|
|
250
|
+
// preloads these into OptionsContext; a sub-table skipped it, so a `ref`
|
|
251
|
+
// column (e.g. a purchase order line's Product) showed the bare uuid.
|
|
252
|
+
const [optionsMap, setOptionsMap] = useState<Map<string, any[]>>(new Map())
|
|
247
253
|
const [rows, setRows] = useState<any[]>([])
|
|
248
254
|
const [loading, setLoading] = useState(true)
|
|
249
255
|
// Paginación server-side + scroll infinito, igual que <DynamicTable>.
|
|
@@ -277,6 +283,42 @@ function OneToManyRelation({
|
|
|
277
283
|
return () => clearTimeout(id)
|
|
278
284
|
}, [search])
|
|
279
285
|
|
|
286
|
+
// Preload the options of the ref/relation columns (same shape <DynamicTable>
|
|
287
|
+
// uses: `useOptions && searchEndpoint`) so their cells resolve the referenced
|
|
288
|
+
// record's name via OptionsContext instead of printing the raw uuid.
|
|
289
|
+
const refEndpointsKey = useMemo(() => {
|
|
290
|
+
const eps = (metadata?.columns || [])
|
|
291
|
+
.filter((c) => (c as any).useOptions && (c as any).searchEndpoint)
|
|
292
|
+
.map((c) => (c as any).searchEndpoint as string)
|
|
293
|
+
return Array.from(new Set(eps)).sort().join('|')
|
|
294
|
+
}, [metadata])
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (!refEndpointsKey) {
|
|
298
|
+
setOptionsMap((prev) => (prev.size ? new Map() : prev))
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
const endpoints = refEndpointsKey.split('|')
|
|
302
|
+
let cancelled = false
|
|
303
|
+
void Promise.all(
|
|
304
|
+
endpoints.map(async (ep) => {
|
|
305
|
+
try {
|
|
306
|
+
const res = (await api.get(ep)) as { data: ApiResponse<any[]> }
|
|
307
|
+
return [ep, res.data?.success ? res.data.data || [] : []] as const
|
|
308
|
+
} catch {
|
|
309
|
+
return [ep, [] as any[]] as const
|
|
310
|
+
}
|
|
311
|
+
}),
|
|
312
|
+
).then((entries) => {
|
|
313
|
+
if (!cancelled) setOptionsMap(new Map(entries))
|
|
314
|
+
})
|
|
315
|
+
return () => {
|
|
316
|
+
cancelled = true
|
|
317
|
+
}
|
|
318
|
+
}, [api, refEndpointsKey])
|
|
319
|
+
|
|
320
|
+
const optionsCtx = useMemo(() => ({ optionsMap }), [optionsMap])
|
|
321
|
+
|
|
280
322
|
// fetchPage REEMPLAZA (página 1) o ANEXA (scroll infinito). Antes esta lista
|
|
281
323
|
// pedía el modelo hijo entero sin page/per_page.
|
|
282
324
|
const fetchPage = useCallback(async (nextPage: number, append: boolean) => {
|
|
@@ -522,6 +564,7 @@ function OneToManyRelation({
|
|
|
522
564
|
// `max-h` + scroll propio: la sub-tabla no puede estirar el modal
|
|
523
565
|
// del padre a lo alto de cientos de filas. El sentinel del fondo
|
|
524
566
|
// pide la página siguiente al entrar en vista.
|
|
567
|
+
<OptionsContext.Provider value={optionsCtx}>
|
|
525
568
|
<div ref={rootRef} className="overflow-auto max-h-[60vh] border rounded-md bg-card">
|
|
526
569
|
<Table noWrapper className="w-full">
|
|
527
570
|
<TableHeader>
|
|
@@ -566,6 +609,7 @@ function OneToManyRelation({
|
|
|
566
609
|
)}
|
|
567
610
|
<div ref={sentinelRef} aria-hidden className="h-px w-full" />
|
|
568
611
|
</div>
|
|
612
|
+
</OptionsContext.Provider>
|
|
569
613
|
)}
|
|
570
614
|
|
|
571
615
|
{/* Cuánto de la relación se ve. Sin esto una sub-tabla paginada
|