next-recomponents 2.0.30 → 2.0.32
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/dist/index.d.mts +0 -27
- package/dist/index.d.ts +0 -27
- package/dist/index.js +168 -245
- package/dist/index.mjs +175 -252
- package/package.json +1 -1
- package/src/table/index.tsx +218 -312
package/src/table/index.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { DataGrid, GridRowModel, GridValidRowModel } from "@mui/x-data-grid";
|
|
4
4
|
import { Box, Dialog } from "@mui/material";
|
|
5
|
-
import React, { useEffect, useMemo,
|
|
5
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
6
6
|
import useExcel from "../use-excel";
|
|
7
7
|
import Button from "../button";
|
|
8
8
|
import regularExpresions from "../regular-expresions";
|
|
@@ -26,7 +26,6 @@ type GridDensity = "compact" | "standard" | "comfortable";
|
|
|
26
26
|
|
|
27
27
|
interface TableProps {
|
|
28
28
|
data: any;
|
|
29
|
-
/** Muestra un input de búsqueda general que filtra filas por palabras clave */
|
|
30
29
|
searchable?: boolean;
|
|
31
30
|
flex?: number;
|
|
32
31
|
editableFields?: string[];
|
|
@@ -48,35 +47,9 @@ interface TableProps {
|
|
|
48
47
|
className?: string;
|
|
49
48
|
fontSize?: string;
|
|
50
49
|
colSize?: Record<string, number>;
|
|
51
|
-
/**
|
|
52
|
-
* Alto fijo de fila en px.
|
|
53
|
-
* Si se omite usa el default de MUI según `density`
|
|
54
|
-
* (compact ≈ 36, standard ≈ 52, comfortable ≈ 68).
|
|
55
|
-
* Se ignora automáticamente cuando `wrapText` es true.
|
|
56
|
-
*/
|
|
57
50
|
rowHeight?: number;
|
|
58
|
-
/**
|
|
59
|
-
* Cuando es true el texto largo hace wrap en lugar de truncarse,
|
|
60
|
-
* y el alto de cada fila crece para mostrar todo el contenido.
|
|
61
|
-
* Internamente activa `getRowHeight={() => "auto"}` en el DataGrid.
|
|
62
|
-
*/
|
|
63
51
|
wrapText?: boolean;
|
|
64
|
-
/**
|
|
65
|
-
* Densidad visual de la tabla.
|
|
66
|
-
* Afecta el padding de celdas y encabezados independientemente
|
|
67
|
-
* del tamaño de fuente.
|
|
68
|
-
* - "compact" → padding mínimo, tabla muy densa
|
|
69
|
-
* - "standard" → (default) comportamiento por defecto de MUI
|
|
70
|
-
* - "comfortable" → padding generoso, fácil de leer
|
|
71
|
-
*/
|
|
72
52
|
density?: GridDensity;
|
|
73
|
-
/**
|
|
74
|
-
* Cuando es true, el alto del contenedor se ajusta automáticamente
|
|
75
|
-
* al contenido de la tabla, eliminando el scroll vertical.
|
|
76
|
-
* Compatible con `wrapText` (filas de alto variable) y con
|
|
77
|
-
* cualquier valor de `density`. Cuando está activo, las props
|
|
78
|
-
* `height` y `HEIGHT_MAP` se ignoran.
|
|
79
|
-
*/
|
|
80
53
|
autoHeight?: boolean;
|
|
81
54
|
[key: string]: any;
|
|
82
55
|
}
|
|
@@ -114,7 +87,7 @@ function EditIcon() {
|
|
|
114
87
|
);
|
|
115
88
|
}
|
|
116
89
|
|
|
117
|
-
// ─── KeyValueTable
|
|
90
|
+
// ─── KeyValueTable ────────────────────────────────────────────────────────────
|
|
118
91
|
|
|
119
92
|
function KeyValueTable({ data }: { data: Record<string, any> }) {
|
|
120
93
|
return (
|
|
@@ -139,7 +112,7 @@ function KeyValueTable({ data }: { data: Record<string, any> }) {
|
|
|
139
112
|
);
|
|
140
113
|
}
|
|
141
114
|
|
|
142
|
-
// ───
|
|
115
|
+
// ─── Footer ───────────────────────────────────────────────────────────────────
|
|
143
116
|
|
|
144
117
|
const FOOTER_LABELS: Record<FooterAggregation, string> = {
|
|
145
118
|
sum: "Suma",
|
|
@@ -160,6 +133,7 @@ function computeAggregation(
|
|
|
160
133
|
return values.length
|
|
161
134
|
? values.reduce((acc, v) => acc + v, 0) / values.length
|
|
162
135
|
: 0;
|
|
136
|
+
|
|
163
137
|
return 0;
|
|
164
138
|
}
|
|
165
139
|
|
|
@@ -171,21 +145,19 @@ function CustomFooter({
|
|
|
171
145
|
footer: FooterType;
|
|
172
146
|
}) {
|
|
173
147
|
const entries = Object.entries(footer);
|
|
148
|
+
|
|
174
149
|
if (!entries.length) return null;
|
|
175
150
|
|
|
176
151
|
return (
|
|
177
152
|
<div className="flex justify-end gap-6 px-4 py-2 bg-gray-100 border-t border-gray-300 text-sm font-semibold text-gray-700">
|
|
178
153
|
{entries.map(([key, type]) => {
|
|
179
154
|
const value = computeAggregation(rows, key, type);
|
|
180
|
-
|
|
181
|
-
minimumFractionDigits: value % 1 !== 0 ? 2 : 0,
|
|
182
|
-
maximumFractionDigits: value % 1 !== 0 ? 2 : 0,
|
|
183
|
-
});
|
|
155
|
+
|
|
184
156
|
return (
|
|
185
157
|
<span key={key}>
|
|
186
158
|
{FOOTER_LABELS[type]} de{" "}
|
|
187
159
|
<span className="text-gray-900">{key}</span>:{" "}
|
|
188
|
-
<span className="text-blue-700">{
|
|
160
|
+
<span className="text-blue-700">{value.toLocaleString()}</span>
|
|
189
161
|
</span>
|
|
190
162
|
);
|
|
191
163
|
})}
|
|
@@ -193,31 +165,67 @@ function CustomFooter({
|
|
|
193
165
|
);
|
|
194
166
|
}
|
|
195
167
|
|
|
196
|
-
// ───
|
|
168
|
+
// ─── Modal ────────────────────────────────────────────────────────────────────
|
|
197
169
|
|
|
198
170
|
interface ModalDialogProps {
|
|
199
171
|
open: boolean;
|
|
200
172
|
onClose: () => void;
|
|
201
173
|
modal: React.ReactNode;
|
|
202
174
|
selectedRow: GridValidRowModel | undefined;
|
|
175
|
+
onPrev: () => void;
|
|
176
|
+
onNext: () => void;
|
|
177
|
+
hasPrev: boolean;
|
|
178
|
+
hasNext: boolean;
|
|
203
179
|
}
|
|
204
180
|
|
|
205
|
-
function ModalDialog({
|
|
181
|
+
function ModalDialog({
|
|
182
|
+
open,
|
|
183
|
+
onClose,
|
|
184
|
+
modal,
|
|
185
|
+
selectedRow,
|
|
186
|
+
onPrev,
|
|
187
|
+
onNext,
|
|
188
|
+
hasPrev,
|
|
189
|
+
hasNext,
|
|
190
|
+
}: ModalDialogProps) {
|
|
206
191
|
return (
|
|
207
192
|
<Dialog open={open} maxWidth="xl" fullWidth>
|
|
208
|
-
<div className="flex justify-
|
|
193
|
+
<div className="flex items-center justify-between p-4 border-b">
|
|
194
|
+
<div className="flex gap-2">
|
|
195
|
+
<button
|
|
196
|
+
disabled={!hasPrev}
|
|
197
|
+
onClick={onPrev}
|
|
198
|
+
className="px-4 py-2 rounded border bg-gray-100 hover:bg-gray-200 disabled:opacity-40"
|
|
199
|
+
>
|
|
200
|
+
← Anterior
|
|
201
|
+
</button>
|
|
202
|
+
|
|
203
|
+
<button
|
|
204
|
+
disabled={!hasNext}
|
|
205
|
+
onClick={onNext}
|
|
206
|
+
className="px-4 py-2 rounded border bg-gray-100 hover:bg-gray-200 disabled:opacity-40"
|
|
207
|
+
>
|
|
208
|
+
Siguiente →
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
209
212
|
<button
|
|
210
213
|
onClick={onClose}
|
|
211
|
-
className="
|
|
214
|
+
className="font-bold w-[35px] h-[35px] flex items-center justify-center hover:animate-pulse border shadow rounded bg-red-500 text-white"
|
|
212
215
|
>
|
|
213
216
|
×
|
|
214
217
|
</button>
|
|
215
218
|
</div>
|
|
219
|
+
|
|
216
220
|
<div className="mt-4 m-auto p-5">
|
|
217
221
|
{selectedRow &&
|
|
218
222
|
React.cloneElement(
|
|
219
223
|
modal as React.ReactElement,
|
|
220
|
-
{
|
|
224
|
+
{
|
|
225
|
+
key: selectedRow.id,
|
|
226
|
+
row: selectedRow,
|
|
227
|
+
hide: onClose,
|
|
228
|
+
} as any,
|
|
221
229
|
)}
|
|
222
230
|
</div>
|
|
223
231
|
</Dialog>
|
|
@@ -226,21 +234,7 @@ function ModalDialog({ open, onClose, modal, selectedRow }: ModalDialogProps) {
|
|
|
226
234
|
|
|
227
235
|
// ─── Toolbar ──────────────────────────────────────────────────────────────────
|
|
228
236
|
|
|
229
|
-
|
|
230
|
-
exportName?: string;
|
|
231
|
-
onSave?: (data: GridValidRowModel[]) => void;
|
|
232
|
-
onSelect?: (data: GridValidRowModel[]) => void;
|
|
233
|
-
rows: GridValidRowModel[];
|
|
234
|
-
filteredRows: GridValidRowModel[];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function Toolbar({
|
|
238
|
-
exportName,
|
|
239
|
-
onSave,
|
|
240
|
-
onSelect,
|
|
241
|
-
rows,
|
|
242
|
-
filteredRows,
|
|
243
|
-
}: ToolbarProps) {
|
|
237
|
+
function Toolbar({ exportName, onSave, onSelect, rows, filteredRows }: any) {
|
|
244
238
|
const excel = useExcel();
|
|
245
239
|
|
|
246
240
|
const stripMeta = (r: GridValidRowModel) => {
|
|
@@ -251,7 +245,7 @@ function Toolbar({
|
|
|
251
245
|
if (!exportName && !onSave && !onSelect) return null;
|
|
252
246
|
|
|
253
247
|
return (
|
|
254
|
-
<div className="flex gap-2
|
|
248
|
+
<div className="flex gap-2 p-2 text-xs">
|
|
255
249
|
{exportName && (
|
|
256
250
|
<Button
|
|
257
251
|
className="bg-green-800 text-white"
|
|
@@ -262,6 +256,7 @@ function Toolbar({
|
|
|
262
256
|
Exportar
|
|
263
257
|
</Button>
|
|
264
258
|
)}
|
|
259
|
+
|
|
265
260
|
{onSelect ? (
|
|
266
261
|
<Button
|
|
267
262
|
disabled={filteredRows.length === 0}
|
|
@@ -279,158 +274,7 @@ function Toolbar({
|
|
|
279
274
|
);
|
|
280
275
|
}
|
|
281
276
|
|
|
282
|
-
// ───
|
|
283
|
-
|
|
284
|
-
function useColumns(
|
|
285
|
-
rows: GridValidRowModel[],
|
|
286
|
-
currentCoin: string,
|
|
287
|
-
options: {
|
|
288
|
-
flex: number;
|
|
289
|
-
editableFields?: string[];
|
|
290
|
-
buttons?: Record<string, any>;
|
|
291
|
-
hideColumns: string[];
|
|
292
|
-
modal?: React.ReactNode;
|
|
293
|
-
wrapText?: boolean;
|
|
294
|
-
handleRowUpdate: (row: GridRowModel) => GridRowModel;
|
|
295
|
-
onModalOpen: (row: GridValidRowModel) => void;
|
|
296
|
-
},
|
|
297
|
-
colSize?: Record<string, number>,
|
|
298
|
-
) {
|
|
299
|
-
const {
|
|
300
|
-
flex,
|
|
301
|
-
editableFields,
|
|
302
|
-
buttons,
|
|
303
|
-
hideColumns,
|
|
304
|
-
modal,
|
|
305
|
-
wrapText,
|
|
306
|
-
handleRowUpdate,
|
|
307
|
-
onModalOpen,
|
|
308
|
-
} = options;
|
|
309
|
-
|
|
310
|
-
return useMemo(() => {
|
|
311
|
-
if (!rows.length) return [];
|
|
312
|
-
|
|
313
|
-
const cols = Object.keys(rows[0])
|
|
314
|
-
.filter((key) => !key.startsWith("_") && !hideColumns.includes(key))
|
|
315
|
-
.map((key) => ({
|
|
316
|
-
field: key,
|
|
317
|
-
headerName: key,
|
|
318
|
-
valueFormatter: (value: any) => {
|
|
319
|
-
if (value == null || value === "") return "";
|
|
320
|
-
const isDate = /(\d{4}-\d{2}-\d{2})(T[\d:,.+-]*(Z)?)?/;
|
|
321
|
-
if (`${value}`.match(isDate)) {
|
|
322
|
-
return value
|
|
323
|
-
.toString()
|
|
324
|
-
.split("T")[0]
|
|
325
|
-
.split("-")
|
|
326
|
-
.reverse()
|
|
327
|
-
.join("/");
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const splited = `${value}`.split(".");
|
|
331
|
-
const hasDecimals =
|
|
332
|
-
splited.length == 2 &&
|
|
333
|
-
splited.every((v: any) => `${v}`.match(regularExpresions.number));
|
|
334
|
-
|
|
335
|
-
if (hasDecimals) {
|
|
336
|
-
return [
|
|
337
|
-
currentCoin,
|
|
338
|
-
(+`${value}`).toLocaleString("en-US", {
|
|
339
|
-
minimumFractionDigits: 2,
|
|
340
|
-
maximumFractionDigits: 2,
|
|
341
|
-
}),
|
|
342
|
-
].join(" ");
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const isNumber = typeof value === "number";
|
|
346
|
-
|
|
347
|
-
if (isNumber) {
|
|
348
|
-
if (isNaN(value)) return value;
|
|
349
|
-
return value;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return value;
|
|
353
|
-
},
|
|
354
|
-
flex: key == "id" ? false : !colSize?.[key],
|
|
355
|
-
width: key == "id" ? 80 : (colSize?.[key] ?? undefined),
|
|
356
|
-
editable: editableFields?.includes(key) ?? false,
|
|
357
|
-
type: typeof rows[0][key] === "number" ? "number" : "string",
|
|
358
|
-
// When wrapText is enabled, allow cells to grow vertically
|
|
359
|
-
...(wrapText && {
|
|
360
|
-
renderHeader: (params: any) => (
|
|
361
|
-
<span
|
|
362
|
-
style={{
|
|
363
|
-
whiteSpace: "normal",
|
|
364
|
-
lineHeight: 1.3,
|
|
365
|
-
wordBreak: "break-word",
|
|
366
|
-
}}
|
|
367
|
-
>
|
|
368
|
-
{params.colDef.headerName}
|
|
369
|
-
</span>
|
|
370
|
-
),
|
|
371
|
-
}),
|
|
372
|
-
renderCell: buttons?.[key]
|
|
373
|
-
? (params: any) => {
|
|
374
|
-
const children =
|
|
375
|
-
buttons?.[key]?.type == "input" ? null : params?.row?.[key];
|
|
376
|
-
|
|
377
|
-
return React.cloneElement(buttons[key], {
|
|
378
|
-
className: `${params?.className ?? ""} m-auto text-xs`,
|
|
379
|
-
children,
|
|
380
|
-
row: params?.row,
|
|
381
|
-
onClick: async (e: TableButtonProps) => {
|
|
382
|
-
e.row = params?.row;
|
|
383
|
-
if (buttons[key]?.props?.onClick) {
|
|
384
|
-
const newVal = await buttons[key].props.onClick(e);
|
|
385
|
-
|
|
386
|
-
if (newVal) handleRowUpdate({ ...e.row, newVal });
|
|
387
|
-
}
|
|
388
|
-
},
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
: wrapText
|
|
392
|
-
? (params: any) => (
|
|
393
|
-
// Plain cell with wrap — no custom button
|
|
394
|
-
<span
|
|
395
|
-
style={{
|
|
396
|
-
whiteSpace: "normal",
|
|
397
|
-
wordBreak: "break-word",
|
|
398
|
-
lineHeight: 1.4,
|
|
399
|
-
padding: "6px 0",
|
|
400
|
-
display: "block",
|
|
401
|
-
}}
|
|
402
|
-
>
|
|
403
|
-
{params.formattedValue ?? params.value}
|
|
404
|
-
</span>
|
|
405
|
-
)
|
|
406
|
-
: null,
|
|
407
|
-
}));
|
|
408
|
-
|
|
409
|
-
if (modal) {
|
|
410
|
-
cols.unshift({
|
|
411
|
-
field: "Modal",
|
|
412
|
-
headerName: "Modal",
|
|
413
|
-
editable: false,
|
|
414
|
-
type: "string",
|
|
415
|
-
width: 10,
|
|
416
|
-
renderCell: (params: any) =>
|
|
417
|
-
(
|
|
418
|
-
<Button
|
|
419
|
-
className="text-xs"
|
|
420
|
-
onClick={() => onModalOpen(params.row)}
|
|
421
|
-
icon={<EditIcon />}
|
|
422
|
-
>
|
|
423
|
-
{params?.row?.["Modal"]}
|
|
424
|
-
</Button>
|
|
425
|
-
) as any,
|
|
426
|
-
} as any);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return cols;
|
|
430
|
-
}, [rows, wrapText]);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// ─── SearchBar ────────────────────────────────────────────────────────────────
|
|
277
|
+
// ─── Search ───────────────────────────────────────────────────────────────────
|
|
434
278
|
|
|
435
279
|
function SearchBar({
|
|
436
280
|
value,
|
|
@@ -441,50 +285,32 @@ function SearchBar({
|
|
|
441
285
|
}) {
|
|
442
286
|
return (
|
|
443
287
|
<div className="flex items-center gap-2 px-2 pb-1">
|
|
444
|
-
<svg
|
|
445
|
-
className="text-gray-400 shrink-0"
|
|
446
|
-
width="14"
|
|
447
|
-
height="14"
|
|
448
|
-
viewBox="0 0 20 20"
|
|
449
|
-
fill="currentColor"
|
|
450
|
-
>
|
|
451
|
-
<path
|
|
452
|
-
fillRule="evenodd"
|
|
453
|
-
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
|
454
|
-
clipRule="evenodd"
|
|
455
|
-
/>
|
|
456
|
-
</svg>
|
|
457
288
|
<input
|
|
458
289
|
type="text"
|
|
459
290
|
value={value}
|
|
460
291
|
onChange={(e) => onChange(e.target.value)}
|
|
461
292
|
placeholder="Buscar…"
|
|
462
|
-
className="w-full max-w-xs text-xs border border-gray-300 rounded px-2 py-1
|
|
293
|
+
className="w-full max-w-xs text-xs border border-gray-300 rounded px-2 py-1"
|
|
463
294
|
/>
|
|
464
|
-
{value && (
|
|
465
|
-
<button
|
|
466
|
-
onClick={() => onChange("")}
|
|
467
|
-
className="text-gray-400 hover:text-gray-600 text-sm leading-none"
|
|
468
|
-
title="Limpiar búsqueda"
|
|
469
|
-
>
|
|
470
|
-
×
|
|
471
|
-
</button>
|
|
472
|
-
)}
|
|
473
295
|
</div>
|
|
474
296
|
);
|
|
475
297
|
}
|
|
476
298
|
|
|
477
|
-
/** Devuelve true si la fila contiene TODAS las palabras del query */
|
|
478
299
|
function rowMatchesSearch(row: GridValidRowModel, query: string): boolean {
|
|
479
300
|
if (!query.trim()) return true;
|
|
301
|
+
|
|
480
302
|
const words = query.trim().toLowerCase().split(/\s+/);
|
|
303
|
+
|
|
481
304
|
const rowText = Object.values(row)
|
|
482
305
|
.filter((v) => v != null && v !== "")
|
|
483
306
|
.join(" ")
|
|
484
307
|
.toLowerCase();
|
|
308
|
+
|
|
485
309
|
return words.every((word) => rowText.includes(word));
|
|
486
310
|
}
|
|
487
311
|
|
|
312
|
+
// ─── Main Table ───────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
488
314
|
function IHTable({
|
|
489
315
|
data,
|
|
490
316
|
flex = 1,
|
|
@@ -510,108 +336,203 @@ function IHTable({
|
|
|
510
336
|
searchable = false,
|
|
511
337
|
autoHeight = false,
|
|
512
338
|
}: TableProps) {
|
|
513
|
-
if (modal && onSelect)
|
|
514
|
-
throw new Error("Solo se puede usar modal o onSelect por separado");
|
|
515
|
-
|
|
516
339
|
const [open, setOpen] = useState(false);
|
|
340
|
+
|
|
517
341
|
const [rows, setRows] = useState<GridValidRowModel[]>(data);
|
|
342
|
+
|
|
343
|
+
const [modalIndex, setModalIndex] = useState<number | null>(null);
|
|
344
|
+
|
|
518
345
|
const [selectedRows, setSelectedRows] = useState<any>({
|
|
519
346
|
type: "include",
|
|
520
347
|
ids: new Set(),
|
|
521
348
|
});
|
|
522
|
-
const [modalRow, setModalRow] = useState<GridValidRowModel | undefined>();
|
|
523
|
-
const [searchQuery, setSearchQuery] = useState("");
|
|
524
349
|
|
|
525
|
-
|
|
526
|
-
const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
|
|
350
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
527
351
|
|
|
528
352
|
useEffect(() => {
|
|
529
353
|
setRows(data);
|
|
530
354
|
}, [data]);
|
|
531
355
|
|
|
356
|
+
const filteredRows = useMemo(() => {
|
|
357
|
+
if (selectedRows?.type === "exclude") {
|
|
358
|
+
return rows.filter((r) => !Array.from(selectedRows.ids).includes(r.id));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (selectedRows?.type === "include") {
|
|
362
|
+
return rows.filter((r) => Array.from(selectedRows.ids).includes(r.id));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return [];
|
|
366
|
+
}, [selectedRows, rows]);
|
|
367
|
+
|
|
368
|
+
const displayRows = useMemo(
|
|
369
|
+
() =>
|
|
370
|
+
searchable ? rows.filter((r) => rowMatchesSearch(r, searchQuery)) : rows,
|
|
371
|
+
[rows, searchQuery, searchable],
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const modalRow = modalIndex != null ? displayRows[modalIndex] : undefined;
|
|
375
|
+
|
|
532
376
|
const handleModalOpen = (row: GridValidRowModel) => {
|
|
533
|
-
|
|
377
|
+
const index = displayRows.findIndex((r) => r.id === row.id);
|
|
378
|
+
|
|
379
|
+
if (index === -1) return;
|
|
380
|
+
|
|
381
|
+
setModalIndex(index);
|
|
534
382
|
setOpen(true);
|
|
535
383
|
};
|
|
384
|
+
|
|
536
385
|
const handleClose = async () => {
|
|
537
|
-
const pass = onCloseModal ? await onCloseModal
|
|
386
|
+
const pass = onCloseModal ? await onCloseModal(modalRow) : true;
|
|
538
387
|
|
|
539
388
|
if (!pass) return;
|
|
389
|
+
|
|
540
390
|
setOpen(false);
|
|
541
|
-
|
|
391
|
+
setModalIndex(null);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const handlePrevRow = () => {
|
|
395
|
+
setModalIndex((prev) => {
|
|
396
|
+
if (prev == null) return prev;
|
|
397
|
+
return Math.max(prev - 1, 0);
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const handleNextRow = () => {
|
|
402
|
+
setModalIndex((prev) => {
|
|
403
|
+
if (prev == null) return prev;
|
|
404
|
+
return Math.min(prev + 1, displayRows.length - 1);
|
|
405
|
+
});
|
|
542
406
|
};
|
|
543
407
|
|
|
544
408
|
const handleRowUpdate = (newRow: GridRowModel) => {
|
|
545
409
|
if (!newRow.id) throw new Error("Fila sin id");
|
|
546
|
-
|
|
410
|
+
|
|
411
|
+
const updated: any = { ...newRow, _edited: true };
|
|
412
|
+
|
|
547
413
|
setRows((prev) =>
|
|
548
414
|
prev.map((row) => (row.id === updated.id ? updated : row)),
|
|
549
415
|
);
|
|
416
|
+
|
|
550
417
|
return updated;
|
|
551
418
|
};
|
|
552
419
|
|
|
553
|
-
const
|
|
554
|
-
if (selectedRows?.type === "exclude") {
|
|
555
|
-
return rows.filter((r) => !Array.from(selectedRows.ids).includes(r.id));
|
|
556
|
-
}
|
|
557
|
-
if (selectedRows?.type === "include") {
|
|
558
|
-
return rows.filter((r) => Array.from(selectedRows.ids).includes(r.id));
|
|
559
|
-
}
|
|
560
|
-
return [];
|
|
561
|
-
}, [selectedRows, rows]);
|
|
420
|
+
const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
|
|
562
421
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
() =>
|
|
566
|
-
searchable ? rows.filter((r) => rowMatchesSearch(r, searchQuery)) : rows,
|
|
567
|
-
[rows, searchQuery, searchable],
|
|
568
|
-
);
|
|
422
|
+
const columns = useMemo(() => {
|
|
423
|
+
if (!displayRows.length) return [];
|
|
569
424
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
425
|
+
const cols = Object.keys(displayRows[0])
|
|
426
|
+
.filter((key) => !key.startsWith("_") && !hideColumns.includes(key))
|
|
427
|
+
.map((key) => ({
|
|
428
|
+
field: key,
|
|
429
|
+
headerName: key,
|
|
430
|
+
flex: key == "id" ? false : !colSize?.[key],
|
|
431
|
+
width: key == "id" ? 80 : (colSize?.[key] ?? undefined),
|
|
432
|
+
editable: editableFields?.includes(key) ?? false,
|
|
433
|
+
type: typeof displayRows[0][key] === "number" ? "number" : "string",
|
|
434
|
+
|
|
435
|
+
valueFormatter: (value: any) => {
|
|
436
|
+
if (value == null || value === "") return "";
|
|
437
|
+
|
|
438
|
+
const isDate = /(\d{4}-\d{2}-\d{2})(T[\d:,.+-]*(Z)?)?/;
|
|
439
|
+
|
|
440
|
+
if (`${value}`.match(isDate)) {
|
|
441
|
+
return value
|
|
442
|
+
.toString()
|
|
443
|
+
.split("T")[0]
|
|
444
|
+
.split("-")
|
|
445
|
+
.reverse()
|
|
446
|
+
.join("/");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const splited = `${value}`.split(".");
|
|
450
|
+
|
|
451
|
+
const hasDecimals =
|
|
452
|
+
splited.length == 2 &&
|
|
453
|
+
splited.every((v: any) => `${v}`.match(regularExpresions.number));
|
|
454
|
+
|
|
455
|
+
if (hasDecimals) {
|
|
456
|
+
return [
|
|
457
|
+
currentCoin,
|
|
458
|
+
(+`${value}`).toLocaleString("en-US", {
|
|
459
|
+
minimumFractionDigits: 2,
|
|
460
|
+
maximumFractionDigits: 2,
|
|
461
|
+
}),
|
|
462
|
+
].join(" ");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return value;
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
renderCell: resolvedButtons?.[key]
|
|
469
|
+
? (params: any) => {
|
|
470
|
+
const children =
|
|
471
|
+
resolvedButtons?.[key]?.type == "input"
|
|
472
|
+
? null
|
|
473
|
+
: params?.row?.[key];
|
|
474
|
+
|
|
475
|
+
return React.cloneElement(resolvedButtons[key], {
|
|
476
|
+
className: `${params?.className ?? ""} m-auto text-xs`,
|
|
477
|
+
children,
|
|
478
|
+
row: params?.row,
|
|
479
|
+
|
|
480
|
+
onClick: async (e: TableButtonProps) => {
|
|
481
|
+
e.row = params?.row;
|
|
482
|
+
|
|
483
|
+
if (resolvedButtons[key]?.props?.onClick) {
|
|
484
|
+
const newVal = await resolvedButtons[key].props.onClick(e);
|
|
485
|
+
|
|
486
|
+
if (newVal) {
|
|
487
|
+
handleRowUpdate({ ...e.row, newVal });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
: null,
|
|
494
|
+
}));
|
|
495
|
+
|
|
496
|
+
if (modal) {
|
|
497
|
+
cols.unshift({
|
|
498
|
+
field: "Modal",
|
|
499
|
+
headerName: "Modal",
|
|
500
|
+
width: 100,
|
|
501
|
+
|
|
502
|
+
renderCell: (params: any) => (
|
|
503
|
+
<Button
|
|
504
|
+
className="text-xs"
|
|
505
|
+
onClick={() => handleModalOpen(params.row)}
|
|
506
|
+
icon={<EditIcon />}
|
|
507
|
+
>
|
|
508
|
+
Abrir
|
|
509
|
+
</Button>
|
|
510
|
+
),
|
|
511
|
+
} as any);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return cols;
|
|
515
|
+
}, [displayRows]);
|
|
585
516
|
|
|
586
517
|
if (!rows.length) return null;
|
|
587
518
|
|
|
588
|
-
// ─── Row height props ──────────────────────────────────────────────────────
|
|
589
|
-
// Priority: wrapText always wins (variable row height).
|
|
590
|
-
// autoHeight delegates height control to MUI; rowHeight prop is still
|
|
591
|
-
// respected when neither wrapText nor autoHeight forces auto sizing.
|
|
592
519
|
const rowHeightProps = wrapText
|
|
593
520
|
? { getRowHeight: () => "auto" as const }
|
|
594
521
|
: rowHeight != null
|
|
595
522
|
? { rowHeight }
|
|
596
523
|
: {};
|
|
597
524
|
|
|
598
|
-
// ─── Container height ──────────────────────────────────────────────────────
|
|
599
|
-
// When autoHeight is active the Box must NOT constrain the height so that
|
|
600
|
-
// MUI's own autoHeight can expand the grid to fit all rows without scroll.
|
|
601
|
-
// We still honour HEIGHT_MAP for small datasets when autoHeight is off,
|
|
602
|
-
// falling back to the explicit `height` prop for larger ones.
|
|
603
525
|
const containerHeight = autoHeight
|
|
604
|
-
? undefined
|
|
526
|
+
? undefined
|
|
605
527
|
: (HEIGHT_MAP[displayRows.length] ?? height);
|
|
606
528
|
|
|
607
|
-
// Whether to hide MUI's built-in pagination footer.
|
|
608
|
-
// With autoHeight all rows are visible, so pagination is never needed.
|
|
609
529
|
const hideFooter =
|
|
610
530
|
autoHeight || displayRows.length <= Object.keys(HEIGHT_MAP).length;
|
|
611
531
|
|
|
612
532
|
return (
|
|
613
533
|
<div className="m-2">
|
|
614
|
-
{header && <div className="font-bold
|
|
534
|
+
{header && <div className="font-bold p-2">{header}</div>}
|
|
535
|
+
|
|
615
536
|
<div className="flex justify-between">
|
|
616
537
|
<Toolbar
|
|
617
538
|
exportName={exportName}
|
|
@@ -620,6 +541,7 @@ function IHTable({
|
|
|
620
541
|
rows={rows}
|
|
621
542
|
filteredRows={filteredRows}
|
|
622
543
|
/>
|
|
544
|
+
|
|
623
545
|
{searchable && (
|
|
624
546
|
<SearchBar value={searchQuery} onChange={setSearchQuery} />
|
|
625
547
|
)}
|
|
@@ -629,10 +551,8 @@ function IHTable({
|
|
|
629
551
|
sx={{
|
|
630
552
|
display: "flex",
|
|
631
553
|
flexDirection: "column",
|
|
632
|
-
// undefined height lets the child DataGrid size itself freely
|
|
633
554
|
height: containerHeight,
|
|
634
555
|
width,
|
|
635
|
-
zIndex: 999999999,
|
|
636
556
|
}}
|
|
637
557
|
>
|
|
638
558
|
{modal && (
|
|
@@ -641,6 +561,10 @@ function IHTable({
|
|
|
641
561
|
onClose={handleClose}
|
|
642
562
|
modal={modal}
|
|
643
563
|
selectedRow={modalRow}
|
|
564
|
+
onPrev={handlePrevRow}
|
|
565
|
+
onNext={handleNextRow}
|
|
566
|
+
hasPrev={(modalIndex ?? 0) > 0}
|
|
567
|
+
hasNext={(modalIndex ?? -1) < displayRows.length - 1}
|
|
644
568
|
/>
|
|
645
569
|
)}
|
|
646
570
|
|
|
@@ -649,8 +573,6 @@ function IHTable({
|
|
|
649
573
|
rows={displayRows}
|
|
650
574
|
columns={columns as any}
|
|
651
575
|
density={density}
|
|
652
|
-
// MUI's native autoHeight prop — renders all rows and sizes the
|
|
653
|
-
// grid to fit them, regardless of density or wrapText.
|
|
654
576
|
autoHeight={autoHeight}
|
|
655
577
|
checkboxSelection={Boolean(onSelect)}
|
|
656
578
|
rowSelectionModel={selectedRows}
|
|
@@ -658,37 +580,19 @@ function IHTable({
|
|
|
658
580
|
{...rowHeightProps}
|
|
659
581
|
sx={{
|
|
660
582
|
fontSize,
|
|
583
|
+
|
|
661
584
|
"& .MuiDataGrid-cell": {
|
|
662
585
|
fontSize,
|
|
663
|
-
// Allow cells to wrap text when wrapText is enabled
|
|
664
|
-
...(wrapText && {
|
|
665
|
-
alignItems: "flex-start",
|
|
666
|
-
paddingTop: "8px",
|
|
667
|
-
paddingBottom: "8px",
|
|
668
|
-
whiteSpace: "normal",
|
|
669
|
-
wordBreak: "break-word",
|
|
670
|
-
}),
|
|
671
586
|
},
|
|
587
|
+
|
|
672
588
|
"& .MuiDataGrid-columnHeader": {
|
|
673
589
|
fontSize,
|
|
674
|
-
...(wrapText && {
|
|
675
|
-
whiteSpace: "normal",
|
|
676
|
-
"& .MuiDataGrid-columnHeaderTitle": {
|
|
677
|
-
whiteSpace: "normal",
|
|
678
|
-
lineHeight: 1.3,
|
|
679
|
-
wordBreak: "break-word",
|
|
680
|
-
},
|
|
681
|
-
}),
|
|
682
590
|
},
|
|
591
|
+
|
|
683
592
|
"& .MuiDataGrid-cell--editable": {
|
|
684
593
|
backgroundColor: "#c6d8f0",
|
|
685
594
|
fontWeight: 500,
|
|
686
595
|
},
|
|
687
|
-
...(displayRows.length <= Object.keys(HEIGHT_MAP).length && {
|
|
688
|
-
"& .MuiDataGrid-filler": {
|
|
689
|
-
display: "none",
|
|
690
|
-
},
|
|
691
|
-
}),
|
|
692
596
|
}}
|
|
693
597
|
editMode="row"
|
|
694
598
|
processRowUpdate={handleRowUpdate}
|
|
@@ -696,16 +600,18 @@ function IHTable({
|
|
|
696
600
|
hideFooter={hideFooter}
|
|
697
601
|
/>
|
|
698
602
|
</Box>
|
|
603
|
+
|
|
699
604
|
<CustomFooter footer={footer} rows={displayRows} />
|
|
700
605
|
</div>
|
|
701
606
|
);
|
|
702
607
|
}
|
|
703
608
|
|
|
704
|
-
// ───
|
|
609
|
+
// ─── Export ───────────────────────────────────────────────────────────────────
|
|
705
610
|
|
|
706
611
|
export default function Table(props: TableProps) {
|
|
707
612
|
if (Array.isArray(props.data)) {
|
|
708
613
|
return <IHTable {...props} />;
|
|
709
614
|
}
|
|
615
|
+
|
|
710
616
|
return <KeyValueTable data={props.data} />;
|
|
711
617
|
}
|