@asteby/metacore-runtime-react 18.4.0 → 18.5.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,25 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d7c792d: Render one_to_many relations as a rich table (headers + currency/image/date/badge cells)
8
+
9
+ `OneToManyRelation` now renders the child list as a real metadata-driven table
10
+ using the same metacore-ui `<Table>` primitives and the exact
11
+ `makeDefaultGetDynamicColumns` cell factory as `<DynamicTable>`, instead of a
12
+ bare flex grid of unlabeled values. Line items now get column headers, money in
13
+ the org currency right-aligned (e.g. `100,00 MXN`), FK thumbnails + labels,
14
+ dates in the org timezone, status/option badges and creator names — matching
15
+ the main dynamic table. The inline edit (DynamicForm dialog) and delete
16
+ (AlertDialog) actions are preserved as a trailing actions column.
17
+
18
+ The org `timeZone`/`currency` contexts were extracted from
19
+ `dialogs/dynamic-record` into a shared `org-runtime-context` module so the
20
+ relation table can consume them without a circular import. `ManyToManyRelation`
21
+ is unchanged.
22
+
3
23
  ## 18.4.0
4
24
 
5
25
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
@@ -28,6 +28,7 @@ import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
28
28
  import { humanizeToken } from '../dynamic-columns-helpers';
29
29
  import { formatDateCell } from '../dynamic-columns';
30
30
  import { ImageUrlContext, identityImageUrl } from '../image-url-context';
31
+ import { TimeZoneContext, CurrencyContext } from '../org-runtime-context';
31
32
  // localizedModelName resolves the (possibly addon-i18n) model name: prefer the
32
33
  // translated titleKey, fall back to the backend-provided raw title.
