@asteby/metacore-runtime-react 18.4.0 → 18.6.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 +30 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +8 -8
- package/dist/dynamic-relation.d.ts.map +1 -1
- package/dist/dynamic-relation.js +51 -18
- package/dist/org-runtime-context.d.ts +17 -0
- package/dist/org-runtime-context.d.ts.map +1 -0
- package/dist/org-runtime-context.js +24 -0
- package/package.json +1 -1
- package/src/dialogs/dynamic-record.tsx +9 -7
- package/src/dynamic-relation.tsx +136 -56
- package/src/org-runtime-context.ts +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ed63683: Record dialog date fields use the real shadcn Calendar (react-day-picker) from
|
|
8
|
+
`@asteby/metacore-ui` instead of the dependency-free native `<input type="date">`
|
|
9
|
+
shim, and match datetime/timestamp(tz) types too. Empty/Go-zero dates
|
|
10
|
+
(0001-01-01) now show the "Seleccionar fecha" placeholder instead of
|
|
11
|
+
"31 de diciembre de 1".
|
|
12
|
+
|
|
13
|
+
## 18.5.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- d7c792d: Render one_to_many relations as a rich table (headers + currency/image/date/badge cells)
|
|
18
|
+
|
|
19
|
+
`OneToManyRelation` now renders the child list as a real metadata-driven table
|
|
20
|
+
using the same metacore-ui `<Table>` primitives and the exact
|
|
21
|
+
`makeDefaultGetDynamicColumns` cell factory as `<DynamicTable>`, instead of a
|
|
22
|
+
bare flex grid of unlabeled values. Line items now get column headers, money in
|
|
23
|
+
the org currency right-aligned (e.g. `100,00 MXN`), FK thumbnails + labels,
|
|
24
|
+
dates in the org timezone, status/option badges and creator names — matching
|
|
25
|
+
the main dynamic table. The inline edit (DynamicForm dialog) and delete
|
|
26
|
+
(AlertDialog) actions are preserved as a trailing actions column.
|
|
27
|
+
|
|
28
|
+
The org `timeZone`/`currency` contexts were extracted from
|
|
29
|
+
`dialogs/dynamic-record` into a shared `org-runtime-context` module so the
|
|
30
|
+
relation table can consume them without a circular import. `ManyToManyRelation`
|
|
31
|
+
is unchanged.
|
|
32
|
+
|
|
3
33
|
## 18.4.0
|
|
4
34
|
|
|
5
35
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AA6C1C,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
|
|
@@ -12,9 +12,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
// SDK stays transport- and host-agnostic.
|
|
13
13
|
import { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
14
14
|
import { useTranslation } from 'react-i18next';
|
|
15
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Input, Textarea, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Skeleton, Badge, Popover, PopoverContent, PopoverTrigger, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from '@asteby/metacore-ui/primitives';
|
|
15
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Input, Textarea, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Skeleton, Badge, Popover, PopoverContent, PopoverTrigger, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Calendar, } from '@asteby/metacore-ui/primitives';
|
|
16
16
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
17
|
-
import { Calendar } from './_primitives';
|
|
18
17
|
import { toast } from 'sonner';
|
|
19
18
|
import { format, parseISO } from 'date-fns';
|
|
20
19
|
import { es } from 'date-fns/locale';
|
|
@@ -28,6 +27,7 @@ import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
|
|
|
28
27
|
import { humanizeToken } from '../dynamic-columns-helpers';
|
|
29
28
|
import { formatDateCell } from '../dynamic-columns';
|
|
30
29
|
import { ImageUrlContext, identityImageUrl } from '../image-url-context';
|
|
30
|
+
import { TimeZoneContext, CurrencyContext } from '../org-runtime-context';
|
|
31
31
|
// localizedModelName resolves the (possibly addon-i18n) model name: prefer the
|
|
32
32
|
// translated titleKey, fall back to the backend-provided raw title.
|
|
33
33
|
function localizedModelName(meta, t) {
|
|
@@ -157,10 +157,6 @@ const MODE_CONFIG = {
|
|
|
157
157
|
// Context threading host runtime values to nested field components (uploads,
|
|
158
158
|
// image leads, tz-aware dates) without prop-drilling through every renderer.
|
|
159
159
|
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
160
|
// Money-key heuristic mirroring the backend's `inferDisplayCellStyle`: lets the
|
|
165
161
|
// dialog format obvious money fields as currency even when the backend hasn't
|
|
166
162
|
// stamped `cellStyle:'currency'` yet. Case-insensitive; matches a key that
|
|
@@ -583,9 +579,13 @@ function EditField({ field, value, onChange }) {
|
|
|
583
579
|
if (field.type === 'color') {
|
|
584
580
|
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "color", value: value || '#6366f1', onChange: (e) => onChange(e.target.value), className: "h-9 w-14 cursor-pointer rounded-md border p-1" }), _jsx(Input, { value: value || '', onChange: (e) => onChange(e.target.value), placeholder: "#6366f1", className: "flex-1 h-9" })] }));
|
|
585
581
|
}
|
|
586
|
-
if (field.type === 'date') {
|
|
582
|
+
if (field.type === 'date' || field.type === 'datetime' || field.type === 'timestamp' || field.type === 'timestamptz') {
|
|
587
583
|
const dateValue = value ? (typeof value === 'string' ? parseISO(value) : new Date(value)) : undefined;
|
|
588
|
-
|
|
584
|
+
// Treat the Go zero-time (0001-01-01) as empty so an unset date shows the
|
|
585
|
+
// placeholder instead of "31 de diciembre de 1".
|
|
586
|
+
const validDate = dateValue && !isNaN(dateValue.getTime()) && dateValue.getFullYear() > 1
|
|
587
|
+
? dateValue
|
|
588
|
+
: undefined;
|
|
589
589
|
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", className: cn("w-full justify-start text-left font-normal h-9", !validDate && "text-muted-foreground"), children: [_jsx(CalendarIcon, { className: "mr-2 h-4 w-4" }), validDate
|
|
590
590
|
? format(validDate, 'PPP', { locale: es })
|
|
591
591
|
: "Seleccionar fecha"] }) }), _jsx(PopoverContent, { className: "w-auto p-0", align: "start", children: _jsx(Calendar, { mode: "single", selected: validDate, onSelect: (date) => onChange(date ? format(date, 'yyyy-MM-dd') : ''), locale: es }) })] }));
|
|
@@ -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":"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"}
|
package/dist/dynamic-relation.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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,
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
@@ -40,9 +40,9 @@ import {
|
|
|
40
40
|
CommandInput,
|
|
41
41
|
CommandItem,
|
|
42
42
|
CommandList,
|
|
43
|
+
Calendar,
|
|
43
44
|
} from '@asteby/metacore-ui/primitives'
|
|
44
45
|
import { cn } from '@asteby/metacore-ui/lib'
|
|
45
|
-
import { Calendar } from './_primitives'
|
|
46
46
|
import { toast } from 'sonner'
|
|
47
47
|
import { format, parseISO } from 'date-fns'
|
|
48
48
|
import { es } from 'date-fns/locale'
|
|
@@ -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
|
|
@@ -1111,9 +1108,14 @@ function EditField({ field, value, onChange }: {
|
|
|
1111
1108
|
)
|
|
1112
1109
|
}
|
|
1113
1110
|
|
|
1114
|
-
if (field.type === 'date') {
|
|
1111
|
+
if (field.type === 'date' || field.type === 'datetime' || field.type === 'timestamp' || field.type === 'timestamptz') {
|
|
1115
1112
|
const dateValue = value ? (typeof value === 'string' ? parseISO(value) : new Date(value)) : undefined
|
|
1116
|
-
|
|
1113
|
+
// Treat the Go zero-time (0001-01-01) as empty so an unset date shows the
|
|
1114
|
+
// placeholder instead of "31 de diciembre de 1".
|
|
1115
|
+
const validDate =
|
|
1116
|
+
dateValue && !isNaN(dateValue.getTime()) && dateValue.getFullYear() > 1
|
|
1117
|
+
? dateValue
|
|
1118
|
+
: undefined
|
|
1117
1119
|
|
|
1118
1120
|
return (
|
|
1119
1121
|
<Popover>
|
package/src/dynamic-relation.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
>
|
|
308
|
-
|
|
309
|
-
{
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
<
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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)
|