@asteby/metacore-runtime-react 18.0.0 → 18.2.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 +47 -0
- package/dist/dialogs/dynamic-record.d.ts +86 -2
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +306 -88
- package/dist/dynamic-columns-shim.d.ts +1 -1
- package/dist/dynamic-columns-shim.d.ts.map +1 -1
- package/dist/dynamic-columns.d.ts +11 -1
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +46 -3
- package/dist/dynamic-select-field.d.ts +21 -0
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +2 -2
- package/dist/dynamic-table.d.ts +8 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/format-date-cell.test.ts +27 -0
- package/src/dialogs/dynamic-record.tsx +476 -114
- package/src/dynamic-columns-shim.ts +1 -0
- package/src/dynamic-columns.tsx +46 -1
- package/src/dynamic-select-field.tsx +2 -2
- package/src/dynamic-table.tsx +10 -2
- package/src/index.ts +2 -1
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// DynamicRecordDialog — renders a create/edit/view modal for a model based
|
|
3
|
-
// on metadata fetched from `/metadata/modal/:model`.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// on metadata fetched from `/metadata/modal/:model`. This is the single,
|
|
4
|
+
// SDK-owned source of truth for declarative record rendering (the ops fork was
|
|
5
|
+
// consolidated back into here): tz-aware dates, FK image/label leads in both
|
|
6
|
+
// view and edit, resolved relation/user-object labels (never raw JSON), nil-UUID
|
|
7
|
+
// elision, pro option color/icon badges, and one_to_many child panels.
|
|
8
|
+
//
|
|
9
|
+
// Host-owned infra that was referenced by alias (axios client, branch store)
|
|
10
|
+
// flows through <ApiProvider> from runtime-react. Host-specific runtime values —
|
|
11
|
+
// the image-url resolver and the org IANA timezone — are passed as props so the
|
|
12
|
+
// SDK stays transport- and host-agnostic.
|
|
6
13
|
import { createContext, useContext, useEffect, useRef, useState } from 'react';
|
|
14
|
+
import { useTranslation } from 'react-i18next';
|
|
7
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';
|
|
8
16
|
import { cn } from '@asteby/metacore-ui/lib';
|
|
9
17
|
import { Calendar } from './_primitives';
|
|
@@ -12,30 +20,103 @@ import { format, parseISO } from 'date-fns';
|
|
|
12
20
|
import { es } from 'date-fns/locale';
|
|
13
21
|
import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon } from 'lucide-react';
|
|
14
22
|
import { useApi } from '../api-context';
|
|
15
|
-
import { DynamicSelectField } from '../dynamic-select-field';
|
|
23
|
+
import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field';
|
|
24
|
+
import { DynamicRelations } from '../dynamic-relations';
|
|
25
|
+
import { useOptionsResolver } from '../use-options-resolver';
|
|
16
26
|
import { getFieldRef } from '../dynamic-form-schema';
|
|
17
|
-
import { normalizeNilUuid } from '../nil-uuid';
|
|
27
|
+
import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
|
|
18
28
|
import { humanizeToken } from '../dynamic-columns-helpers';
|
|
29
|
+
import { formatDateCell } from '../dynamic-columns';
|
|
30
|
+
const identityImageUrl = (p) => p ?? '';
|
|
31
|
+
// localizedModelName resolves the (possibly addon-i18n) model name: prefer the
|
|
32
|
+
// translated titleKey, fall back to the backend-provided raw title.
|
|
33
|
+
function localizedModelName(meta, t) {
|
|
34
|
+
if (meta.titleKey && t(meta.titleKey) !== meta.titleKey)
|
|
35
|
+
return t(meta.titleKey);
|
|
36
|
+
return meta.title || '';
|
|
37
|
+
}
|
|
19
38
|
function resolvePath(obj, path) {
|
|
20
39
|
return path.split('.').reduce((acc, part) => acc?.[part], obj);
|
|
21
40
|
}
|
|
41
|
+
// objectLabel pulls a human label off a resolved relation/user object the
|
|
42
|
+
// backend serves: `{value,label}` (FK sibling), `{name,...}` (user object such
|
|
43
|
+
// as created_by), or `{title}`. Returns undefined for plain/empty objects.
|
|
44
|
+
function objectLabel(value) {
|
|
45
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
46
|
+
return undefined;
|
|
47
|
+
const label = value.label ?? value.name ?? value.title;
|
|
48
|
+
if (label != null && label !== '')
|
|
49
|
+
return String(label);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
// pickImage reads an image-ish path off a resolved object (FK sibling, user).
|
|
53
|
+
function pickImage(value) {
|
|
54
|
+
if (!value || typeof value !== 'object')
|
|
55
|
+
return undefined;
|
|
56
|
+
const img = value.image ?? value.avatar ?? value.logo ?? value.thumbnail;
|
|
57
|
+
return typeof img === 'string' && img !== '' ? img : undefined;
|
|
58
|
+
}
|
|
59
|
+
// relationSiblingValue reads the resolved relation the table served alongside an
|
|
60
|
+
// FK column. A field `category_id` (search/dynamic_select/ref) ships a sibling
|
|
61
|
+
// `record.category = {value,label,image?}` (or a bare string/{name}); returns
|
|
62
|
+
// the raw sibling (object or string) so the caller can extract label + image.
|
|
63
|
+
function relationSiblingValue(field, record) {
|
|
64
|
+
if (!record)
|
|
65
|
+
return undefined;
|
|
66
|
+
const candidates = [];
|
|
67
|
+
const ref = getFieldRef(field);
|
|
68
|
+
if (ref)
|
|
69
|
+
candidates.push(ref);
|
|
70
|
+
if (typeof field.key === 'string' && field.key.endsWith('_id'))
|
|
71
|
+
candidates.push(field.key.slice(0, -3));
|
|
72
|
+
for (const key of candidates) {
|
|
73
|
+
const sib = record[key];
|
|
74
|
+
if (sib === undefined || sib === null)
|
|
75
|
+
continue;
|
|
76
|
+
if (typeof sib === 'string') {
|
|
77
|
+
if (sib === '' || isNilUuid(sib))
|
|
78
|
+
continue;
|
|
79
|
+
return sib;
|
|
80
|
+
}
|
|
81
|
+
if (typeof sib === 'object')
|
|
82
|
+
return sib;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
// servedOption matches a field's served option list (enum/select with
|
|
87
|
+
// {value,label,color,icon,image}) against the current value.
|
|
88
|
+
function servedOption(field, value) {
|
|
89
|
+
if (!field.options?.length)
|
|
90
|
+
return undefined;
|
|
91
|
+
return field.options.find(o => o.value === String(value ?? ''));
|
|
92
|
+
}
|
|
93
|
+
// createdBySibling reads the resolver object the backend serves for the
|
|
94
|
+
// auto-injected `created_by` avatar column: {name, avatar, email}.
|
|
95
|
+
function createdBySibling(value, record) {
|
|
96
|
+
const obj = (value && typeof value === 'object' ? value : undefined) ?? record?.created_by;
|
|
97
|
+
if (obj && typeof obj === 'object' && (obj.name || obj.avatar || obj.email))
|
|
98
|
+
return obj;
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
// isRelationField — a field that resolves to another row (so view renders a lead
|
|
102
|
+
// + label and edit renders the searchable picker).
|
|
103
|
+
function isRelationField(field) {
|
|
104
|
+
return (field.type === 'search' ||
|
|
105
|
+
field.type === 'dynamic_select' ||
|
|
106
|
+
field.widget === 'dynamic_select' ||
|
|
107
|
+
!!getFieldRef(field) ||
|
|
108
|
+
!!field.searchEndpoint);
|
|
109
|
+
}
|
|
22
110
|
function formatDisplayValue(rawValue, field) {
|
|
23
111
|
// Unset nullable FK serialized as the nil UUID renders as empty, not zeros.
|
|
24
112
|
const value = normalizeNilUuid(rawValue);
|
|
25
113
|
if (value === null || value === undefined || value === '')
|
|
26
114
|
return '—';
|
|
115
|
+
const objLabel = objectLabel(value);
|
|
116
|
+
if (objLabel !== undefined)
|
|
117
|
+
return objLabel;
|
|
27
118
|
if (field.type === 'boolean' || typeof value === 'boolean')
|
|
28
119
|
return value ? 'Sí' : 'No';
|
|
29
|
-
if (field.type === 'date') {
|
|
30
|
-
try {
|
|
31
|
-
return new Date(value).toLocaleDateString('es-MX', {
|
|
32
|
-
day: 'numeric', month: 'long', year: 'numeric',
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return String(value);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
120
|
if (field.type === 'select' && field.options?.length) {
|
|
40
121
|
const match = field.options.find(o => o.value === String(value));
|
|
41
122
|
// Matched option label wins (localized); humanize the raw token only
|
|
@@ -46,31 +127,43 @@ function formatDisplayValue(rawValue, field) {
|
|
|
46
127
|
}
|
|
47
128
|
const MODE_CONFIG = {
|
|
48
129
|
create: {
|
|
49
|
-
getTitle: (meta) =>
|
|
130
|
+
getTitle: (meta, t) => {
|
|
131
|
+
const name = localizedModelName(meta, t);
|
|
132
|
+
return name ? `Crear ${name}` : (meta.createTitle || meta.title || 'Nuevo registro');
|
|
133
|
+
},
|
|
50
134
|
description: 'Completa los campos para crear un nuevo registro.',
|
|
51
135
|
submitLabel: 'Crear',
|
|
52
136
|
submittingLabel: 'Creando...',
|
|
53
137
|
cancelLabel: 'Cancelar',
|
|
54
138
|
},
|
|
55
139
|
edit: {
|
|
56
|
-
getTitle: (meta) =>
|
|
140
|
+
getTitle: (meta, t) => {
|
|
141
|
+
const name = localizedModelName(meta, t);
|
|
142
|
+
return name ? `Editar ${name}` : (meta.editTitle || meta.title || 'Editar registro');
|
|
143
|
+
},
|
|
57
144
|
description: 'Modifica los campos y guarda los cambios.',
|
|
58
145
|
submitLabel: 'Guardar cambios',
|
|
59
146
|
submittingLabel: 'Guardando...',
|
|
60
147
|
cancelLabel: 'Cancelar',
|
|
61
148
|
},
|
|
62
149
|
view: {
|
|
63
|
-
getTitle: (meta) => meta.title || 'Ver registro',
|
|
150
|
+
getTitle: (meta, t) => localizedModelName(meta, t) || meta.title || 'Ver registro',
|
|
64
151
|
description: 'Información detallada del registro.',
|
|
65
152
|
submitLabel: '',
|
|
66
153
|
submittingLabel: '',
|
|
67
154
|
cancelLabel: 'Cerrar',
|
|
68
155
|
},
|
|
69
156
|
};
|
|
157
|
+
// Context threading host runtime values to nested field components (uploads,
|
|
158
|
+
// image leads, tz-aware dates) without prop-drilling through every renderer.
|
|
70
159
|
const ModelContext = createContext('');
|
|
71
|
-
|
|
160
|
+
const ImageUrlContext = createContext(identityImageUrl);
|
|
161
|
+
const TimeZoneContext = createContext(undefined);
|
|
162
|
+
export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, }) {
|
|
72
163
|
const api = useApi();
|
|
164
|
+
const { t } = useTranslation();
|
|
73
165
|
const [modalMeta, setModalMeta] = useState(schema ? schema : null);
|
|
166
|
+
const [relations, setRelations] = useState([]);
|
|
74
167
|
const [record, setRecord] = useState(null);
|
|
75
168
|
const [formValues, setFormValues] = useState({});
|
|
76
169
|
const [loading, setLoading] = useState(false);
|
|
@@ -80,14 +173,39 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
80
173
|
const isView = mode === 'view';
|
|
81
174
|
const isEditable = mode === 'create' || mode === 'edit';
|
|
82
175
|
const config = MODE_CONFIG[mode];
|
|
176
|
+
// ── Fetch metadata + record when dialog opens ──────────────────────────
|
|
83
177
|
useEffect(() => {
|
|
84
178
|
if (!open)
|
|
85
179
|
return;
|
|
86
180
|
if (!isCreate && !recordId)
|
|
87
181
|
return;
|
|
88
182
|
let cancelled = false;
|
|
183
|
+
// Seed instantly from the row the table already has so view/edit render
|
|
184
|
+
// without a spinner. The list row carries the pro siblings (resolved
|
|
185
|
+
// relation, served options, image url) the table cells used.
|
|
186
|
+
const seed = !isCreate && initialRecord ? initialRecord : null;
|
|
187
|
+
if (seed)
|
|
188
|
+
setRecord(seed);
|
|
189
|
+
const seedForm = (meta, rec) => {
|
|
190
|
+
const initial = {};
|
|
191
|
+
for (const field of meta.fields ?? []) {
|
|
192
|
+
initial[field.key] = resolvePath(rec, field.key) ?? field.defaultValue ?? '';
|
|
193
|
+
}
|
|
194
|
+
setFormValues(initial);
|
|
195
|
+
};
|
|
196
|
+
// A field value is "missing" from the seed row when the list omitted that
|
|
197
|
+
// column. Sibling pro fields aren't form fields, so we only check the
|
|
198
|
+
// declared field keys.
|
|
199
|
+
const seedIsComplete = (meta, rec) => (meta.fields ?? []).every(f => {
|
|
200
|
+
if (f.hidden)
|
|
201
|
+
return true;
|
|
202
|
+
const v = resolvePath(rec, f.key);
|
|
203
|
+
return v !== undefined;
|
|
204
|
+
});
|
|
89
205
|
const load = async () => {
|
|
90
|
-
|
|
206
|
+
// Only show the skeleton when we have nothing to render yet.
|
|
207
|
+
if (!seed)
|
|
208
|
+
setLoading(true);
|
|
91
209
|
try {
|
|
92
210
|
let meta = schema ? schema : null;
|
|
93
211
|
if (!meta) {
|
|
@@ -106,8 +224,14 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
106
224
|
: field.defaultValue) ?? '';
|
|
107
225
|
}
|
|
108
226
|
setFormValues(initial);
|
|
227
|
+
return;
|
|
109
228
|
}
|
|
110
|
-
|
|
229
|
+
// Render immediately from the seed row.
|
|
230
|
+
if (seed && meta)
|
|
231
|
+
seedForm(meta, seed);
|
|
232
|
+
// Only hit the record endpoint if the seed is absent or missing
|
|
233
|
+
// some declared field — keeps the modal instant for full rows.
|
|
234
|
+
if (!seed || (meta && !seedIsComplete(meta, seed))) {
|
|
111
235
|
const recordEndpoint = endpoint
|
|
112
236
|
? `${endpoint}/${recordId}`
|
|
113
237
|
: `/dynamic/${model}/${recordId}`;
|
|
@@ -115,17 +239,18 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
115
239
|
if (cancelled)
|
|
116
240
|
return;
|
|
117
241
|
const rec = recRes.data?.data ?? recRes.data;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
242
|
+
// Merge so the fetched record fills gaps without dropping the
|
|
243
|
+
// table's pro siblings (the detail endpoint may omit them).
|
|
244
|
+
const merged = seed ? { ...seed, ...rec } : rec;
|
|
245
|
+
setRecord(merged);
|
|
246
|
+
if (meta)
|
|
247
|
+
seedForm(meta, merged);
|
|
124
248
|
}
|
|
125
249
|
}
|
|
126
250
|
catch (err) {
|
|
127
251
|
console.error('[DynamicRecordDialog] load error:', err);
|
|
128
|
-
|
|
252
|
+
if (!seed)
|
|
253
|
+
toast.error('Error al cargar los datos');
|
|
129
254
|
}
|
|
130
255
|
finally {
|
|
131
256
|
if (!cancelled)
|
|
@@ -134,14 +259,51 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
134
259
|
};
|
|
135
260
|
load();
|
|
136
261
|
return () => { cancelled = true; };
|
|
137
|
-
|
|
262
|
+
// initialRecord intentionally omitted: the row identity is captured per open
|
|
263
|
+
// via recordId; re-seeding mid-open would clobber edits.
|
|
264
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
265
|
+
}, [open, recordId, model, endpoint, isCreate, schema]);
|
|
266
|
+
// Reset when closed
|
|
138
267
|
useEffect(() => {
|
|
139
268
|
if (!open) {
|
|
140
269
|
setModalMeta(null);
|
|
270
|
+
setRelations([]);
|
|
141
271
|
setRecord(null);
|
|
142
272
|
setFormValues({});
|
|
143
273
|
}
|
|
144
274
|
}, [open]);
|
|
275
|
+
// Fetch the model's declared one_to_many/many_to_many edges so view AND edit
|
|
276
|
+
// show child records (e.g. a sales order's line items) below the scalar
|
|
277
|
+
// fields. The modal form is driven by MODAL metadata (fields); relations live
|
|
278
|
+
// on TABLE metadata, hence the separate fetch. Skipped on create (no parent
|
|
279
|
+
// record yet). View renders them read-only; edit lets the user add/edit/delete.
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (!open || mode === 'create' || !recordId) {
|
|
282
|
+
setRelations([]);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
let cancelled = false;
|
|
286
|
+
api.get(`/metadata/table/${model}`)
|
|
287
|
+
.then(res => {
|
|
288
|
+
if (cancelled)
|
|
289
|
+
return;
|
|
290
|
+
const meta = res.data?.data ?? res.data;
|
|
291
|
+
const rels = Array.isArray(meta?.relations) ? meta.relations : [];
|
|
292
|
+
// Localize each panel header: the backend serves `label` as an
|
|
293
|
+
// i18n key (addon bundle, loaded live) and the SDK renders it verbatim.
|
|
294
|
+
setRelations(rels.map(rel => ({
|
|
295
|
+
...rel,
|
|
296
|
+
label: rel.label && t(rel.label) !== rel.label
|
|
297
|
+
? t(rel.label)
|
|
298
|
+
: rel.label || rel.name,
|
|
299
|
+
})));
|
|
300
|
+
})
|
|
301
|
+
.catch(() => {
|
|
302
|
+
if (!cancelled)
|
|
303
|
+
setRelations([]);
|
|
304
|
+
});
|
|
305
|
+
return () => { cancelled = true; };
|
|
306
|
+
}, [open, mode, model, recordId, api, t]);
|
|
145
307
|
const handleSubmit = async (e) => {
|
|
146
308
|
e?.preventDefault();
|
|
147
309
|
if (!modalMeta)
|
|
@@ -157,16 +319,16 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
157
319
|
setSaving(true);
|
|
158
320
|
try {
|
|
159
321
|
if (isCreate && onCreate) {
|
|
160
|
-
await onCreate(formValues);
|
|
161
|
-
toast.success('Registro creado correctamente');
|
|
162
|
-
onSaved?.();
|
|
322
|
+
const created = await onCreate(formValues);
|
|
323
|
+
toast.success(modalMeta?.messages?.created || 'Registro creado correctamente');
|
|
324
|
+
onSaved?.(created ?? undefined);
|
|
163
325
|
onOpenChange(false);
|
|
164
326
|
return;
|
|
165
327
|
}
|
|
166
328
|
if (!isCreate && recordId && onUpdate) {
|
|
167
|
-
await onUpdate(String(recordId), formValues);
|
|
168
|
-
toast.success('Guardado correctamente');
|
|
169
|
-
onSaved?.();
|
|
329
|
+
const updated = await onUpdate(String(recordId), formValues);
|
|
330
|
+
toast.success(modalMeta?.messages?.updated || 'Guardado correctamente');
|
|
331
|
+
onSaved?.(updated ?? undefined);
|
|
170
332
|
onOpenChange(false);
|
|
171
333
|
return;
|
|
172
334
|
}
|
|
@@ -182,8 +344,13 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
182
344
|
res = await api.put(updateEndpoint, formValues);
|
|
183
345
|
}
|
|
184
346
|
if (res.data?.success !== false) {
|
|
185
|
-
|
|
186
|
-
|
|
347
|
+
// Prefer the addon's localized message (modal metadata), then a
|
|
348
|
+
// localized fallback. NOT res.data.message — the dynamic CRUD
|
|
349
|
+
// endpoint returns a raw English string that would leak into the toast.
|
|
350
|
+
toast.success(modalMeta?.messages?.[isCreate ? 'created' : 'updated']
|
|
351
|
+
|| (isCreate ? 'Registro creado correctamente' : 'Guardado correctamente'));
|
|
352
|
+
// Hand the persisted record back so callers can auto-select it.
|
|
353
|
+
onSaved?.(res.data?.data ?? res.data ?? undefined);
|
|
187
354
|
onOpenChange(false);
|
|
188
355
|
}
|
|
189
356
|
else {
|
|
@@ -213,7 +380,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
213
380
|
setDeleting(false);
|
|
214
381
|
}
|
|
215
382
|
};
|
|
216
|
-
const title = modalMeta ? config.getTitle(modalMeta) : '';
|
|
383
|
+
const title = modalMeta ? config.getTitle(modalMeta, t) : '';
|
|
217
384
|
const visibleFields = modalMeta?.fields?.filter(f => {
|
|
218
385
|
if (f.hidden)
|
|
219
386
|
return false;
|
|
@@ -221,10 +388,10 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
221
388
|
return false;
|
|
222
389
|
return true;
|
|
223
390
|
}) ?? [];
|
|
224
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
391
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsxs(TimeZoneContext.Provider, { value: timeZone, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: [visibleFields.map(field => {
|
|
392
|
+
const isFullWidth = field.type === 'textarea';
|
|
393
|
+
return (_jsx("div", { className: isFullWidth ? 'sm:col-span-2' : '', children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, onChange: val => setFormValues((prev) => ({ ...prev, [field.key]: val })) }) }, field.key));
|
|
394
|
+
}), record?.external_url && (_jsx("div", { className: "sm:col-span-2", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit' }) }))] }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel }), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }));
|
|
228
395
|
}
|
|
229
396
|
function LoadingSkeleton() {
|
|
230
397
|
return (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-9 w-full" })] }, i))) }));
|
|
@@ -233,12 +400,75 @@ function FieldRow({ field, record, value, mode, onChange }) {
|
|
|
233
400
|
const isReadonly = field.readonly || mode === 'view';
|
|
234
401
|
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: [field.label, field.required && mode !== 'view' && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), isReadonly ? (_jsx(ViewValue, { field: field, value: value, record: record })) : (_jsx(EditField, { field: field, value: value, onChange: onChange }))] }));
|
|
235
402
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
403
|
+
// RelationViewValue — read-only FK lead. Resolves the relation's label + image
|
|
404
|
+
// from (1) the sibling object the table served, then (2) the canonical options
|
|
405
|
+
// endpoint, and renders an OptionLead (thumbnail / icon / color dot) + label.
|
|
406
|
+
function RelationViewValue({ field, value, record }) {
|
|
407
|
+
const getImageUrl = useContext(ImageUrlContext);
|
|
408
|
+
const sib = relationSiblingValue(field, record);
|
|
409
|
+
const sibLabel = typeof sib === 'string' ? sib : objectLabel(sib);
|
|
410
|
+
const sibImage = pickImage(sib);
|
|
411
|
+
// The raw FK id, tolerating an inline resolved object as the value itself.
|
|
412
|
+
const rawVal = value && typeof value === 'object' ? (value.value ?? value.id) : value;
|
|
413
|
+
const inlineLabel = sibLabel ?? objectLabel(value);
|
|
414
|
+
const inlineImage = sibImage ?? pickImage(value);
|
|
415
|
+
const fieldRef = getFieldRef(field);
|
|
416
|
+
// Only resolve over the network when we still lack both label and image and
|
|
417
|
+
// there is something to look up.
|
|
418
|
+
const needResolve = !inlineLabel && !inlineImage && !!(fieldRef || field.searchEndpoint) && rawVal != null && rawVal !== '';
|
|
419
|
+
const { options } = useOptionsResolver({
|
|
420
|
+
modelKey: '',
|
|
421
|
+
fieldKey: 'id',
|
|
422
|
+
ref: fieldRef,
|
|
423
|
+
endpoint: fieldRef ? undefined : field.searchEndpoint,
|
|
424
|
+
query: '',
|
|
425
|
+
limit: 50,
|
|
426
|
+
enabled: needResolve,
|
|
427
|
+
});
|
|
428
|
+
const resolved = options.find(o => String(o.id) === String(rawVal));
|
|
429
|
+
const label = inlineLabel ??
|
|
430
|
+
resolved?.label ??
|
|
431
|
+
(rawVal != null && rawVal !== '' && !isNilUuid(rawVal) ? String(rawVal) : undefined);
|
|
432
|
+
const image = inlineImage ?? resolved?.image ?? undefined;
|
|
433
|
+
if (!label && !image) {
|
|
434
|
+
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
435
|
+
}
|
|
436
|
+
const lead = {
|
|
437
|
+
image: image ? getImageUrl(image) : null,
|
|
438
|
+
color: resolved?.color ?? null,
|
|
439
|
+
icon: resolved?.icon ?? null,
|
|
440
|
+
};
|
|
441
|
+
return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(OptionLead, { option: lead, size: 24 }), _jsx("span", { className: "text-sm", children: label ?? '—' })] }));
|
|
442
|
+
}
|
|
443
|
+
export function ViewValue({ field, value: rawValue, record, getImageUrl: getImageUrlProp, timeZone: timeZoneProp, }) {
|
|
444
|
+
const ctxImageUrl = useContext(ImageUrlContext);
|
|
445
|
+
const ctxTimeZone = useContext(TimeZoneContext);
|
|
446
|
+
const getImageUrl = getImageUrlProp ?? ctxImageUrl;
|
|
447
|
+
const timeZone = timeZoneProp ?? ctxTimeZone;
|
|
448
|
+
// created_by / avatar resolver sibling → name (+ avatar) instead of "—".
|
|
449
|
+
if (field.type === 'avatar' || field.key === 'created_by' || field.key === 'created_by_id') {
|
|
450
|
+
const user = createdBySibling(rawValue, record);
|
|
451
|
+
if (user) {
|
|
452
|
+
return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [user.avatar ? (_jsx("img", { src: getImageUrl(String(user.avatar)), alt: user.name ?? '', className: "h-6 w-6 rounded-full object-cover" })) : null, _jsx("span", { className: "text-sm", children: user.name ?? user.email ?? '—' })] }));
|
|
453
|
+
}
|
|
454
|
+
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
455
|
+
}
|
|
456
|
+
// Nil/zero UUID (unset nullable FK serialized as all-zeros) → empty marker.
|
|
457
|
+
if (isNilUuid(rawValue)) {
|
|
458
|
+
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
459
|
+
}
|
|
239
460
|
const value = normalizeNilUuid(rawValue);
|
|
240
|
-
|
|
241
|
-
|
|
461
|
+
// Relation (search / dynamic_select / ref / any *_id) → resolved thumbnail +
|
|
462
|
+
// label. The *_id catch-all covers plain-typed FK columns not tagged as a
|
|
463
|
+
// relation field.
|
|
464
|
+
if (isRelationField(field) || (typeof field.key === 'string' && field.key.endsWith('_id'))) {
|
|
465
|
+
return _jsx(RelationViewValue, { field: field, value: value, record: record });
|
|
466
|
+
}
|
|
467
|
+
// The value is itself a resolved object the backend served inline — render
|
|
468
|
+
// its label/name, never the raw JSON.
|
|
469
|
+
const inlineLabel = objectLabel(value);
|
|
470
|
+
if (inlineLabel !== undefined) {
|
|
471
|
+
return _jsx("p", { className: "text-sm py-1", children: inlineLabel });
|
|
242
472
|
}
|
|
243
473
|
if (field.type === 'boolean' || typeof value === 'boolean') {
|
|
244
474
|
return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, disabled: true }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
|
|
@@ -247,16 +477,31 @@ function ViewValue({ field, value: rawValue }) {
|
|
|
247
477
|
return value ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "h-5 w-5 rounded-full border shadow-sm", style: { backgroundColor: value } }), _jsx("span", { className: "text-sm", children: value })] })) : (_jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "-" }));
|
|
248
478
|
}
|
|
249
479
|
if (field.type === 'image') {
|
|
250
|
-
return value ? (_jsx("img", { src: value, alt: field.label, className: "h-16 w-16 rounded-lg object-cover border" })) : (_jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "Sin imagen" }));
|
|
480
|
+
return value ? (_jsx("img", { src: getImageUrl(String(value)), alt: field.label, className: "h-16 w-16 rounded-lg object-cover border" })) : (_jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "Sin imagen" }));
|
|
251
481
|
}
|
|
252
482
|
if (field.type === 'url' && value) {
|
|
253
483
|
return (_jsx("a", { href: value, target: "_blank", rel: "noreferrer", className: "text-sm text-primary hover:underline truncate", children: value }));
|
|
254
484
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
485
|
+
// Date/datetime/timestamp → tz-aware format. `date` pins to UTC (calendar
|
|
486
|
+
// day); instants render in the org timezone with a full-precision tooltip.
|
|
487
|
+
if (field.type === 'date' || field.type === 'datetime' || field.type === 'timestamp') {
|
|
488
|
+
const renderAs = field.type === 'date' ? 'date' : field.type;
|
|
489
|
+
const formatted = formatDateCell(value, renderAs, es, timeZone);
|
|
490
|
+
if (formatted) {
|
|
491
|
+
return (_jsx("p", { className: "text-sm py-1", title: formatted.title, children: formatted.display }));
|
|
259
492
|
}
|
|
493
|
+
return _jsx("p", { className: "text-sm py-1 text-muted-foreground", children: "\u2014" });
|
|
494
|
+
}
|
|
495
|
+
// Enum/option field with served options → colored/iconed badge using the
|
|
496
|
+
// served label (e.g. "Almacenable" instead of "storable").
|
|
497
|
+
const opt = servedOption(field, value);
|
|
498
|
+
if (opt) {
|
|
499
|
+
const lead = {
|
|
500
|
+
image: opt.image ? getImageUrl(opt.image) : null,
|
|
501
|
+
color: opt.color ?? null,
|
|
502
|
+
icon: opt.icon ?? null,
|
|
503
|
+
};
|
|
504
|
+
return (_jsxs(Badge, { variant: "secondary", className: "w-fit flex items-center gap-1", style: opt.color && !opt.icon ? { backgroundColor: opt.color, color: '#fff', borderColor: 'transparent' } : undefined, children: [_jsx(OptionLead, { option: lead, size: 16 }), opt.label] }));
|
|
260
505
|
}
|
|
261
506
|
const display = formatDisplayValue(value, field);
|
|
262
507
|
if (field.type === 'textarea') {
|
|
@@ -272,17 +517,14 @@ function EditField({ field, value, onChange }) {
|
|
|
272
517
|
return (_jsx(Textarea, { value: value ?? '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, rows: 4 }));
|
|
273
518
|
}
|
|
274
519
|
// Media widgets: the kernel may serve an explicit `widget: 'upload'` (or the
|
|
275
|
-
// `image` type) for a file/photo column.
|
|
276
|
-
// that POSTs to the host upload endpoint — same control as the Brand logo.
|
|
520
|
+
// `image` type) for a file/photo column.
|
|
277
521
|
if (field.type === 'image' || field.widget === 'upload') {
|
|
278
522
|
return _jsx(ImageUploadField, { field: field, value: value, onChange: onChange });
|
|
279
523
|
}
|
|
280
524
|
// FK columns: a `ref` (kernel-derived belongs_to target) or an explicit
|
|
281
|
-
// `widget: 'dynamic_select'` renders the async searchable picker
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
// `options` are handled by the enum <Select> branch below; a ref column does
|
|
285
|
-
// not ship inline options, so this never shadows a static enum.
|
|
525
|
+
// `widget: 'dynamic_select'` renders the SDK's async searchable picker — with
|
|
526
|
+
// option thumbnails and the inline-create "+" — against /api/options/<ref>.
|
|
527
|
+
// Static inline `options` are handled by the enum <Select> branch below.
|
|
286
528
|
if ((getFieldRef(field) || field.widget === 'dynamic_select') && !field.options?.length) {
|
|
287
529
|
return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange });
|
|
288
530
|
}
|
|
@@ -315,6 +557,7 @@ function EditField({ field, value, onChange }) {
|
|
|
315
557
|
function ImageUploadField({ field: _field, value, onChange }) {
|
|
316
558
|
const api = useApi();
|
|
317
559
|
const model = useContext(ModelContext);
|
|
560
|
+
const getImageUrl = useContext(ImageUrlContext);
|
|
318
561
|
const [uploading, setUploading] = useState(false);
|
|
319
562
|
const inputRef = useRef(null);
|
|
320
563
|
async function handleFile(e) {
|
|
@@ -340,7 +583,7 @@ function ImageUploadField({ field: _field, value, onChange }) {
|
|
|
340
583
|
inputRef.current.value = '';
|
|
341
584
|
}
|
|
342
585
|
}
|
|
343
|
-
return (_jsxs("div", { className: "flex items-center gap-3", children: [value ? (_jsxs("div", { className: "relative", children: [_jsx("img", { src: value, alt: "", className: "h-16 w-16 rounded-lg object-cover border" }), _jsx("button", { type: "button", onClick: () => onChange(''), className: "absolute -top-1.5 -right-1.5 size-5 bg-destructive text-white rounded-full flex items-center justify-center hover:bg-destructive/90", children: _jsx(XIcon, { className: "size-3" }) })] })) : (_jsx("button", { type: "button", onClick: () => inputRef.current?.click(), disabled: uploading, className: "h-16 w-16 rounded-lg border-2 border-dashed border-muted-foreground/30 flex flex-col items-center justify-center gap-1 hover:border-primary/50 hover:bg-muted/50 transition-colors disabled:opacity-50", children: uploading ? (_jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground" })) : (_jsx(Upload, { className: "size-4 text-muted-foreground" })) })), _jsx("input", { ref: inputRef, type: "file", accept: "image/*", onChange: handleFile, className: "hidden" }), !value && _jsx("span", { className: "text-xs text-muted-foreground", children: "PNG, JPG, WebP" })] }));
|
|
586
|
+
return (_jsxs("div", { className: "flex items-center gap-3", children: [value ? (_jsxs("div", { className: "relative", children: [_jsx("img", { src: getImageUrl(String(value)), alt: "", className: "h-16 w-16 rounded-lg object-cover border" }), _jsx("button", { type: "button", onClick: () => onChange(''), className: "absolute -top-1.5 -right-1.5 size-5 bg-destructive text-white rounded-full flex items-center justify-center hover:bg-destructive/90", children: _jsx(XIcon, { className: "size-3" }) })] })) : (_jsx("button", { type: "button", onClick: () => inputRef.current?.click(), disabled: uploading, className: "h-16 w-16 rounded-lg border-2 border-dashed border-muted-foreground/30 flex flex-col items-center justify-center gap-1 hover:border-primary/50 hover:bg-muted/50 transition-colors disabled:opacity-50", children: uploading ? (_jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground" })) : (_jsx(Upload, { className: "size-4 text-muted-foreground" })) })), _jsx("input", { ref: inputRef, type: "file", accept: "image/*", onChange: handleFile, className: "hidden" }), !value && _jsx("span", { className: "text-xs text-muted-foreground", children: "PNG, JPG, WebP" })] }));
|
|
344
587
|
}
|
|
345
588
|
function extractArray(res) {
|
|
346
589
|
const d = res.data;
|
|
@@ -351,31 +594,6 @@ function extractArray(res) {
|
|
|
351
594
|
return [];
|
|
352
595
|
}
|
|
353
596
|
const searchCache = new Map();
|
|
354
|
-
function SearchViewValue({ field, value }) {
|
|
355
|
-
const api = useApi();
|
|
356
|
-
const [label, setLabel] = useState(String(value));
|
|
357
|
-
useEffect(() => {
|
|
358
|
-
if (!field.searchEndpoint || !value)
|
|
359
|
-
return;
|
|
360
|
-
const cacheKey = field.searchEndpoint;
|
|
361
|
-
const cached = searchCache.get(cacheKey);
|
|
362
|
-
if (cached) {
|
|
363
|
-
const match = cached.find((item) => item.value === value || item.id === value);
|
|
364
|
-
if (match) {
|
|
365
|
-
setLabel(match.label || match.name || String(value));
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
api.get(field.searchEndpoint, { params: { search: '', limit: 50 } }).then(res => {
|
|
370
|
-
const items = extractArray(res);
|
|
371
|
-
searchCache.set(cacheKey, items);
|
|
372
|
-
const match = items.find((item) => item.value === value || item.id === value);
|
|
373
|
-
if (match)
|
|
374
|
-
setLabel(match.label || match.name || String(value));
|
|
375
|
-
}).catch(() => { });
|
|
376
|
-
}, [value, field.searchEndpoint]);
|
|
377
|
-
return _jsx("p", { className: "text-sm py-1", children: label });
|
|
378
|
-
}
|
|
379
597
|
function SearchField({ field, value, onChange }) {
|
|
380
598
|
const api = useApi();
|
|
381
599
|
const [open, setOpen] = useState(false);
|
|
@@ -401,7 +619,7 @@ function SearchField({ field, value, onChange }) {
|
|
|
401
619
|
if (match)
|
|
402
620
|
setSelectedLabel(match.label || match.name || '');
|
|
403
621
|
}).catch(() => { });
|
|
404
|
-
}, [value, field.searchEndpoint]);
|
|
622
|
+
}, [value, field.searchEndpoint, api]);
|
|
405
623
|
useEffect(() => {
|
|
406
624
|
if (!open || !field.searchEndpoint)
|
|
407
625
|
return;
|
|
@@ -423,7 +641,7 @@ function SearchField({ field, value, onChange }) {
|
|
|
423
641
|
.finally(() => setLoading(false));
|
|
424
642
|
}, query ? 250 : 0);
|
|
425
643
|
return () => clearTimeout(timer);
|
|
426
|
-
}, [query, open, field.searchEndpoint]);
|
|
644
|
+
}, [query, open, field.searchEndpoint, api]);
|
|
427
645
|
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", role: "combobox", className: cn("w-full justify-between font-normal h-9", !value && "text-muted-foreground"), children: [_jsx("span", { className: "truncate", children: selectedLabel || `Seleccionar ${field.label?.toLowerCase() || ''}...` }), _jsx(ChevronDown, { className: "ml-auto h-4 w-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "w-[--radix-popover-trigger-width] p-0", align: "start", side: "bottom", sideOffset: 4, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: `Buscar ${field.label?.toLowerCase() || ''}...`, value: query, onValueChange: setQuery }), _jsx(CommandList, { className: "max-h-[200px]", children: loading ? (_jsxs("div", { className: "py-6 text-center text-sm", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin mx-auto mb-1 text-muted-foreground" }), _jsx("span", { className: "text-muted-foreground text-xs", children: "Buscando..." })] })) : results.length === 0 ? (_jsx(CommandEmpty, { children: "Sin resultados." })) : (_jsx(CommandGroup, { children: results.map((item) => {
|
|
428
646
|
const itemValue = item.value ?? item.id;
|
|
429
647
|
const itemLabel = item.label ?? item.name ?? '';
|
|
@@ -433,6 +651,6 @@ function SearchField({ field, value, onChange }) {
|
|
|
433
651
|
setSelectedLabel(itemLabel);
|
|
434
652
|
setOpen(false);
|
|
435
653
|
setQuery('');
|
|
436
|
-
}, children: [isSelected && _jsx(Check, { className: "mr-2 h-3.5 w-3.5 shrink-0 text-primary" }), item.image && (_jsx(
|
|
654
|
+
}, children: [isSelected && _jsx(Check, { className: "mr-2 h-3.5 w-3.5 shrink-0 text-primary" }), item.image && (_jsx(OptionThumb, { image: item.image, size: 20 })), _jsxs("div", { className: "flex flex-col min-w-0 ml-2", children: [_jsx("span", { className: "truncate", children: itemLabel }), item.description && (_jsx("span", { className: "text-[11px] text-muted-foreground truncate", children: item.description }))] })] }, itemValue));
|
|
437
655
|
}) })) })] }) })] }));
|
|
438
656
|
}
|
|
@@ -16,7 +16,7 @@ export interface ColumnFilterConfig {
|
|
|
16
16
|
searchEndpoint?: string;
|
|
17
17
|
}
|
|
18
18
|
/** Signature for the host-provided `getDynamicColumns` factory. */
|
|
19
|
-
export type GetDynamicColumns = (metadata: TableMetadata, handleAction: (action: string, row: any) => void, t: (key: string, options?: any) => string, language: string, columnFilterConfigs: Map<string, ColumnFilterConfig
|
|
19
|
+
export type GetDynamicColumns = (metadata: TableMetadata, handleAction: (action: string, row: any) => void, t: (key: string, options?: any) => string, language: string, columnFilterConfigs: Map<string, ColumnFilterConfig>, timeZone?: string) => ColumnDef<any>[];
|
|
20
20
|
/** Signature for the host-provided `DynamicIcon` renderer. */
|
|
21
21
|
export type DynamicIconComponent = React.ComponentType<{
|
|
22
22
|
name: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-columns-shim.d.ts","sourceRoot":"","sources":["../src/dynamic-columns-shim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;IAClF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAChD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,EACzC,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,
|
|
1
|
+
{"version":3,"file":"dynamic-columns-shim.d.ts","sourceRoot":"","sources":["../src/dynamic-columns-shim.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,cAAc,GAAG,MAAM,GAAG,MAAM,CAAA;IAClF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG,CAC5B,QAAQ,EAAE,aAAa,EACvB,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,EAChD,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,MAAM,EACzC,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACpD,QAAQ,CAAC,EAAE,MAAM,KAChB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;AAErB,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA"}
|