@asteby/metacore-runtime-react 49.4.0 → 49.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 +14 -0
- package/dist/api-context.d.ts +9 -1
- package/dist/api-context.d.ts.map +1 -1
- package/dist/api-context.js +6 -4
- package/dist/business/customer-picker.d.ts +8 -2
- package/dist/business/customer-picker.d.ts.map +1 -1
- package/dist/business/customer-picker.js +30 -27
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +1 -0
- package/dist/business/line-product-cell.d.ts +8 -10
- package/dist/business/line-product-cell.d.ts.map +1 -1
- package/dist/business/line-product-cell.js +13 -90
- package/dist/business/product-options.d.ts +41 -0
- package/dist/business/product-options.d.ts.map +1 -0
- package/dist/business/product-options.js +58 -0
- package/dist/business/product-picker.d.ts +5 -1
- package/dist/business/product-picker.d.ts.map +1 -1
- package/dist/business/product-picker.js +26 -35
- package/dist/business/relate-documents.d.ts +1 -1
- package/dist/business/relate-documents.d.ts.map +1 -1
- package/dist/business/relate-documents.js +6 -8
- package/dist/business/vehicle-picker.d.ts +8 -2
- package/dist/business/vehicle-picker.d.ts.map +1 -1
- package/dist/business/vehicle-picker.js +28 -28
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +28 -43
- package/dist/dynamic-multi-select-field.d.ts.map +1 -1
- package/dist/dynamic-multi-select-field.js +17 -5
- package/dist/dynamic-select-field.d.ts +7 -23
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +67 -161
- package/dist/dynamic-table.d.ts +31 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +37 -4
- package/dist/entity-select.d.ts +5 -2
- package/dist/entity-select.d.ts.map +1 -1
- package/dist/entity-select.js +23 -116
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/query-batch.d.ts.map +1 -1
- package/dist/query-batch.js +17 -1
- package/dist/record-picker-actions.d.ts +40 -2
- package/dist/record-picker-actions.d.ts.map +1 -1
- package/dist/record-picker-actions.js +44 -4
- package/dist/record-picker-dialog.d.ts +23 -0
- package/dist/record-picker-dialog.d.ts.map +1 -0
- package/dist/record-picker-dialog.js +37 -0
- package/dist/record-picker-option.d.ts +25 -0
- package/dist/record-picker-option.d.ts.map +1 -0
- package/dist/record-picker-option.js +55 -0
- package/dist/record-picker.d.ts +149 -0
- package/dist/record-picker.d.ts.map +1 -0
- package/dist/record-picker.js +372 -0
- package/package.json +1 -1
- package/src/__tests__/api-batch-opt-out.test.tsx +83 -0
- package/src/__tests__/business-pickers.test.tsx +6 -3
- package/src/__tests__/document-editor-inline-selects.test.tsx +2 -1
- package/src/__tests__/document-form-dialog.test.tsx +4 -3
- package/src/__tests__/document-lines-grid.test.tsx +2 -1
- package/src/__tests__/dynamic-table-bulk-actions.test.tsx +140 -0
- package/src/__tests__/dynamic-table-load-error.test.tsx +97 -0
- package/src/__tests__/record-picker.test.tsx +469 -0
- package/src/api-context.tsx +19 -4
- package/src/business/customer-picker.tsx +87 -98
- package/src/business/index.ts +1 -0
- package/src/business/line-product-cell.tsx +55 -180
- package/src/business/product-options.tsx +119 -0
- package/src/business/product-picker.tsx +63 -126
- package/src/business/relate-documents.tsx +36 -45
- package/src/business/vehicle-picker.tsx +76 -99
- package/src/dialogs/dynamic-record.tsx +44 -97
- package/src/dynamic-multi-select-field.tsx +39 -13
- package/src/dynamic-select-field.tsx +143 -368
- package/src/dynamic-table.tsx +91 -4
- package/src/entity-select.tsx +53 -202
- package/src/index.ts +27 -2
- package/src/query-batch.ts +18 -1
- package/src/record-picker-actions.tsx +101 -8
- package/src/record-picker-dialog.tsx +80 -0
- package/src/record-picker-option.tsx +96 -0
- package/src/record-picker.tsx +971 -0
- package/dist/business/use-async-search.d.ts +0 -10
- package/dist/business/use-async-search.d.ts.map +0 -1
- package/dist/business/use-async-search.js +0 -38
- package/src/business/use-async-search.ts +0 -43
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// DynamicSelectField — async, searchable single-select for declarative forms.
|
|
3
|
+
// A configuration of the shared <RecordPicker> (record-picker.tsx).
|
|
3
4
|
//
|
|
4
5
|
// This is the declarative answer to "I don't want to type a raw FK UUID".
|
|
5
6
|
// Instead of a plain <select> that dumps every option (RefSelect) or a free
|
|
@@ -25,15 +26,16 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
25
26
|
import { useEffect, useRef, useState } from 'react';
|
|
26
27
|
import { getOptionFilter } from './option-filter';
|
|
27
28
|
import { useTranslation } from 'react-i18next';
|
|
28
|
-
import { Badge, Button
|
|
29
|
-
import {
|
|
30
|
-
import { resolveColorCss } from '@asteby/metacore-ui/lib';
|
|
29
|
+
import { Badge, Button } from '@asteby/metacore-ui/primitives';
|
|
30
|
+
import { ScanLine } from 'lucide-react';
|
|
31
31
|
import { BarcodeScanner } from './barcode-scanner';
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
32
|
+
import { RecordPicker } from './record-picker';
|
|
33
|
+
import { OptionLead } from './record-picker-option';
|
|
34
|
+
import { recordLabel, requestRecordCreate, requestRecordEdit, withSearchPrefill } from './record-picker-actions';
|
|
35
35
|
import { useOptionsResolver } from './use-options-resolver';
|
|
36
|
+
import { useDebouncedValue } from './use-debounced-value';
|
|
36
37
|
import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-schema';
|
|
38
|
+
export { OptionLead, OptionThumb } from './record-picker-option';
|
|
37
39
|
/**
|
|
38
40
|
* Default hint shown when a cascading picker's depended-on field is still
|
|
39
41
|
* empty. Domain-neutral on purpose; a caller may override it per field via
|
|
@@ -41,86 +43,36 @@ import { getDependsOn, getFieldRef, resolveOptionsSource } from './dynamic-form-
|
|
|
41
43
|
*/
|
|
42
44
|
export const DEFAULT_DEPENDS_HINT = 'Selecciona primero el campo del que depende';
|
|
43
45
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* pixels (kept small — 20–24px — so the picker reads as a list, not a gallery).
|
|
49
|
-
* Inline style for the box dimensions: arbitrary Tailwind classes from a
|
|
50
|
-
* federated addon don't always survive the host's class scan.
|
|
51
|
-
*/
|
|
52
|
-
export function OptionThumb({ image, name, size = 20, }) {
|
|
53
|
-
const [broken, setBroken] = useState(false);
|
|
54
|
-
const box = { width: size, height: size };
|
|
55
|
-
// Missing or 404 image (bare avatar filenames often 404) → initials, never
|
|
56
|
-
// an invisible box that looks like "no avatar".
|
|
57
|
-
if (!image || broken) {
|
|
58
|
-
return _jsx(InitialsAvatar, { name: name, size: size, rounded: "sm", tone: "neutral" });
|
|
59
|
-
}
|
|
60
|
-
// A lucide icon name stored where an image url/path is expected (the `icon`
|
|
61
|
-
// form widget's icon mode) → render the glyph instead of a broken <img>.
|
|
62
|
-
if (isLucideIconName(image)) {
|
|
63
|
-
return (_jsx("span", { className: "flex shrink-0 items-center justify-center rounded-sm bg-muted", style: box, "aria-hidden": true, children: _jsx(DynamicIcon, { name: image, className: "size-4" }) }));
|
|
64
|
-
}
|
|
65
|
-
return (_jsx("img", { src: image, alt: "", "aria-hidden": true, loading: "lazy", className: "shrink-0 rounded-sm object-cover", style: box, onError: () => setBroken(true) }));
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Leading visual for an option: a photo thumbnail (FK relations with an image),
|
|
69
|
-
* else a declared icon, else a color dot (enum/status options with a color).
|
|
70
|
-
* Returns null when the option carries none, so plain text options stay plain.
|
|
46
|
+
* Declarative `dynamic_select` (manifest `ref` / `optionsConfig.source`) on top
|
|
47
|
+
* of the shared {@link RecordPicker}: options from the canonical options
|
|
48
|
+
* endpoint (`useOptionsResolver`: option_filter, options[].when keep-value,
|
|
49
|
+
* cascade `filter_value`), inline create/edit through the host modal events.
|
|
71
50
|
*/
|
|
72
|
-
export function OptionLead({ option, size = 20, }) {
|
|
73
|
-
if (!option)
|
|
74
|
-
return null;
|
|
75
|
-
if (option.image)
|
|
76
|
-
return _jsx(OptionThumb, { image: option.image, name: option.label, size: size });
|
|
77
|
-
if (option.icon) {
|
|
78
|
-
return (_jsx("span", { className: "flex shrink-0 items-center justify-center", style: { width: size, height: size, color: option.color ? resolveColorCss(option.color) : undefined }, "aria-hidden": true, children: _jsx(DynamicIcon, { name: option.icon, className: "size-4" }) }));
|
|
79
|
-
}
|
|
80
|
-
if (option.color) {
|
|
81
|
-
return (_jsx("span", { className: "shrink-0 rounded-full", style: { width: Math.round(size * 0.5), height: Math.round(size * 0.5), background: resolveColorCss(option.color) }, "aria-hidden": true }));
|
|
82
|
-
}
|
|
83
|
-
// No image/icon/color: an imageless reference option. Show its initials
|
|
84
|
-
// (shared InitialsAvatar) so it stays visually aligned with the sibling
|
|
85
|
-
// options that DO carry a thumbnail, rather than a blank gap.
|
|
86
|
-
if (option.label)
|
|
87
|
-
return _jsx(InitialsAvatar, { name: option.label, size: size, rounded: "sm", tone: "neutral" });
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
function useDebounced(value, ms) {
|
|
91
|
-
return useDebouncedValue(value, ms);
|
|
92
|
-
}
|
|
93
51
|
export function DynamicSelectField({ field, value, onChange, onPick, seedOption, dependsValue, dependsHint, readonly = false, staticOptions = null, descriptionAsBadge = false, hideCreate = false, createDefaults, createLockedFields, invalid = false, }) {
|
|
94
52
|
const { t } = useTranslation();
|
|
95
53
|
const ph = (fallback) => field.placeholder ? t(field.placeholder, { defaultValue: field.placeholder }) : fallback;
|
|
96
54
|
const [open, setOpen] = useState(false);
|
|
97
55
|
const [search, setSearch] = useState('');
|
|
98
56
|
const [scanOpen, setScanOpen] = useState(false);
|
|
99
|
-
const debounced =
|
|
57
|
+
const debounced = useDebouncedValue(search, 250);
|
|
100
58
|
// Escaneo por cámara para "llenar rápido": opt-in por campo (`scan`). El
|
|
101
|
-
// botón aparece SIEMPRE que el campo lo declara (igual que el POS)
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// cuando no hay cámara. Un código escaneado alimenta la búsqueda y abre el
|
|
105
|
-
// picker para elegir la referencia sin tipear el UUID.
|
|
59
|
+
// botón aparece SIEMPRE que el campo lo declara (igual que el POS). El
|
|
60
|
+
// BarcodeScanner ya degrada con un mensaje cuando no hay cámara. Un código
|
|
61
|
+
// escaneado alimenta la búsqueda y abre el picker.
|
|
106
62
|
const scanEnabled = !!(field.scan ?? field.scannable);
|
|
107
63
|
const handleScanDetected = (code) => {
|
|
108
64
|
setSearch(code);
|
|
109
65
|
setOpen(true);
|
|
110
66
|
};
|
|
111
|
-
// Remember the
|
|
112
|
-
//
|
|
67
|
+
// Remember the option the user actually picked so the trigger shows a name
|
|
68
|
+
// (not a UUID) without a round-trip.
|
|
113
69
|
const [picked, setPicked] = useState(null);
|
|
114
|
-
// Tolerate the snake_case `source`/`relation` aliases the
|
|
115
|
-
// for the FK target, not just camelCase `ref`.
|
|
70
|
+
// Tolerate the snake_case `source`/`relation` aliases for the FK target.
|
|
116
71
|
const fieldRef = getFieldRef(field);
|
|
117
|
-
//
|
|
118
|
-
// over the field's `ref` — query the SOURCE model with `field=<value>`;
|
|
119
|
-
// otherwise keep the canonical `ref`-based resolution.
|
|
72
|
+
// An `optionsConfig.source` (dependent/scoped picker) wins over `ref`.
|
|
120
73
|
const source = resolveOptionsSource(field);
|
|
121
74
|
// Cascade: a `dependsOn` field whose value is still empty leaves this
|
|
122
|
-
// picker disabled until the parent is set.
|
|
123
|
-
// value the caller threaded from the form context.
|
|
75
|
+
// picker disabled until the parent is set.
|
|
124
76
|
const dependsOn = getDependsOn(field);
|
|
125
77
|
const scope = dependsValue ? String(dependsValue) : '';
|
|
126
78
|
const blockedByDependency = !!dependsOn && scope === '';
|
|
@@ -131,21 +83,15 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
|
|
|
131
83
|
fieldKey: source.fieldKey,
|
|
132
84
|
ref: source.ref,
|
|
133
85
|
// optionsConfig.source → `/options/<source>`. Else searchEndpoint only
|
|
134
|
-
// drives the URL when there's no ref
|
|
135
|
-
// kernel-derived path and wins.
|
|
86
|
+
// drives the URL when there's no ref (ref is canonical and wins).
|
|
136
87
|
endpoint: source.endpoint ?? (source.ref ? undefined : field.searchEndpoint),
|
|
137
88
|
query: debounced,
|
|
138
89
|
limit: 20,
|
|
139
|
-
// Cascade scope forwarded as filter_value (only when this field
|
|
140
|
-
// declares a dependency). Re-fetches when the parent value changes.
|
|
141
90
|
filterValue: dependsOn ? scope : undefined,
|
|
142
91
|
optionFilter,
|
|
143
92
|
keepValue: value,
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
// fetches. A readonly cell fetches eagerly so its value's label resolves
|
|
147
|
-
// to the name without the user ever opening it.
|
|
148
|
-
// Static lists never hit the network.
|
|
93
|
+
// Fetch only while open; a readonly cell fetches eagerly so its label
|
|
94
|
+
// resolves to the name. Blocked cascades and static lists never fetch.
|
|
149
95
|
enabled: !useStatic && (open || readonly) && !blockedByDependency,
|
|
150
96
|
});
|
|
151
97
|
const options = useStatic
|
|
@@ -158,8 +104,7 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
|
|
|
158
104
|
})
|
|
159
105
|
: fetchedOptions;
|
|
160
106
|
const loading = useStatic ? false : fetchLoading;
|
|
161
|
-
//
|
|
162
|
-
// belongs to the new scope, so clear the selection (skip the initial mount).
|
|
107
|
+
// A new cascade scope invalidates the selection (skip the initial mount).
|
|
163
108
|
const prevScopeRef = useRef(scope);
|
|
164
109
|
useEffect(() => {
|
|
165
110
|
if (!dependsOn)
|
|
@@ -171,17 +116,11 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
|
|
|
171
116
|
onChange('');
|
|
172
117
|
}
|
|
173
118
|
}, [dependsOn, scope, value, onChange]);
|
|
174
|
-
// The currently-selected option, resolved either from what the user picked
|
|
175
|
-
// (cached in `picked`) or from the loaded page. Drives both the trigger
|
|
176
|
-
// label and its thumbnail.
|
|
177
119
|
const selectedOption = (picked && String(picked.id) === String(value) ? picked : null) ??
|
|
178
120
|
options.find((o) => String(o.id) === String(value)) ??
|
|
179
121
|
(seedOption && String(seedOption.id) === String(value) ? seedOption : null) ??
|
|
180
122
|
null;
|
|
181
123
|
const selectedLabel = selectedOption?.label ?? (value ? String(value) : '');
|
|
182
|
-
// Only switch the picker into "with thumbnails" mode when the data actually
|
|
183
|
-
// carries images — a relation whose options have no `image` keeps the plain
|
|
184
|
-
// text list it had before (no empty placeholder column).
|
|
185
124
|
const handlePick = (opt) => {
|
|
186
125
|
setPicked(opt);
|
|
187
126
|
onChange(String(opt.id));
|
|
@@ -189,88 +128,55 @@ export function DynamicSelectField({ field, value, onChange, onPick, seedOption,
|
|
|
189
128
|
setOpen(false);
|
|
190
129
|
setSearch('');
|
|
191
130
|
};
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
// via
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Inline-edit: with a value selected the joined pencil opens THAT record's
|
|
198
|
-
// edit modal through the sibling `metacore:edit-record` event (the host's
|
|
199
|
-
// RecordCreateBridge already listens for it). On save the trigger label
|
|
200
|
-
// refreshes from the returned record.
|
|
201
|
-
const openEdit = () => {
|
|
202
|
-
if (!fieldRef || !value || typeof window === 'undefined')
|
|
131
|
+
const toOption = (id, label) => ({ id, value: id, label, name: label });
|
|
132
|
+
// Inline create / edit: the REFERENCED model's real modal, rendered by the
|
|
133
|
+
// host (RecordCreateBridge) via decoupled window events — no host import.
|
|
134
|
+
const openCreate = (query) => {
|
|
135
|
+
if (!fieldRef)
|
|
203
136
|
return;
|
|
204
137
|
setOpen(false);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
138
|
+
requestRecordCreate({
|
|
139
|
+
model: fieldRef,
|
|
140
|
+
// The searched text seeds the new record's name; explicit
|
|
141
|
+
// createDefaults win. Keys that aren't fields are ignored.
|
|
142
|
+
defaults: withSearchPrefill(query, 'name', createDefaults),
|
|
143
|
+
lockedFields: createLockedFields,
|
|
144
|
+
onCreated: (rec) => {
|
|
145
|
+
if (rec && rec.id != null) {
|
|
146
|
+
const id = String(rec.id);
|
|
147
|
+
handlePick(toOption(id, recordLabel(rec) ?? id));
|
|
148
|
+
}
|
|
215
149
|
},
|
|
216
|
-
})
|
|
150
|
+
});
|
|
217
151
|
};
|
|
218
|
-
const
|
|
219
|
-
if (!fieldRef ||
|
|
152
|
+
const openEdit = () => {
|
|
153
|
+
if (!fieldRef || !value)
|
|
220
154
|
return;
|
|
221
155
|
setOpen(false);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
onCreated: (rec) => {
|
|
231
|
-
if (rec && rec.id != null) {
|
|
232
|
-
const id = String(rec.id);
|
|
233
|
-
const label = String(rec.name ?? rec.label ?? rec.title ?? rec.id);
|
|
234
|
-
handlePick({ id, value: id, label, name: label });
|
|
235
|
-
}
|
|
236
|
-
},
|
|
156
|
+
const id = String(value);
|
|
157
|
+
requestRecordEdit({
|
|
158
|
+
model: fieldRef,
|
|
159
|
+
recordId: id,
|
|
160
|
+
onSaved: (rec) => {
|
|
161
|
+
const label = recordLabel(rec);
|
|
162
|
+
if (label != null)
|
|
163
|
+
setPicked({ ...(selectedOption ?? toOption(id, label)), id, value: id, label, name: label });
|
|
237
164
|
},
|
|
238
|
-
})
|
|
165
|
+
});
|
|
239
166
|
};
|
|
240
|
-
|
|
241
|
-
// label (name) as a disabled control — no popover, no inline-create. While
|
|
242
|
-
// the eager fetch is in flight the label falls back to the seed/raw value,
|
|
243
|
-
// then snaps to the name once options arrive.
|
|
244
|
-
if (readonly) {
|
|
245
|
-
return (_jsx(Button, { type: "button", variant: "outline", role: "combobox", id: field.key, disabled: true, "aria-readonly": "true", className: "w-full min-w-0 cursor-default justify-start font-normal opacity-100", children: _jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [selectedOption ? _jsx(OptionLead, { option: selectedOption, size: 20 }) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedOption ? '' : 'text-muted-foreground'), children: selectedOption?.label ?? (loading ? 'Cargando…' : ph('—')) })] }) }));
|
|
246
|
-
}
|
|
247
|
-
// w-full + min-w-0: as a grid cell child, the row must be allowed to shrink
|
|
248
|
-
// to the cell. Without min-w-0 the combobox+button row sizes to its content
|
|
249
|
-
// (the long empty-state placeholder) and overflows the column, pushing the
|
|
250
|
-
// "+" off-screen — it only "fit" once a short value was selected.
|
|
251
|
-
const canMutate = !!fieldRef && !hideCreate && !useStatic;
|
|
252
|
-
const joined = canMutate && !blockedByDependency && hasRecordPickerAction(!!value, openCreate, openEdit);
|
|
167
|
+
const canMutate = !!fieldRef && !hideCreate && !useStatic && !blockedByDependency;
|
|
253
168
|
const fieldName = field.label ? t(field.label, { defaultValue: field.label }) : fieldRef ?? '';
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
? debounced
|
|
267
|
-
? 'Sin resultados'
|
|
268
|
-
: 'Sin opciones'
|
|
269
|
-
: debounced
|
|
270
|
-
? 'Sin resultados'
|
|
271
|
-
: 'Sin resultados — escribe para filtrar' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
|
|
272
|
-
const isSel = String(opt.id) === String(value);
|
|
273
|
-
return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0') }), _jsx(OptionLead, { option: opt, size: 24 }), descriptionAsBadge ? (_jsxs("div", { className: "ml-2 flex min-w-0 flex-1 items-center gap-2", children: [_jsx("span", { className: "min-w-0 flex-1 truncate", children: opt.label }), opt.description ? (_jsx(Badge, { variant: "secondary", className: "shrink-0 font-normal tabular-nums", children: opt.description })) : null] })) : (_jsxs("div", { className: "ml-2 flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] }))] }, String(opt.id)));
|
|
274
|
-
}) })), canMutate && !loading && (_jsx(PickerCreateItem, { label: fieldName, onSelect: openCreate }))] })] }) })] }), canMutate && !blockedByDependency && (_jsx(RecordPickerAction, { hasValue: !!value, label: fieldName, onCreate: openCreate, onEdit: openEdit }))] }), scanEnabled && (_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: () => setScanOpen(true), title: "Escanear con la c\u00E1mara", "aria-label": "Escanear c\u00F3digo de barras con la c\u00E1mara", children: _jsx(ScanLine, { className: "size-4" }) })), scanEnabled && (_jsx(BarcodeScanner, { open: scanOpen, onClose: () => setScanOpen(false), onDetected: handleScanDetected, continuous: false, position: "fixed", title: `Escanear ${field.label ?? ''}`.trim() }))] }));
|
|
169
|
+
const badge = (opt) => opt.description ? (_jsx(Badge, { variant: "secondary", className: "shrink-0 font-normal tabular-nums", children: opt.description })) : null;
|
|
170
|
+
return (_jsx(RecordPicker, { slot: "dynamic-select", id: field.key, items: options, loading: loading, getKey: (o) => String(o.id), getLabel: (o) => o.label, value: value, selected: selectedOption, onSelect: handlePick, query: search, onQueryChange: setSearch, open: open && !blockedByDependency, onOpenChange: (o) => {
|
|
171
|
+
if (!blockedByDependency)
|
|
172
|
+
setOpen(o);
|
|
173
|
+
}, disabled: blockedByDependency, readOnly: readonly, invalid: invalid, renderLead: (o, where) => _jsx(OptionLead, { option: o, size: where === 'option' ? 24 : 20 }), getDescription: descriptionAsBadge ? undefined : (o) => o.description, renderTrailing: descriptionAsBadge ? (o) => badge(o) : undefined, renderValue: () => {
|
|
174
|
+
if (readonly) {
|
|
175
|
+
return (_jsxs(_Fragment, { children: [selectedOption ? _jsx(OptionLead, { option: selectedOption, size: 20 }) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedOption ? '' : 'text-muted-foreground'), children: selectedOption?.label ?? (loading ? 'Cargando…' : ph('—')) })] }));
|
|
176
|
+
}
|
|
177
|
+
return (_jsxs(_Fragment, { children: [value && selectedOption ? _jsx(OptionLead, { option: selectedOption, size: 20 }) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: blockedByDependency ? dependsHint || DEFAULT_DEPENDS_HINT : selectedLabel || ph('Buscar…') }), descriptionAsBadge && selectedOption ? badge(selectedOption) : null] }));
|
|
178
|
+
}, placeholder: ph('Buscar…'), searchPlaceholder: ph('Buscar…'), emptyText: debounced ? 'Sin resultados' : useStatic ? 'Sin opciones' : 'Sin resultados — escribe para filtrar', entityLabel: fieldName, onCreate: canMutate ? openCreate : undefined, onEdit: canMutate ? openEdit : undefined, triggerProps: {
|
|
179
|
+
'data-depends-blocked': blockedByDependency ? '' : undefined,
|
|
180
|
+
}, after: scanEnabled ? (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: () => setScanOpen(true), title: "Escanear con la c\u00E1mara", "aria-label": "Escanear c\u00F3digo de barras con la c\u00E1mara", children: _jsx(ScanLine, { className: "size-4" }) }), _jsx(BarcodeScanner, { open: scanOpen, onClose: () => setScanOpen(false), onDetected: handleScanDetected, continuous: false, position: "fixed", title: `Escanear ${field.label ?? ''}`.trim() })] })) : null }));
|
|
275
181
|
}
|
|
276
182
|
export default DynamicSelectField;
|
package/dist/dynamic-table.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
1
2
|
import { type ColumnDef } from '@tanstack/react-table';
|
|
2
3
|
import type { GetDynamicColumns } from './dynamic-columns-shim';
|
|
3
4
|
export interface DynamicTableProps {
|
|
@@ -90,6 +91,35 @@ export interface DynamicTableProps {
|
|
|
90
91
|
* render every row. A positive number overrides the threshold.
|
|
91
92
|
*/
|
|
92
93
|
virtualizeRows?: boolean | number;
|
|
94
|
+
/**
|
|
95
|
+
* Host-provided actions rendered in the floating bar that appears while
|
|
96
|
+
* rows are selected, next to the built-in "Eliminar". Pass a node, or a
|
|
97
|
+
* function that receives the current selection:
|
|
98
|
+
* - `selectedRows`: the selected row objects (current page only);
|
|
99
|
+
* - `selectedIds`: their `id`s;
|
|
100
|
+
* - `clearSelection()`: unselect everything (call after a successful action);
|
|
101
|
+
* - `refresh()`: reload the list.
|
|
102
|
+
* The selection is cleared whenever the page, page size, sort, search or
|
|
103
|
+
* column filters change (classic pagination), so a bulk action can never
|
|
104
|
+
* hit rows the user no longer sees. The host owns the write call and its
|
|
105
|
+
* authorization — the table only supplies the selection.
|
|
106
|
+
*/
|
|
107
|
+
extraBulkActions?: ReactNode | ((ctx: DynamicTableBulkContext) => ReactNode);
|
|
108
|
+
/** Hide the built-in bulk "Eliminar" (e.g. a model whose rows must not be deleted in batch). */
|
|
109
|
+
hideBulkDelete?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* First-use content shown instead of "No se encontraron resultados" when the
|
|
112
|
+
* list loaded fine, is empty, and no search/filter is active. Never shown
|
|
113
|
+
* after a load error (a retry is offered) or a 403.
|
|
114
|
+
*/
|
|
115
|
+
emptyState?: ReactNode;
|
|
116
|
+
}
|
|
117
|
+
/** Context handed to `DynamicTableProps.extraBulkActions` when it is a function. */
|
|
118
|
+
export interface DynamicTableBulkContext {
|
|
119
|
+
selectedRows: any[];
|
|
120
|
+
selectedIds: Array<string | number>;
|
|
121
|
+
clearSelection: () => void;
|
|
122
|
+
refresh: () => void;
|
|
93
123
|
}
|
|
94
|
-
export declare function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync, hideImport, hideExport, hiddenColumns, allowedActionKeys, onAction, onRowClick, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, timeZone: timeZoneProp, currency: currencyProp, pagination: paginationMode, infiniteScroll: infiniteScrollProp, virtualizeRows, }: DynamicTableProps): import("react").JSX.Element;
|
|
124
|
+
export declare function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync, hideImport, hideExport, hiddenColumns, allowedActionKeys, onAction, onRowClick, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, timeZone: timeZoneProp, currency: currencyProp, pagination: paginationMode, infiniteScroll: infiniteScrollProp, virtualizeRows, extraBulkActions, hideBulkDelete, emptyState, }: DynamicTableProps): import("react").JSX.Element;
|
|
95
125
|
//# sourceMappingURL=dynamic-table.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAYA,OAAO,EAAsE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAM1G,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAmC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAyGnF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,UAAU,CAAA;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACjC;;;;;;;;;;;;OAYG;IACH,gBAAgB,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,GAAG,EAAE,uBAAuB,KAAK,SAAS,CAAC,CAAA;IAC5E,gGAAgG;IAChG,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,CAAA;CACzB;AAED,oFAAoF;AACpF,MAAM,WAAW,uBAAuB;IACpC,YAAY,EAAE,GAAG,EAAE,CAAA;IACnB,WAAW,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;IACnC,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAA;CACtB;AAQD,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,aAAoB,EACpB,UAAU,EACV,UAAU,EACV,aAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,cAAc,EAC1B,cAAc,EAAE,kBAA0B,EAC1C,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,UAAU,GACb,EAAE,iBAAiB,+BAioDnB"}
|
package/dist/dynamic-table.js
CHANGED
|
@@ -118,7 +118,7 @@ function isForbiddenError(error) {
|
|
|
118
118
|
const e = error;
|
|
119
119
|
return e?.response?.status === 403 || e?.status === 403;
|
|
120
120
|
}
|
|
121
|
-
export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync = true, hideImport, hideExport, hiddenColumns = [], allowedActionKeys, onAction, onRowClick, refreshTrigger, defaultFilters, extraColumns = [], getDynamicColumns = defaultGetDynamicColumns, timeZone: timeZoneProp, currency: currencyProp, pagination: paginationMode, infiniteScroll: infiniteScrollProp = false, virtualizeRows, }) {
|
|
121
|
+
export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync = true, hideImport, hideExport, hiddenColumns = [], allowedActionKeys, onAction, onRowClick, refreshTrigger, defaultFilters, extraColumns = [], getDynamicColumns = defaultGetDynamicColumns, timeZone: timeZoneProp, currency: currencyProp, pagination: paginationMode, infiniteScroll: infiniteScrollProp = false, virtualizeRows, extraBulkActions, hideBulkDelete, emptyState, }) {
|
|
122
122
|
// The org's timezone/currency: an explicit prop wins, else the app-wide
|
|
123
123
|
// OrgRuntimeProvider (without it, money fell back to USD).
|
|
124
124
|
const orgTimeZone = useTimeZone();
|
|
@@ -165,6 +165,9 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
165
165
|
// model. Rendered as "no permission" instead of masquerading as an empty
|
|
166
166
|
// list. Purely display state — never a fetch dependency (no refetch loop).
|
|
167
167
|
const [forbidden, setForbidden] = useState(false);
|
|
168
|
+
// A list request that failed for a reason other than 403 (500, network,
|
|
169
|
+
// `success: false`). Shown with a retry instead of a misleading "no results".
|
|
170
|
+
const [loadError, setLoadError] = useState(false);
|
|
168
171
|
const infPageRef = useRef(1);
|
|
169
172
|
const [optionsMap, setOptionsMap] = useState(new Map());
|
|
170
173
|
const [exportOpen, setExportOpen] = useState(false);
|
|
@@ -744,6 +747,7 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
744
747
|
params.include = relationInclude;
|
|
745
748
|
const res = await api.get(endpoint || `/data/${model}`, { params });
|
|
746
749
|
setForbidden(false);
|
|
750
|
+
setLoadError(!res.data.success);
|
|
747
751
|
if (res.data.success) {
|
|
748
752
|
const rows = res.data.data || [];
|
|
749
753
|
setData(rows);
|
|
@@ -759,11 +763,13 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
759
763
|
catch (error) {
|
|
760
764
|
if (isForbiddenError(error)) {
|
|
761
765
|
setForbidden(true);
|
|
766
|
+
setLoadError(false);
|
|
762
767
|
setData([]);
|
|
763
768
|
setRowCount(0);
|
|
764
769
|
}
|
|
765
770
|
else {
|
|
766
771
|
console.error('Error al cargar los datos', error);
|
|
772
|
+
setLoadError(true);
|
|
767
773
|
}
|
|
768
774
|
}
|
|
769
775
|
finally {
|
|
@@ -813,6 +819,7 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
813
819
|
params,
|
|
814
820
|
}));
|
|
815
821
|
setForbidden(false);
|
|
822
|
+
setLoadError(!res.data.success);
|
|
816
823
|
if (res.data.success) {
|
|
817
824
|
const rows = res.data.data || [];
|
|
818
825
|
setData((prev) => (append ? dedupeById(prev, rows) : rows));
|
|
@@ -832,6 +839,7 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
832
839
|
catch (error) {
|
|
833
840
|
if (isForbiddenError(error)) {
|
|
834
841
|
setForbidden(true);
|
|
842
|
+
setLoadError(false);
|
|
835
843
|
if (!append) {
|
|
836
844
|
setData([]);
|
|
837
845
|
setRowCount(0);
|
|
@@ -840,6 +848,7 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
840
848
|
}
|
|
841
849
|
else {
|
|
842
850
|
console.error('Error al cargar los datos', error);
|
|
851
|
+
setLoadError(true);
|
|
843
852
|
}
|
|
844
853
|
}
|
|
845
854
|
finally {
|
|
@@ -1237,6 +1246,10 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
1237
1246
|
manualSorting: true,
|
|
1238
1247
|
manualFiltering: true,
|
|
1239
1248
|
enableRowSelection: true,
|
|
1249
|
+
// Key the selection by record id, not by position in the page: a bulk
|
|
1250
|
+
// action must act on the rows the user ticked, never on whatever row
|
|
1251
|
+
// lands at the same index after the data changes.
|
|
1252
|
+
getRowId: (row, index) => (row?.id != null ? String(row.id) : String(index)),
|
|
1240
1253
|
onRowSelectionChange: setRowSelection,
|
|
1241
1254
|
onSortingChange: setSorting,
|
|
1242
1255
|
onColumnVisibilityChange: setColumnVisibility,
|
|
@@ -1251,6 +1264,19 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
1251
1264
|
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
1252
1265
|
});
|
|
1253
1266
|
const tableRows = table.getRowModel().rows;
|
|
1267
|
+
const bulkSelectedRows = table.getFilteredSelectedRowModel().rows.map((r) => r.original);
|
|
1268
|
+
// Classic pagination swaps the visible rows on every page / sort / search /
|
|
1269
|
+
// filter change; drop the selection so it never refers to rows that are no
|
|
1270
|
+
// longer on screen. Infinite mode accumulates rows (selection is id-keyed,
|
|
1271
|
+
// so it stays valid) and is left alone.
|
|
1272
|
+
const selectionScope = infiniteScroll ? '' : JSON.stringify([pagination.pageIndex, pagination.pageSize, sorting, columnFilters, globalFilter]);
|
|
1273
|
+
const prevSelectionScope = useRef(selectionScope);
|
|
1274
|
+
useEffect(() => {
|
|
1275
|
+
if (prevSelectionScope.current !== selectionScope) {
|
|
1276
|
+
prevSelectionScope.current = selectionScope;
|
|
1277
|
+
setRowSelection({});
|
|
1278
|
+
}
|
|
1279
|
+
}, [selectionScope]);
|
|
1254
1280
|
const virtualizeThreshold = resolveVirtualizeThreshold(virtualizeRows);
|
|
1255
1281
|
const shouldVirtualize = virtualizeThreshold !== false && tableRows.length >= virtualizeThreshold;
|
|
1256
1282
|
const colSpan = Math.max(columns.length, 1);
|
|
@@ -1302,7 +1328,7 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
1302
1328
|
const isActionsColumn = cell.column.id === 'actions';
|
|
1303
1329
|
const isSelectColumn = cell.column.id === 'select';
|
|
1304
1330
|
return (_jsx(TableCell, { style: cell.column.columnDef.size ? { width: cell.column.columnDef.size } : undefined, className: cn('py-2', isActionsColumn && 'sticky right-0 bg-card shadow-[-2px_0_5px_-2px_rgba(0,0,0,0.1)]'), onClick: (isActionsColumn || isSelectColumn) ? (e) => e.stopPropagation() : undefined, children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
|
|
1305
|
-
}) }, row.id)))), aggregateColumns.length > 0 && Object.keys(footerTotals).length > 0 && (_jsx(TableRow, { className: 'border-0 hover:bg-transparent', children: _jsx(TableCell, { colSpan: columns.length, className: 'h-full p-0' }) }))] })) : (_jsx(TableRow, { className: 'border-b-0 hover:bg-transparent', children: _jsx(TableCell, { colSpan: columns.length, className: 'h-full p-0', children: _jsxs("div", { className: "flex h-full py-12 flex-col items-center justify-center gap-2 text-muted-foreground", children: [_jsx("div", { className: "flex h-20 w-20 items-center justify-center rounded-full bg-muted/50", children: _jsx(Inbox, { className: "h-10 w-10" }) }), _jsx("div", { className: "flex flex-col items-center gap-1", children: forbidden ? (_jsxs(_Fragment, { children: [_jsx("h3", { className: "text-lg font-semibold text-foreground", role: "alert", children: t('dynamic.forbidden_title', { defaultValue: 'Sin permiso para ver este módulo' }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('dynamic.forbidden_hint', { defaultValue: 'Tu rol no tiene acceso de lectura a estos datos. Pide acceso a un administrador.' }) })] })) : (_jsxs(_Fragment, { children: [_jsx("h3", { className: "text-lg font-semibold text-foreground", children: "No se encontraron resultados" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "No hay datos para mostrar en este momento." })] })) })] }) }) })) }), aggregateColumns.length > 0 && Object.keys(footerTotals).length > 0 && (_jsx(TableFooter, { className: "bg-transparent", children: _jsx(TableRow, { className: "hover:bg-transparent", children: table.getVisibleLeafColumns().map((leaf, idx) => {
|
|
1331
|
+
}) }, row.id)))), aggregateColumns.length > 0 && Object.keys(footerTotals).length > 0 && (_jsx(TableRow, { className: 'border-0 hover:bg-transparent', children: _jsx(TableCell, { colSpan: columns.length, className: 'h-full p-0' }) }))] })) : (_jsx(TableRow, { className: 'border-b-0 hover:bg-transparent', children: _jsx(TableCell, { colSpan: columns.length, className: 'h-full p-0', children: _jsxs("div", { className: "flex h-full py-12 flex-col items-center justify-center gap-2 text-muted-foreground", children: [_jsx("div", { className: "flex h-20 w-20 items-center justify-center rounded-full bg-muted/50", children: _jsx(Inbox, { className: "h-10 w-10" }) }), _jsx("div", { className: "flex flex-col items-center gap-1", children: forbidden ? (_jsxs(_Fragment, { children: [_jsx("h3", { className: "text-lg font-semibold text-foreground", role: "alert", children: t('dynamic.forbidden_title', { defaultValue: 'Sin permiso para ver este módulo' }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('dynamic.forbidden_hint', { defaultValue: 'Tu rol no tiene acceso de lectura a estos datos. Pide acceso a un administrador.' }) })] })) : loadError ? (_jsxs(_Fragment, { children: [_jsx("h3", { className: "text-lg font-semibold text-foreground", role: "alert", children: t('dynamic.load_error_title', { defaultValue: 'No se pudieron cargar los datos' }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('dynamic.load_error_hint', { defaultValue: 'Ocurrió un error al consultar la lista. Inténtalo de nuevo.' }) }), _jsx(Button, { variant: "outline", size: "sm", className: "mt-2", onClick: handleRefresh, children: t('dynamic.retry', { defaultValue: 'Reintentar' }) })] })) : emptyState && !hasActiveFilters ? (emptyState) : (_jsxs(_Fragment, { children: [_jsx("h3", { className: "text-lg font-semibold text-foreground", children: "No se encontraron resultados" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "No hay datos para mostrar en este momento." })] })) })] }) }) })) }), aggregateColumns.length > 0 && Object.keys(footerTotals).length > 0 && (_jsx(TableFooter, { className: "bg-transparent", children: _jsx(TableRow, { className: "hover:bg-transparent", children: table.getVisibleLeafColumns().map((leaf, idx) => {
|
|
1306
1332
|
const col = (metadata?.columns ?? []).find((c) => c.key === leaf.id);
|
|
1307
1333
|
const isFirst = idx === 0;
|
|
1308
1334
|
const stickyBase = 'sticky bottom-0 z-10 border-t bg-background py-2 font-semibold';
|
|
@@ -1342,9 +1368,16 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
|
|
|
1342
1368
|
(typeof header === 'string' ? header : cell.column.id);
|
|
1343
1369
|
return (_jsxs("div", { className: 'flex items-start justify-between gap-3 text-sm', children: [_jsx("span", { className: 'shrink-0 text-muted-foreground', children: label }), _jsx("span", { className: 'min-w-0 overflow-hidden break-words text-right font-medium [&_[data-slot=badge]]:max-w-full [&_[data-slot=badge]]:whitespace-normal [&_[data-slot=badge]]:text-start', children: flexRender(cell.column.columnDef.cell, cell.getContext()) })] }, cell.id));
|
|
1344
1370
|
}), actionsCell && (_jsx("div", { className: 'flex justify-end border-t pt-2', onClick: onRowClick ? (e) => e.stopPropagation() : undefined, children: flexRender(actionsCell.column.columnDef.cell, actionsCell.getContext()) }))] }, row.id));
|
|
1345
|
-
}))) : (_jsxs("div", { className: 'flex flex-col items-center justify-center gap-2 rounded-lg border bg-card py-12 text-muted-foreground', children: [_jsx("div", { className: 'flex h-16 w-16 items-center justify-center rounded-full bg-muted/50', children: _jsx(Inbox, { className: 'h-8 w-8' }) }), forbidden ? (_jsxs(_Fragment, { children: [_jsx("h3", { className: 'text-base font-semibold text-foreground', role: 'alert', children: t('dynamic.forbidden_title', { defaultValue: 'Sin permiso para ver este módulo' }) }), _jsx("p", { className: 'text-sm text-muted-foreground', children: t('dynamic.forbidden_hint', { defaultValue: 'Tu rol no tiene acceso de lectura a estos datos. Pide acceso a un administrador.' }) })] })) : (_jsxs(_Fragment, { children: [_jsx("h3", { className: 'text-base font-semibold text-foreground', children: "No se encontraron resultados" }), _jsx("p", { className: 'text-sm text-muted-foreground', children: "No hay datos para mostrar en este momento." })] }))] })), infiniteScroll && (_jsxs(_Fragment, { children: [loadingMore && (_jsx(Skeleton, { className: 'h-16 w-full shrink-0', "data-testid": 'table-loading-more-mobile' })), _jsx("div", { ref: infMobileSentinel, className: 'h-1 w-full shrink-0', "aria-hidden": true })] }))] }), _jsx("div", { className: 'shrink-0 pt-4', children: infiniteScroll ? (data.length > 0 && (_jsx("p", { className: 'text-center text-xs text-muted-foreground tabular-nums', children: t('common.showingCount', {
|
|
1371
|
+
}))) : (_jsxs("div", { className: 'flex flex-col items-center justify-center gap-2 rounded-lg border bg-card py-12 text-muted-foreground', children: [_jsx("div", { className: 'flex h-16 w-16 items-center justify-center rounded-full bg-muted/50', children: _jsx(Inbox, { className: 'h-8 w-8' }) }), forbidden ? (_jsxs(_Fragment, { children: [_jsx("h3", { className: 'text-base font-semibold text-foreground', role: 'alert', children: t('dynamic.forbidden_title', { defaultValue: 'Sin permiso para ver este módulo' }) }), _jsx("p", { className: 'text-sm text-muted-foreground', children: t('dynamic.forbidden_hint', { defaultValue: 'Tu rol no tiene acceso de lectura a estos datos. Pide acceso a un administrador.' }) })] })) : loadError ? (_jsxs(_Fragment, { children: [_jsx("h3", { className: 'text-base font-semibold text-foreground', role: 'alert', children: t('dynamic.load_error_title', { defaultValue: 'No se pudieron cargar los datos' }) }), _jsx("p", { className: 'text-sm text-muted-foreground', children: t('dynamic.load_error_hint', { defaultValue: 'Ocurrió un error al consultar la lista. Inténtalo de nuevo.' }) }), _jsx(Button, { variant: 'outline', size: 'sm', onClick: handleRefresh, children: t('dynamic.retry', { defaultValue: 'Reintentar' }) })] })) : emptyState && !hasActiveFilters ? (emptyState) : (_jsxs(_Fragment, { children: [_jsx("h3", { className: 'text-base font-semibold text-foreground', children: "No se encontraron resultados" }), _jsx("p", { className: 'text-sm text-muted-foreground', children: "No hay datos para mostrar en este momento." })] }))] })), infiniteScroll && (_jsxs(_Fragment, { children: [loadingMore && (_jsx(Skeleton, { className: 'h-16 w-full shrink-0', "data-testid": 'table-loading-more-mobile' })), _jsx("div", { ref: infMobileSentinel, className: 'h-1 w-full shrink-0', "aria-hidden": true })] }))] }), _jsx("div", { className: 'shrink-0 pt-4', children: infiniteScroll ? (data.length > 0 && (_jsx("p", { className: 'text-center text-xs text-muted-foreground tabular-nums', children: t('common.showingCount', {
|
|
1346
1372
|
defaultValue: '{{count}} de {{total}}',
|
|
1347
1373
|
count: data.length,
|
|
1348
1374
|
total: rowCount,
|
|
1349
|
-
}) }))) : (_jsx(DataTablePagination, { table: table, pageSizeOptions: metadata.perPageOptions })) })] }), rowActionDialogs, _jsx(AlertDialog, { open: showBulkDeleteConfirm, onOpenChange: (open) => !open && !isBulkDeleting && setShowBulkDeleteConfirm(false), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: isBulkDeleting ? 'Eliminando registros...' : '¿Eliminar múltiples registros?' }), _jsx(AlertDialogDescription, { children: isBulkDeleting ? (_jsxs("div", { className: "space-y-4 mt-4", children: [_jsx(Progress, { value: (bulkDeleteProgress / bulkDeleteTotal) * 100 }), _jsxs("p", { className: "text-center text-sm", children: ["Procesando ", bulkDeleteProgress, " de ", bulkDeleteTotal, " registros..."] })] })) : (_jsxs(_Fragment, { children: ["Esta acci\u00F3n no se puede deshacer. Se eliminar\u00E1n permanentemente ", _jsx("strong", { children: Object.keys(rowSelection).length }), " registro(s) de nuestros servidores."] })) })] }), !isBulkDeleting && (_jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: t('common.cancel') }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); confirmBulkDelete(); }, className: "bg-red-600 hover:bg-red-700", children: "Eliminar todos" })] }))] }) }), bulkReasonPrompt.dialog, exportEnabled && (_jsx(ExportDialog, { open: exportOpen, onOpenChange: setExportOpen, model: model, metadata: metadata, currentFilters: buildFilterParams(), hasActiveFilters: hasActiveFilters })), importEnabled && (_jsx(ImportDialog, { open: importOpen, onOpenChange: setImportOpen, model: model, metadata: metadata, onImported: handleRefresh })),
|
|
1375
|
+
}) }))) : (_jsx(DataTablePagination, { table: table, pageSizeOptions: metadata.perPageOptions })) })] }), rowActionDialogs, _jsx(AlertDialog, { open: showBulkDeleteConfirm, onOpenChange: (open) => !open && !isBulkDeleting && setShowBulkDeleteConfirm(false), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: isBulkDeleting ? 'Eliminando registros...' : '¿Eliminar múltiples registros?' }), _jsx(AlertDialogDescription, { children: isBulkDeleting ? (_jsxs("div", { className: "space-y-4 mt-4", children: [_jsx(Progress, { value: (bulkDeleteProgress / bulkDeleteTotal) * 100 }), _jsxs("p", { className: "text-center text-sm", children: ["Procesando ", bulkDeleteProgress, " de ", bulkDeleteTotal, " registros..."] })] })) : (_jsxs(_Fragment, { children: ["Esta acci\u00F3n no se puede deshacer. Se eliminar\u00E1n permanentemente ", _jsx("strong", { children: Object.keys(rowSelection).length }), " registro(s) de nuestros servidores."] })) })] }), !isBulkDeleting && (_jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: t('common.cancel') }), _jsx(AlertDialogAction, { onClick: (e) => { e.preventDefault(); confirmBulkDelete(); }, className: "bg-red-600 hover:bg-red-700", children: "Eliminar todos" })] }))] }) }), bulkReasonPrompt.dialog, exportEnabled && (_jsx(ExportDialog, { open: exportOpen, onOpenChange: setExportOpen, model: model, metadata: metadata, currentFilters: buildFilterParams(), hasActiveFilters: hasActiveFilters })), importEnabled && (_jsx(ImportDialog, { open: importOpen, onOpenChange: setImportOpen, model: model, metadata: metadata, onImported: handleRefresh })), _jsxs(DataTableBulkActions, { table: table, entityName: "registro", children: [typeof extraBulkActions === 'function'
|
|
1376
|
+
? extraBulkActions({
|
|
1377
|
+
selectedRows: bulkSelectedRows,
|
|
1378
|
+
selectedIds: bulkSelectedRows.map((r) => r.id).filter((id) => id != null),
|
|
1379
|
+
clearSelection: () => table.resetRowSelection(),
|
|
1380
|
+
refresh: handleRefresh,
|
|
1381
|
+
})
|
|
1382
|
+
: extraBulkActions, !hideBulkDelete && (_jsxs(Button, { variant: "destructive", size: "sm", className: "h-8", onClick: () => setShowBulkDeleteConfirm(true), children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5" }), " Eliminar"] }))] })] }) }));
|
|
1350
1383
|
}
|
package/dist/entity-select.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { LucideIcon } from 'lucide-react';
|
|
2
2
|
/** One searchable option: `value` is the id, `label` the display text. */
|
|
3
3
|
export interface EntitySelectOption {
|
|
4
4
|
value: string;
|
|
@@ -52,5 +52,8 @@ export interface EntitySelectProps {
|
|
|
52
52
|
*/
|
|
53
53
|
lockedCreateFields?: string[];
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* @deprecated Thin wrapper over {@link RecordPicker}; prefer it for new code.
|
|
57
|
+
*/
|
|
58
|
+
export declare function EntitySelect({ model, value, label, onSelect, fetcher, icon, placeholder, searchPlaceholder, emptyText, preload, canCreate, canEdit, endpoint, labelField, disabled, createDefaults, lockedCreateFields, }: EntitySelectProps): import("react").JSX.Element;
|
|
56
59
|
//# sourceMappingURL=entity-select.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-select.d.ts","sourceRoot":"","sources":["../src/entity-select.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"entity-select.d.ts","sourceRoot":"","sources":["../src/entity-select.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAM9C,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sDAAsD;IACtD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC3D,sFAAsF;IACtF,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAE1E,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAChC;AAOD;;GAEG;AACH,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,EACR,OAAO,EACP,IAAI,EACJ,WAA4B,EAC5B,iBAA6B,EAC7B,SAA4B,EAC5B,OAAe,EACf,SAAS,EACT,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,QAAgB,EAChB,cAAc,EACd,kBAAkB,GACrB,EAAE,iBAAiB,+BAmDnB"}
|