33
34
  function localizedModelName(meta, t) {
@@ -157,10 +158,6 @@ const MODE_CONFIG = {
157
158
  // Context threading host runtime values to nested field components (uploads,
158
159
  // image leads, tz-aware dates) without prop-drilling through every renderer.
159
160
  const ModelContext = createContext('');
160
- const TimeZoneContext = createContext(undefined);
161
- // Org ISO-4217 currency (org config, like the timezone) used as the fallback
162
- // for money fields that don't carry an explicit per-field currency.
163
- const CurrencyContext = createContext(undefined);
164
161
  // Money-key heuristic mirroring the backend's `inferDisplayCellStyle`: lets the
165
162
  // dialog format obvious money fields as currency even when the backend hasn't
166
163
  // stamped `cellStyle:'currency'` yet. Case-insensitive; matches a key that
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-relation.d.ts","sourceRoot":"","sources":["../src/dynamic-relation.tsx"],"names":[],"mappings":"AA6CA,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;CACtB;AAiBD,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,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,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":"AA+DA,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;CACtB;AAiBD,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,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,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"}
@@ -5,15 +5,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  // - "many_to_many": multi-select sobre la tabla destino con sync a la pivot.
6
6
  // La RFC completa vive en `packages/runtime-react/docs/relations.md`.
7
7
  import { useCallback, useEffect, useMemo, useState } from 'react';
8
- import { Button, Skeleton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Dialog, DialogContent, DialogHeader, DialogTitle, MultiSelect, } from '@asteby/metacore-ui/primitives';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { flexRender, getCoreRowModel, useReactTable, } from '@tanstack/react-table';
10
+ import { cn } from '@asteby/metacore-ui/lib';
11
+ import { Button, Skeleton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Dialog, DialogContent, DialogHeader, DialogTitle, MultiSelect, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@asteby/metacore-ui/primitives';
9
12
  import { Plus, Trash2, Pencil } from 'lucide-react';
10
13
  import { useApi } from './api-context';
11
14
  import { useMetadataCache } from './metadata-cache';
12
15
  import { DynamicForm } from './dynamic-form';
13
16
  import { useImageUrl } from './image-url-context';
14
- import { OptionThumb } from './dynamic-select-field';
17
+ import { useTimeZone, useCurrency } from './org-runtime-context';
18
+ import { makeDefaultGetDynamicColumns } from './dynamic-columns';
15
19
  import { useOptionsResolver } from './use-options-resolver';
16
- import { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, formatRelationCell, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
20
+ import { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
17
21
  export { buildCreatePayload, buildPivotAttachPayload, buildPivotRowIndex, buildRelationFilterParams, deriveRelationFormFields, diffSelection, extractSelectedTargetIds, formatRelationCell, objectLabel, pickOptionLabel, relationRowKey, } from './dynamic-relation-helpers';
18
22
  const DEFAULT_STRINGS = {
19
23
  title: '',
@@ -38,6 +42,9 @@ export function DynamicRelation(props) {
38
42
  function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoint, hiddenColumns = [], canCreate = true, canDelete = true, canEdit = true, strings, className, onChange, }) {
39
43
  const api = useApi();
40
44
  const getImageUrl = useImageUrl();
45
+ const timeZone = useTimeZone();
46
+ const currency = useCurrency();
47
+ const { i18n } = useTranslation();
41
48
  const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache();
42
49
  const cachedMeta = getMetadata(model);
43
50
  const labels = { ...DEFAULT_STRINGS, ...(strings || {}) };
@@ -88,6 +95,43 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
88
95
  const hidden = new Set([foreignKey, ...Object.keys(filters || {}), ...hiddenColumns]);
89
96
  return metadata.columns.filter(c => !hidden.has(c.key) && !c.hidden);
90
97
  }, [metadata, foreignKey, filtersKey, hiddenColumns]);
98
+ // Reuse the EXACT column factory the main `<DynamicTable>` uses so each cell
99
+ // renders identically — money in the org currency right-aligned, FK chips
100
+ // with thumbnails, dates in the org timezone, status/option badges, creator
101
+ // names — instead of a hand-rolled parallel formatting stack. Stable per
102
+ // image-url resolver.
103
+ const buildColumns = useMemo(() => makeDefaultGetDynamicColumns({ getImageUrl }), [getImageUrl]);
104
+ const showActions = canEdit || canDelete;
105
+ const columns = useMemo(() => {
106
+ if (!metadata)
107
+ return [];
108
+ // Feed the factory a metadata view scoped to the visible columns only,
109
+ // with model-level actions stripped — the relation list owns its own
110
+ // inline edit/delete column (appended below), and the factory's
111
+ // select/actions columns don't belong in an embedded child list.
112
+ const scopedMeta = {
113
+ ...metadata,
114
+ columns: visibleColumns,
115
+ actions: [],
116
+ hasActions: false,
117
+ enableCRUDActions: false,
118
+ };
119
+ const base = buildColumns(scopedMeta, () => { }, (key, opts) => opts?.defaultValue ?? key, i18n?.language || 'es', new Map(), timeZone, currency).filter((c) => c.id !== 'select' && c.id !== 'actions');
120
+ if (!showActions)
121
+ return base;
122
+ const actionsCol = {
123
+ id: 'actions',
124
+ header: () => _jsx("span", { className: "sr-only", children: labels.editLabel }),
125
+ size: 80,
126
+ cell: ({ row }) => (_jsxs("div", { className: "flex items-center justify-end gap-1", children: [canEdit && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => { setEditingRow(row.original); setFormOpen(true); }, "aria-label": labels.editLabel, children: _jsx(Pencil, { className: "h-4 w-4" }) })), canDelete && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setRowToDelete(row.original), "aria-label": labels.removeLabel, children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })),
127
+ };
128
+ return [...base, actionsCol];
129
+ }, [metadata, visibleColumns, buildColumns, i18n?.language, timeZone, currency, showActions, canEdit, canDelete, labels.editLabel, labels.removeLabel]);
130
+ const table = useReactTable({
131
+ data: rows,
132
+ columns,
133
+ getCoreRowModel: getCoreRowModel(),
134
+ });
91
135
  const handleSubmit = useCallback(async (values) => {
92
136
  setSubmitting(true);
93
137
  try {
@@ -137,21 +181,10 @@ function OneToManyRelation({ kind, model, foreignKey, parentId, filters, endpoin
137
181
  setSubmitting(false);
138
182
  }
139
183
  }, [api, dataEndpoint, fetchAll, onChange, rowToDelete]);
140
- return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate) && (_jsxs("div", { className: "flex items-center justify-between pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), 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("div", { className: "border rounded-md divide-y bg-card", children: rows.map((row, idx) => (_jsxs("div", { className: "flex items-center justify-between gap-3 px-3 py-2", children: [_jsx("div", { className: "flex-1 grid grid-cols-[repeat(auto-fit,minmax(0,1fr))] gap-2 text-sm", children: visibleColumns.map(col => {
141
- const cell = formatRelationCell(row, col);
142
- // FK column whose backend-resolved sibling
143
- // carries an image render a thumbnail + label
144
- // instead of plain text (e.g. a line item's
145
- // product photo). The sibling is the column key
146
- // with the trailing `_id` stripped.
147
- const isFk = !!col.ref || col.key.endsWith('_id');
148
- const sibling = isFk ? row[col.key.replace(/_id$/, '')] : undefined;
149
- if (sibling && typeof sibling === 'object' && sibling.image) {
150
- const label = sibling.label ?? sibling.name ?? cell;
151
- return (_jsxs("span", { className: "flex min-w-0 items-center gap-2", title: String(label), children: [_jsx(OptionThumb, { image: getImageUrl(sibling.image), size: 20 }), _jsx("span", { className: "truncate", children: label })] }, col.key));
152
- }
153
- return (_jsx("span", { className: "truncate", title: cell, children: cell }, col.key));
154
- }) }), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [canEdit && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => { setEditingRow(row); setFormOpen(true); }, "aria-label": labels.editLabel, children: _jsx(Pencil, { className: "h-4 w-4" }) })), canDelete && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setRowToDelete(row), "aria-label": labels.removeLabel, children: _jsx(Trash2, { className: "h-4 w-4" }) }))] })] }, relationRowKey(row, idx, foreignKey)))) })), _jsx(Dialog, { open: formOpen, onOpenChange: (open) => { setFormOpen(open); if (!open)
184
+ return (_jsxs("div", { className: className, "data-relation-kind": kind, "data-relation-model": model, children: [(labels.title || canCreate) && (_jsxs("div", { className: "flex items-center justify-between pb-3", children: [labels.title ? _jsx("h3", { className: "text-sm font-medium", children: labels.title }) : _jsx("span", {}), 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("div", { className: "overflow-x-auto 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) => {
185
+ const isActions = header.id === 'actions';
186
+ 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));
187
+ }) }, 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)))) })] }) })), _jsx(Dialog, { open: formOpen, onOpenChange: (open) => { setFormOpen(open); if (!open)
155
188
  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 })] })] }) })] }));
