next-recomponents 2.0.30 → 2.0.31
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 +167 -245
- package/dist/index.mjs +174 -252
- package/package.json +1 -1
- package/src/table/index.tsx +217 -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,66 @@ 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
|
+
row: selectedRow,
|
|
226
|
+
hide: onClose,
|
|
227
|
+
} as any,
|
|
221
228
|
)}
|
|
222
229
|
</div>
|
|
223
230
|
</Dialog>
|
|
@@ -226,21 +233,7 @@ function ModalDialog({ open, onClose, modal, selectedRow }: ModalDialogProps) {
|
|
|
226
233
|
|
|
227
234
|
// ─── Toolbar ──────────────────────────────────────────────────────────────────
|
|
228
235
|
|
|
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) {
|
|
236
|
+
function Toolbar({ exportName, onSave, onSelect, rows, filteredRows }: any) {
|
|
244
237
|
const excel = useExcel();
|
|
245
238
|
|
|
246
239
|
const stripMeta = (r: GridValidRowModel) => {
|
|
@@ -251,7 +244,7 @@ function Toolbar({
|
|
|
251
244
|
if (!exportName && !onSave && !onSelect) return null;
|
|
252
245
|
|
|
253
246
|
return (
|
|
254
|
-
<div className="flex gap-2
|
|
247
|
+
<div className="flex gap-2 p-2 text-xs">
|
|
255
248
|
{exportName && (
|
|
256
249
|
<Button
|
|
257
250
|
className="bg-green-800 text-white"
|
|
@@ -262,6 +255,7 @@ function Toolbar({
|
|
|
262
255
|
Exportar
|
|
263
256
|
</Button>
|
|
264
257
|
)}
|
|
258
|
+
|
|
265
259
|
{onSelect ? (
|
|
266
260
|
<Button
|
|
267
261
|
disabled={filteredRows.length === 0}
|
|
@@ -279,158 +273,7 @@ function Toolbar({
|
|
|
279
273
|
);
|
|
280
274
|
}
|
|
281
275
|
|
|
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 ────────────────────────────────────────────────────────────────
|
|
276
|
+
// ─── Search ───────────────────────────────────────────────────────────────────
|
|
434
277
|
|
|
435
278
|
function SearchBar({
|
|
436
279
|
value,
|
|
@@ -441,50 +284,32 @@ function SearchBar({
|
|
|
441
284
|
}) {
|
|
442
285
|
return (
|
|
443
286
|
<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
287
|
<input
|
|
458
288
|
type="text"
|
|
459
289
|
value={value}
|
|
460
290
|
onChange={(e) => onChange(e.target.value)}
|
|
461
291
|
placeholder="Buscar…"
|
|
462
|
-
className="w-full max-w-xs text-xs border border-gray-300 rounded px-2 py-1
|
|
292
|
+
className="w-full max-w-xs text-xs border border-gray-300 rounded px-2 py-1"
|
|
463
293
|
/>
|
|
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
294
|
</div>
|
|
474
295
|
);
|
|
475
296
|
}
|
|
476
297
|
|
|
477
|
-
/** Devuelve true si la fila contiene TODAS las palabras del query */
|
|
478
298
|
function rowMatchesSearch(row: GridValidRowModel, query: string): boolean {
|
|
479
299
|
if (!query.trim()) return true;
|
|
300
|
+
|
|
480
301
|
const words = query.trim().toLowerCase().split(/\s+/);
|
|
302
|
+
|
|
481
303
|
const rowText = Object.values(row)
|
|
482
304
|
.filter((v) => v != null && v !== "")
|
|
483
305
|
.join(" ")
|
|
484
306
|
.toLowerCase();
|
|
307
|
+
|
|
485
308
|
return words.every((word) => rowText.includes(word));
|
|
486
309
|
}
|
|
487
310
|
|
|
311
|
+
// ─── Main Table ───────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
488
313
|
function IHTable({
|
|
489
314
|
data,
|
|
490
315
|
flex = 1,
|
|
@@ -510,108 +335,203 @@ function IHTable({
|
|
|
510
335
|
searchable = false,
|
|
511
336
|
autoHeight = false,
|
|
512
337
|
}: TableProps) {
|
|
513
|
-
if (modal && onSelect)
|
|
514
|
-
throw new Error("Solo se puede usar modal o onSelect por separado");
|
|
515
|
-
|
|
516
338
|
const [open, setOpen] = useState(false);
|
|
339
|
+
|
|
517
340
|
const [rows, setRows] = useState<GridValidRowModel[]>(data);
|
|
341
|
+
|
|
342
|
+
const [modalIndex, setModalIndex] = useState<number | null>(null);
|
|
343
|
+
|
|
518
344
|
const [selectedRows, setSelectedRows] = useState<any>({
|
|
519
345
|
type: "include",
|
|
520
346
|
ids: new Set(),
|
|
521
347
|
});
|
|
522
|
-
const [modalRow, setModalRow] = useState<GridValidRowModel | undefined>();
|
|
523
|
-
const [searchQuery, setSearchQuery] = useState("");
|
|
524
348
|
|
|
525
|
-
|
|
526
|
-
const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
|
|
349
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
527
350
|
|
|
528
351
|
useEffect(() => {
|
|
529
352
|
setRows(data);
|
|
530
353
|
}, [data]);
|
|
531
354
|
|
|
355
|
+
const filteredRows = useMemo(() => {
|
|
356
|
+
if (selectedRows?.type === "exclude") {
|
|
357
|
+
return rows.filter((r) => !Array.from(selectedRows.ids).includes(r.id));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (selectedRows?.type === "include") {
|
|
361
|
+
return rows.filter((r) => Array.from(selectedRows.ids).includes(r.id));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return [];
|
|
365
|
+
}, [selectedRows, rows]);
|
|
366
|
+
|
|
367
|
+
const displayRows = useMemo(
|
|
368
|
+
() =>
|
|
369
|
+
searchable ? rows.filter((r) => rowMatchesSearch(r, searchQuery)) : rows,
|
|
370
|
+
[rows, searchQuery, searchable],
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const modalRow = modalIndex != null ? displayRows[modalIndex] : undefined;
|
|
374
|
+
|
|
532
375
|
const handleModalOpen = (row: GridValidRowModel) => {
|
|
533
|
-
|
|
376
|
+
const index = displayRows.findIndex((r) => r.id === row.id);
|
|
377
|
+
|
|
378
|
+
if (index === -1) return;
|
|
379
|
+
|
|
380
|
+
setModalIndex(index);
|
|
534
381
|
setOpen(true);
|
|
535
382
|
};
|
|
383
|
+
|
|
536
384
|
const handleClose = async () => {
|
|
537
|
-
const pass = onCloseModal ? await onCloseModal
|
|
385
|
+
const pass = onCloseModal ? await onCloseModal(modalRow) : true;
|
|
538
386
|
|
|
539
387
|
if (!pass) return;
|
|
388
|
+
|
|
540
389
|
setOpen(false);
|
|
541
|
-
|
|
390
|
+
setModalIndex(null);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const handlePrevRow = () => {
|
|
394
|
+
setModalIndex((prev) => {
|
|
395
|
+
if (prev == null) return prev;
|
|
396
|
+
return Math.max(prev - 1, 0);
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const handleNextRow = () => {
|
|
401
|
+
setModalIndex((prev) => {
|
|
402
|
+
if (prev == null) return prev;
|
|
403
|
+
return Math.min(prev + 1, displayRows.length - 1);
|
|
404
|
+
});
|
|
542
405
|
};
|
|
543
406
|
|
|
544
407
|
const handleRowUpdate = (newRow: GridRowModel) => {
|
|
545
408
|
if (!newRow.id) throw new Error("Fila sin id");
|
|
546
|
-
|
|
409
|
+
|
|
410
|
+
const updated: any = { ...newRow, _edited: true };
|
|
411
|
+
|
|
547
412
|
setRows((prev) =>
|
|
548
413
|
prev.map((row) => (row.id === updated.id ? updated : row)),
|
|
549
414
|
);
|
|
415
|
+
|
|
550
416
|
return updated;
|
|
551
417
|
};
|
|
552
418
|
|
|
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]);
|
|
419
|
+
const resolvedButtons = modal ? { ...buttons, Modal: "" } : buttons;
|
|
562
420
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
() =>
|
|
566
|
-
searchable ? rows.filter((r) => rowMatchesSearch(r, searchQuery)) : rows,
|
|
567
|
-
[rows, searchQuery, searchable],
|
|
568
|
-
);
|
|
421
|
+
const columns = useMemo(() => {
|
|
422
|
+
if (!displayRows.length) return [];
|
|
569
423
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
424
|
+
const cols = Object.keys(displayRows[0])
|
|
425
|
+
.filter((key) => !key.startsWith("_") && !hideColumns.includes(key))
|
|
426
|
+
.map((key) => ({
|
|
427
|
+
field: key,
|
|
428
|
+
headerName: key,
|
|
429
|
+
flex: key == "id" ? false : !colSize?.[key],
|
|
430
|
+
width: key == "id" ? 80 : (colSize?.[key] ?? undefined),
|
|
431
|
+
editable: editableFields?.includes(key) ?? false,
|
|
432
|
+
type: typeof displayRows[0][key] === "number" ? "number" : "string",
|
|
433
|
+
|
|
434
|
+
valueFormatter: (value: any) => {
|
|
435
|
+
if (value == null || value === "") return "";
|
|
436
|
+
|
|
437
|
+
const isDate = /(\d{4}-\d{2}-\d{2})(T[\d:,.+-]*(Z)?)?/;
|
|
438
|
+
|
|
439
|
+
if (`${value}`.match(isDate)) {
|
|
440
|
+
return value
|
|
441
|
+
.toString()
|
|
442
|
+
.split("T")[0]
|
|
443
|
+
.split("-")
|
|
444
|
+
.reverse()
|
|
445
|
+
.join("/");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const splited = `${value}`.split(".");
|
|
449
|
+
|
|
450
|
+
const hasDecimals =
|
|
451
|
+
splited.length == 2 &&
|
|
452
|
+
splited.every((v: any) => `${v}`.match(regularExpresions.number));
|
|
453
|
+
|
|
454
|
+
if (hasDecimals) {
|
|
455
|
+
return [
|
|
456
|
+
currentCoin,
|
|
457
|
+
(+`${value}`).toLocaleString("en-US", {
|
|
458
|
+
minimumFractionDigits: 2,
|
|
459
|
+
maximumFractionDigits: 2,
|
|
460
|
+
}),
|
|
461
|
+
].join(" ");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return value;
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
renderCell: resolvedButtons?.[key]
|
|
468
|
+
? (params: any) => {
|
|
469
|
+
const children =
|
|
470
|
+
resolvedButtons?.[key]?.type == "input"
|
|
471
|
+
? null
|
|
472
|
+
: params?.row?.[key];
|
|
473
|
+
|
|
474
|
+
return React.cloneElement(resolvedButtons[key], {
|
|
475
|
+
className: `${params?.className ?? ""} m-auto text-xs`,
|
|
476
|
+
children,
|
|
477
|
+
row: params?.row,
|
|
478
|
+
|
|
479
|
+
onClick: async (e: TableButtonProps) => {
|
|
480
|
+
e.row = params?.row;
|
|
481
|
+
|
|
482
|
+
if (resolvedButtons[key]?.props?.onClick) {
|
|
483
|
+
const newVal = await resolvedButtons[key].props.onClick(e);
|
|
484
|
+
|
|
485
|
+
if (newVal) {
|
|
486
|
+
handleRowUpdate({ ...e.row, newVal });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
: null,
|
|
493
|
+
}));
|
|
494
|
+
|
|
495
|
+
if (modal) {
|
|
496
|
+
cols.unshift({
|
|
497
|
+
field: "Modal",
|
|
498
|
+
headerName: "Modal",
|
|
499
|
+
width: 100,
|
|
500
|
+
|
|
501
|
+
renderCell: (params: any) => (
|
|
502
|
+
<Button
|
|
503
|
+
className="text-xs"
|
|
504
|
+
onClick={() => handleModalOpen(params.row)}
|
|
505
|
+
icon={<EditIcon />}
|
|
506
|
+
>
|
|
507
|
+
Abrir
|
|
508
|
+
</Button>
|
|
509
|
+
),
|
|
510
|
+
} as any);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return cols;
|
|
514
|
+
}, [displayRows]);
|
|
585
515
|
|
|
586
516
|
if (!rows.length) return null;
|
|
587
517
|
|
|
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
518
|
const rowHeightProps = wrapText
|
|
593
519
|
? { getRowHeight: () => "auto" as const }
|
|
594
520
|
: rowHeight != null
|
|
595
521
|
? { rowHeight }
|
|
596
522
|
: {};
|
|
597
523
|
|
|
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
524
|
const containerHeight = autoHeight
|
|
604
|
-
? undefined
|
|
525
|
+
? undefined
|
|
605
526
|
: (HEIGHT_MAP[displayRows.length] ?? height);
|
|
606
527
|
|
|
607
|
-
// Whether to hide MUI's built-in pagination footer.
|
|
608
|
-
// With autoHeight all rows are visible, so pagination is never needed.
|
|
609
528
|
const hideFooter =
|
|
610
529
|
autoHeight || displayRows.length <= Object.keys(HEIGHT_MAP).length;
|
|
611
530
|
|
|
612
531
|
return (
|
|
613
532
|
<div className="m-2">
|
|
614
|
-
{header && <div className="font-bold
|
|
533
|
+
{header && <div className="font-bold p-2">{header}</div>}
|
|
534
|
+
|
|
615
535
|
<div className="flex justify-between">
|
|
616
536
|
<Toolbar
|
|
617
537
|
exportName={exportName}
|
|
@@ -620,6 +540,7 @@ function IHTable({
|
|
|
620
540
|
rows={rows}
|
|
621
541
|
filteredRows={filteredRows}
|
|
622
542
|
/>
|
|
543
|
+
|
|
623
544
|
{searchable && (
|
|
624
545
|
<SearchBar value={searchQuery} onChange={setSearchQuery} />
|
|
625
546
|
)}
|
|
@@ -629,10 +550,8 @@ function IHTable({
|
|
|
629
550
|
sx={{
|
|
630
551
|
display: "flex",
|
|
631
552
|
flexDirection: "column",
|
|
632
|
-
// undefined height lets the child DataGrid size itself freely
|
|
633
553
|
height: containerHeight,
|
|
634
554
|
width,
|
|
635
|
-
zIndex: 999999999,
|
|
636
555
|
}}
|
|
637
556
|
>
|
|
638
557
|
{modal && (
|
|
@@ -641,6 +560,10 @@ function IHTable({
|
|
|
641
560
|
onClose={handleClose}
|
|
642
561
|
modal={modal}
|
|
643
562
|
selectedRow={modalRow}
|
|
563
|
+
onPrev={handlePrevRow}
|
|
564
|
+
onNext={handleNextRow}
|
|
565
|
+
hasPrev={(modalIndex ?? 0) > 0}
|
|
566
|
+
hasNext={(modalIndex ?? -1) < displayRows.length - 1}
|
|
644
567
|
/>
|
|
645
568
|
)}
|
|
646
569
|
|
|
@@ -649,8 +572,6 @@ function IHTable({
|
|
|
649
572
|
rows={displayRows}
|
|
650
573
|
columns={columns as any}
|
|
651
574
|
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
575
|
autoHeight={autoHeight}
|
|
655
576
|
checkboxSelection={Boolean(onSelect)}
|
|
656
577
|
rowSelectionModel={selectedRows}
|
|
@@ -658,37 +579,19 @@ function IHTable({
|
|
|
658
579
|
{...rowHeightProps}
|
|
659
580
|
sx={{
|
|
660
581
|
fontSize,
|
|
582
|
+
|
|
661
583
|
"& .MuiDataGrid-cell": {
|
|
662
584
|
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
585
|
},
|
|
586
|
+
|
|
672
587
|
"& .MuiDataGrid-columnHeader": {
|
|
673
588
|
fontSize,
|
|
674
|
-
...(wrapText && {
|
|
675
|
-
whiteSpace: "normal",
|
|
676
|
-
"& .MuiDataGrid-columnHeaderTitle": {
|
|
677
|
-
whiteSpace: "normal",
|
|
678
|
-
lineHeight: 1.3,
|
|
679
|
-
wordBreak: "break-word",
|
|
680
|
-
},
|
|
681
|
-
}),
|
|
682
589
|
},
|
|
590
|
+
|
|
683
591
|
"& .MuiDataGrid-cell--editable": {
|
|
684
592
|
backgroundColor: "#c6d8f0",
|
|
685
593
|
fontWeight: 500,
|
|
686
594
|
},
|
|
687
|
-
...(displayRows.length <= Object.keys(HEIGHT_MAP).length && {
|
|
688
|
-
"& .MuiDataGrid-filler": {
|
|
689
|
-
display: "none",
|
|
690
|
-
},
|
|
691
|
-
}),
|
|
692
595
|
}}
|
|
693
596
|
editMode="row"
|
|
694
597
|
processRowUpdate={handleRowUpdate}
|
|
@@ -696,16 +599,18 @@ function IHTable({
|
|
|
696
599
|
hideFooter={hideFooter}
|
|
697
600
|
/>
|
|
698
601
|
</Box>
|
|
602
|
+
|
|
699
603
|
<CustomFooter footer={footer} rows={displayRows} />
|
|
700
604
|
</div>
|
|
701
605
|
);
|
|
702
606
|
}
|
|
703
607
|
|
|
704
|
-
// ───
|
|
608
|
+
// ─── Export ───────────────────────────────────────────────────────────────────
|
|
705
609
|
|
|
706
610
|
export default function Table(props: TableProps) {
|
|
707
611
|
if (Array.isArray(props.data)) {
|
|
708
612
|
return <IHTable {...props} />;
|
|
709
613
|
}
|
|
614
|
+
|
|
710
615
|
return <KeyValueTable data={props.data} />;
|
|
711
616
|
}
|