@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 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":"AAkEA,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"}
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"}
@@ -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
- const isActions = header.id === 'actions';
266
- 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));
267
- }) }, 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
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "29.0.0",
3
+ "version": "29.0.1",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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