156
189
  }
157
190
  function ManyToManyRelation({ kind, through, references, foreignKey, referencesKey, parentId, filters, pivotEndpoint, referencesEndpoint, displayKey, canCreate = true, canDelete = true, strings, className, onChange, }) {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * IANA timezone (e.g. the org's `America/Mexico_City`) used to render
3
+ * datetime/timestamp cells. `undefined` outside a provider → renderers fall
4
+ * back to the viewer's browser zone (legacy behaviour).
5
+ */
6
+ export declare const TimeZoneContext: import("react").Context<string | undefined>;
7
+ /** Reads the nearest org timezone (undefined outside a provider). */
8
+ export declare const useTimeZone: () => string | undefined;
9
+ /**
10
+ * Org ISO-4217 currency (org config, like the timezone) used as the fallback
11
+ * for money cells/fields that don't carry an explicit per-column currency.
12
+ * `undefined` outside a provider → renderers fall back to 'USD'.
13
+ */
14
+ export declare const CurrencyContext: import("react").Context<string | undefined>;
15
+ /** Reads the nearest org currency (undefined outside a provider). */
16
+ export declare const useCurrency: () => string | undefined;
17
+ //# sourceMappingURL=org-runtime-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"org-runtime-context.d.ts","sourceRoot":"","sources":["../src/org-runtime-context.ts"],"names":[],"mappings":"AASA;;;;GAIG;AACH,eAAO,MAAM,eAAe,6CAA+C,CAAA;AAE3E,qEAAqE;AACrE,eAAO,MAAM,WAAW,0BAAoC,CAAA;AAE5D;;;;GAIG;AACH,eAAO,MAAM,eAAe,6CAA+C,CAAA;AAE3E,qEAAqE;AACrE,eAAO,MAAM,WAAW,0BAAoC,CAAA"}
@@ -0,0 +1,24 @@
1
+ // Org runtime contexts — thread the org's display config (timezone, currency)
2
+ // to nested field/cell/relation renderers without prop-drilling. Lives in its
3
+ // own module (mirroring `image-url-context`) so any renderer can consume them
4
+ // without importing from `dialogs/dynamic-record`. That dialog imports
5
+ // `dynamic-relations` → `dynamic-relation`, so the relation table cannot import
6
+ // these contexts back from the dialog without a circular import — hence this
7
+ // standalone module is the single source of truth.
8
+ import { createContext, useContext } from 'react';
9
+ /**
10
+ * IANA timezone (e.g. the org's `America/Mexico_City`) used to render
11
+ * datetime/timestamp cells. `undefined` outside a provider → renderers fall
12
+ * back to the viewer's browser zone (legacy behaviour).
13
+ */
14
+ export const TimeZoneContext = createContext(undefined);
15
+ /** Reads the nearest org timezone (undefined outside a provider). */
16
+ export const useTimeZone = () => useContext(TimeZoneContext);
17
+ /**
18
+ * Org ISO-4217 currency (org config, like the timezone) used as the fallback
19
+ * for money cells/fields that don't carry an explicit per-column currency.
20
+ * `undefined` outside a provider → renderers fall back to 'USD'.
21
+ */
22
+ export const CurrencyContext = createContext(undefined);
23
+ /** Reads the nearest org currency (undefined outside a provider). */
24
+ export const useCurrency = () => useContext(CurrencyContext);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.4.0",
3
+ "version": "18.5.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -57,6 +57,7 @@ import { humanizeToken } from '../dynamic-columns-helpers'
57
57
  import { formatDateCell } from '../dynamic-columns'
58
58
  import type { ActionFieldDef, RelationMeta } from '../types'
59
59
  import { ImageUrlContext, identityImageUrl, type GetImageUrl } from '../image-url-context'
60
+ import { TimeZoneContext, CurrencyContext } from '../org-runtime-context'
60
61
 
61
62
  // Re-export the resolver type so `index.ts`'s
62
63
  // `export type { … GetImageUrl } from './dialogs/dynamic-record'` keeps working.
@@ -350,10 +351,6 @@ const MODE_CONFIG = {
350
351
  // Context threading host runtime values to nested field components (uploads,
351
352
  // image leads, tz-aware dates) without prop-drilling through every renderer.
352
353
  const ModelContext = createContext('')
353
- const TimeZoneContext = createContext<string | undefined>(undefined)
354
- // Org ISO-4217 currency (org config, like the timezone) used as the fallback
355
- // for money fields that don't carry an explicit per-field currency.
356
- const CurrencyContext = createContext<string | undefined>(undefined)
357
354
 
358
355
  // Money-key heuristic mirroring the backend's `inferDisplayCellStyle`: lets the
359
356
  // dialog format obvious money fields as currency even when the backend hasn't
@@ -4,6 +4,18 @@
4
4
  // - "many_to_many": multi-select sobre la tabla destino con sync a la pivot.
5
5
  // La RFC completa vive en `packages/runtime-react/docs/relations.md`.
6
6
  import { useCallback, useEffect, useMemo, useState } from 'react'
7
+ import { useTranslation } from 'react-i18next'
8
+ import {
9
+ type ColumnDef,
10
+ type Row,
11
+ type Cell,
12
+ type HeaderGroup,
13
+ type Header,
14
+ flexRender,
15
+ getCoreRowModel,
16
+ useReactTable,
17
+ } from '@tanstack/react-table'
18
+ import { cn } from '@asteby/metacore-ui/lib'
7
19
  import {
8
20
  Button,
9
21
  Skeleton,
@@ -20,13 +32,20 @@ import {
20
32
  DialogHeader,
21
33
  DialogTitle,
22
34
  MultiSelect,
35
+ Table,
36
+ TableBody,
37
+ TableCell,
38
+ TableHead,
39
+ TableHeader,
40
+ TableRow,
23
41
  } from '@asteby/metacore-ui/primitives'
24
42
  import { Plus, Trash2, Pencil } from 'lucide-react'
25
43
  import { useApi } from './api-context'
26
44
  import { useMetadataCache } from './metadata-cache'
27
45
  import { DynamicForm } from './dynamic-form'
28
46
  import { useImageUrl } from './image-url-context'
29
- import { OptionThumb } from './dynamic-select-field'
47
+ import { useTimeZone, useCurrency } from './org-runtime-context'
48
+ import { makeDefaultGetDynamicColumns } from './dynamic-columns'
30
49
  import { useOptionsResolver } from './use-options-resolver'
31
50
  import type { ApiResponse, TableMetadata } from './types'
32
51
  import {
@@ -37,7 +56,6 @@ import {
37
56
  deriveRelationFormFields,
38
57
  diffSelection,
39
58
  extractSelectedTargetIds,
40
- formatRelationCell,
41
59
  pickOptionLabel,
42
60
  relationRowKey,
43
61
  type DynamicRelationKind,
@@ -172,6 +190,9 @@ function OneToManyRelation({
172
190
  }: DynamicRelationOneToManyProps) {
173
191
  const api = useApi()
174
192
  const getImageUrl = useImageUrl()
193
+ const timeZone = useTimeZone()
194
+ const currency = useCurrency()
195
+ const { i18n } = useTranslation()
175
196
  const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache()
176
197
  const cachedMeta = getMetadata(model)
177
198
  const labels = { ...DEFAULT_STRINGS, ...(strings || {}) }
@@ -228,6 +249,81 @@ function OneToManyRelation({
228
249
  return metadata.columns.filter(c => !hidden.has(c.key) && !c.hidden)
229
250
  }, [metadata, foreignKey, filtersKey, hiddenColumns])
230
251
 
252
+ // Reuse the EXACT column factory the main `<DynamicTable>` uses so each cell
253
+ // renders identically — money in the org currency right-aligned, FK chips
254
+ // with thumbnails, dates in the org timezone, status/option badges, creator
255
+ // names — instead of a hand-rolled parallel formatting stack. Stable per
256
+ // image-url resolver.
257
+ const buildColumns = useMemo(
258
+ () => makeDefaultGetDynamicColumns({ getImageUrl }),
259
+ [getImageUrl],
260
+ )
261
+
262
+ const showActions = canEdit || canDelete
263
+
264
+ const columns = useMemo<ColumnDef<any>[]>(() => {
265
+ if (!metadata) return []
266
+ // Feed the factory a metadata view scoped to the visible columns only,
267
+ // with model-level actions stripped — the relation list owns its own
268
+ // inline edit/delete column (appended below), and the factory's
269
+ // select/actions columns don't belong in an embedded child list.
270
+ const scopedMeta = {
271
+ ...metadata,
272
+ columns: visibleColumns,
273
+ actions: [],
274
+ hasActions: false,
275
+ enableCRUDActions: false,
276
+ } as TableMetadata
277
+ const base = buildColumns(
278
+ scopedMeta,
279
+ () => {},
280
+ (key: string, opts?: any) => opts?.defaultValue ?? key,
281
+ i18n?.language || 'es',
282
+ new Map(),
283
+ timeZone,
284
+ currency,
285
+ ).filter((c) => c.id !== 'select' && c.id !== 'actions')
286
+
287
+ if (!showActions) return base
288
+
289
+ const actionsCol: ColumnDef<any> = {
290
+ id: 'actions',
291
+ header: () => <span className="sr-only">{labels.editLabel}</span>,
292
+ size: 80,
293
+ cell: ({ row }: { row: Row<any> }) => (
294
+ <div className="flex items-center justify-end gap-1">
295
+ {canEdit && (
296
+ <Button
297
+ size="sm"
298
+ variant="ghost"
299
+ onClick={() => { setEditingRow(row.original); setFormOpen(true) }}
300
+ aria-label={labels.editLabel}
301
+ >
302
+ <Pencil className="h-4 w-4" />
303
+ </Button>
304
+ )}
305
+ {canDelete && (
306
+ <Button
307
+ size="sm"
308
+ variant="ghost"
309
+ onClick={() => setRowToDelete(row.original)}
310
+ aria-label={labels.removeLabel}
311
+ >
312
+ <Trash2 className="h-4 w-4" />
313
+ </Button>
314
+ )}
315
+ </div>
316
+ ),
317
+ }
318
+ return [...base, actionsCol]
319
+ }, [metadata, visibleColumns, buildColumns, i18n?.language, timeZone, currency, showActions, canEdit, canDelete, labels.editLabel, labels.removeLabel])
320
+
321
+ const table = useReactTable({
322
+ data: rows,
323
+ columns,
324
+ getCoreRowModel: getCoreRowModel(),
325
+ })
326
+
231
327
  const handleSubmit = useCallback(async (values: Record<string, any>) => {
232
328
  setSubmitting(true)
233
329
  try {
@@ -299,62 +395,46 @@ function OneToManyRelation({
299
395
  {labels.emptyState}
300
396
  </div>
301
397
  ) : (
302
- <div className="border rounded-md divide-y bg-card">
303
- {rows.map((row, idx) => (
304
- <div
305
- key={relationRowKey(row, idx, foreignKey)}
306
- className="flex items-center justify-between gap-3 px-3 py-2"
307
- >
308
- <div className="flex-1 grid grid-cols-[repeat(auto-fit,minmax(0,1fr))] gap-2 text-sm">
309
- {visibleColumns.map(col => {
310
- const cell = formatRelationCell(row, col)
311
- // FK column whose backend-resolved sibling
312
- // carries an image → render a thumbnail + label
313
- // instead of plain text (e.g. a line item's
314
- // product photo). The sibling is the column key
315
- // with the trailing `_id` stripped.
316
- const isFk = !!col.ref || col.key.endsWith('_id')
317
- const sibling = isFk ? (row as any)[col.key.replace(/_id$/, '')] : undefined
318
- if (sibling && typeof sibling === 'object' && sibling.image) {
319
- const label = sibling.label ?? sibling.name ?? cell
398
+ // Real metadata-driven table — same metacore-ui primitives and
399
+ // cell renderers as `<DynamicTable>` so headers, money/currency,
400
+ // FK thumbnails, dates and badges all match the main table.
401
+ <div className="overflow-x-auto border rounded-md bg-card">
402
+ <Table noWrapper className="w-full">
403
+ <TableHeader>
404
+ {table.getHeaderGroups().map((headerGroup: HeaderGroup<any>) => (
405
+ <TableRow key={headerGroup.id} className="border-b-0 hover:bg-transparent">
406
+ {headerGroup.headers.map((header: Header<any, unknown>) => {
407
+ const isActions = header.id === 'actions'
320
408
  return (
321
- <span key={col.key} className="flex min-w-0 items-center gap-2" title={String(label)}>
322
- <OptionThumb image={getImageUrl(sibling.image)} size={20} />
323
- <span className="truncate">{label}</span>
324
- </span>
409
+ <TableHead
410
+ key={header.id}
411
+ colSpan={header.colSpan}
412
+ style={header.column.columnDef.size ? { width: header.column.columnDef.size } : undefined}
413
+ className={cn('bg-card border-b h-10', isActions && 'text-right')}
414
+ >
415
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
416
+ </TableHead>
325
417
  )
326
- }
327
- return (
328
- <span key={col.key} className="truncate" title={cell}>
329
- {cell}
330
- </span>
331
- )
332
- })}
333
- </div>
334
- <div className="flex items-center gap-1 shrink-0">
335
- {canEdit && (
336
- <Button
337
- size="sm"
338
- variant="ghost"
339
- onClick={() => { setEditingRow(row); setFormOpen(true) }}
340
- aria-label={labels.editLabel}
341
- >
342
- <Pencil className="h-4 w-4" />
343
- </Button>
344
- )}
345
- {canDelete && (
346
- <Button
347
- size="sm"
348
- variant="ghost"
349
- onClick={() => setRowToDelete(row)}
350
- aria-label={labels.removeLabel}
351
- >
352
- <Trash2 className="h-4 w-4" />
353
- </Button>
354
- )}
355
- </div>
356
- </div>
357
- ))}
418
+ })}
419
+ </TableRow>
420
+ ))}
421
+ </TableHeader>
422
+ <TableBody>
423
+ {table.getRowModel().rows.map((row: Row<any>, idx: number) => (
424
+ <TableRow key={relationRowKey(row.original, idx, foreignKey)}>
425
+ {row.getVisibleCells().map((cell: Cell<any, unknown>) => (
426
+ <TableCell
427
+ key={cell.id}
428
+ style={cell.column.columnDef.size ? { width: cell.column.columnDef.size } : undefined}
429
+ className="py-2"
430
+ >
431
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
432
+ </TableCell>
433
+ ))}
434
+ </TableRow>
435
+ ))}
436
+ </TableBody>
437
+ </Table>
358
438
  </div>
359
439
  )}
360
440
 
@@ -0,0 +1,28 @@
1
+ // Org runtime contexts — thread the org's display config (timezone, currency)
2
+ // to nested field/cell/relation renderers without prop-drilling. Lives in its
3
+ // own module (mirroring `image-url-context`) so any renderer can consume them
4
+ // without importing from `dialogs/dynamic-record`. That dialog imports
5
+ // `dynamic-relations` → `dynamic-relation`, so the relation table cannot import
6
+ // these contexts back from the dialog without a circular import — hence this
7
+ // standalone module is the single source of truth.
8
+ import { createContext, useContext } from 'react'
9
+
10
+ /**
11
+ * IANA timezone (e.g. the org's `America/Mexico_City`) used to render
12
+ * datetime/timestamp cells. `undefined` outside a provider → renderers fall
13
+ * back to the viewer's browser zone (legacy behaviour).
14
+ */
15
+ export const TimeZoneContext = createContext<string | undefined>(undefined)
16
+
17
+ /** Reads the nearest org timezone (undefined outside a provider). */
18
+ export const useTimeZone = () => useContext(TimeZoneContext)
19
+
20
+ /**
21
+ * Org ISO-4217 currency (org config, like the timezone) used as the fallback
22
+ * for money cells/fields that don't carry an explicit per-column currency.
23
+ * `undefined` outside a provider → renderers fall back to 'USD'.
24
+ */
25
+ export const CurrencyContext = createContext<string | undefined>(undefined)
26
+
27
+ /** Reads the nearest org currency (undefined outside a provider). */
28
+ export const useCurrency = () => useContext(CurrencyContext)