next-recomponents 2.0.41 → 2.0.42

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.
@@ -0,0 +1,579 @@
1
+ import React, {
2
+ ReactNode,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import Pre from "../pre";
10
+ import { ContextType } from "./context";
11
+ import { Header } from "./header";
12
+ import { ExcelIcon, SearchIcon } from "./icons";
13
+ import Searchable from "./searchable";
14
+ import Modal from "../modal";
15
+ import { EditIcon } from "./icons";
16
+ import regularExpresions, { TableProps } from "./types";
17
+ type Widths = Record<string, number>;
18
+ export default function HTable({
19
+ context,
20
+ }: {
21
+ context: ContextType & Omit<TableProps, "data">;
22
+ }) {
23
+ const [isLoading, setIsloading] = useState(false);
24
+ const { cols, headers } = context;
25
+ const [currentIndex, setCurrentIndex] = useState(-1);
26
+ const tableRef = useRef<HTMLDivElement>(null);
27
+ const modalRef = useRef<HTMLButtonElement>(null);
28
+ const catDensities: any = { confortable: 80, compact: 30, standard: 53 };
29
+ const padding =
30
+ context?.rowHeight || context?.density
31
+ ? catDensities[context?.density]
32
+ : 53;
33
+
34
+ function useResizableColumns(
35
+ headers: string[],
36
+ defaultWidth = 160,
37
+ minWidth = 60,
38
+ ) {
39
+ function getWidth(h: string) {
40
+ return [
41
+ "__select__",
42
+ "__modal__",
43
+ ...(context?.hideColumns || []),
44
+ ].includes(h)
45
+ ? minWidth
46
+ : defaultWidth;
47
+ }
48
+
49
+ function resize(headers: string[]) {
50
+ return Object.fromEntries(headers.map((h) => [h, getWidth(h)]));
51
+ }
52
+ const [widths, setWidths] = useState<Widths>({});
53
+
54
+ // si headers cambia dinámicamente, hay que rellenar los que falten
55
+ // useEffect(() => {
56
+ // setWidths((prev) => {
57
+ // const missing = headers.filter((h) => !(h in prev));
58
+ // if (missing.length === 0) return prev;
59
+ // return {
60
+ // ...prev,
61
+ // ...Object.fromEntries(missing.map((h) => [h, getWidth(h)])),
62
+ // };
63
+ // });
64
+ // }, [headers, defaultWidth]);
65
+
66
+ const startDrag = useCallback(
67
+ (e: React.MouseEvent, headerKey: string) => {
68
+ e.preventDefault();
69
+ const startX = e.clientX;
70
+ const startWidth = widths[headerKey] ?? defaultWidth;
71
+
72
+ const onMove = (ev: MouseEvent) => {
73
+ const newWidth = Math.max(
74
+ minWidth,
75
+ startWidth + (ev.clientX - startX),
76
+ );
77
+ setWidths((prev) => ({ ...prev, [headerKey]: newWidth }));
78
+ };
79
+ const onUp = () => {
80
+ window.removeEventListener("mousemove", onMove);
81
+ window.removeEventListener("mouseup", onUp);
82
+ };
83
+ window.addEventListener("mousemove", onMove);
84
+ window.addEventListener("mouseup", onUp);
85
+ },
86
+ [widths, defaultWidth, minWidth],
87
+ );
88
+
89
+ const gridTemplateColumns = headers
90
+ .filter((h) => !context?.hideColumns?.includes?.(h))
91
+ .map((h) => `${widths[h] ?? defaultWidth}px`)
92
+ .join(" ");
93
+
94
+ return { widths, gridTemplateColumns, startDrag, setWidths };
95
+ }
96
+ const { gridTemplateColumns, startDrag, widths, setWidths } =
97
+ useResizableColumns(headers);
98
+
99
+ useEffect(() => {
100
+ if (!tableRef.current) return;
101
+ const observer = new ResizeObserver(([entry]) => {
102
+ const sizeados = headers.filter((h) =>
103
+ [
104
+ "__select__",
105
+ "__modal__",
106
+ // ...(context?.hideColumns || []),
107
+ // ...(context?.colSize ? Object.keys(context.colSize) : []),
108
+ ].includes(h),
109
+ );
110
+
111
+ const personalizados = context?.colSize
112
+ ? (Object.values(context.colSize) as number[])
113
+ : [];
114
+
115
+ const personalizadosSum =
116
+ personalizados.length > 0
117
+ ? personalizados.reduce((acc, i) => acc + i, 0)
118
+ : 0;
119
+ const ancho = +entry.contentRect.width;
120
+ const sizeadosLength = sizeados.length;
121
+ const sizeadosWidth = sizeadosLength * 60;
122
+
123
+ const w =
124
+ (ancho - sizeadosWidth - personalizadosSum) /
125
+ (headers.length - sizeadosLength - personalizados.length);
126
+ console.log({
127
+ w,
128
+ ancho,
129
+ sizeadosWidth,
130
+ sizeadosLength,
131
+ personalizadosSum,
132
+ headers: headers.length,
133
+ personalizados: personalizados.length,
134
+ });
135
+ setWidths(
136
+ Object.fromEntries(
137
+ headers.map((h) => [
138
+ h,
139
+ context?.colSize?.[h]
140
+ ? context?.colSize?.[h]
141
+ : ["__select__", "__modal__"].includes(h)
142
+ ? 60
143
+ : w,
144
+ ]),
145
+ ),
146
+ );
147
+ });
148
+ observer.observe(tableRef.current);
149
+ return () => observer.disconnect();
150
+ }, [headers.length]);
151
+ const searchedData = context.filteredData.filter((row) => {
152
+ if (context.searchBy === "") return true;
153
+
154
+ return Object.values(row).some((value) =>
155
+ String(value).toLowerCase().includes(context.searchBy.toLowerCase()),
156
+ );
157
+ });
158
+ return (
159
+ <div
160
+ className={[context.className, "bg-white relative"].join(" ")}
161
+ ref={tableRef}
162
+ >
163
+ <div className="bg-white px-1 font-bold">{context.header}</div>
164
+ <div className="bg-white flex gap-2 items-center">
165
+ {context.searchable && <Searchable context={context} />}
166
+ {context.exportName && (
167
+ <button
168
+ onClick={(e) => {
169
+ context.excel.export(
170
+ context.data.map((d) => {
171
+ const { __select__, __modal__, ...datums } = d;
172
+ return datums;
173
+ }),
174
+ );
175
+ }}
176
+ className={
177
+ "flex gap-1 items-center border shadow rounded p-1 text-white bg-green-800 px-2"
178
+ }
179
+ >
180
+ <ExcelIcon />
181
+ Exportar
182
+ </button>
183
+ )}
184
+ {context.onSelect && (
185
+ <button
186
+ className={
187
+ "border shadow rounded p-1 text-white px-2 " +
188
+ (context.selected.length > 0 ? "bg-blue-500" : "bg-gray-500")
189
+ }
190
+ disabled={context.selected.length == 0}
191
+ onClick={async (e) => {
192
+ setIsloading(true);
193
+
194
+ const ex = context.filteredData
195
+ .filter((d) => context.selected.includes(d.id))
196
+ .map((d) => {
197
+ const { __select__, __modal__, ...datums } = d;
198
+ return datums;
199
+ });
200
+ await context.onSelect(ex);
201
+ setIsloading(false);
202
+ }}
203
+ >
204
+ {context?.onSelectLabel || "Guardar selección"}
205
+ </button>
206
+ )}
207
+ {context.onSave && (
208
+ <button
209
+ className={
210
+ "border shadow rounded p-1 text-white px-2 " +
211
+ (context.editions.length > 0 ? "bg-blue-500" : "bg-gray-500")
212
+ }
213
+ disabled={context.editions.length == 0}
214
+ onClick={async (e) => {
215
+ setIsloading(true);
216
+ const ex = context.filteredData.map((d) => {
217
+ const { __select__, __modal__, ...datums } = d;
218
+ return datums;
219
+ });
220
+ await context.onSave(ex);
221
+ setIsloading(false);
222
+ }}
223
+ >
224
+ {context.onSaveLabel || "Guardar cambios"}
225
+ </button>
226
+ )}
227
+ </div>
228
+ <div className="grid " style={{ gridTemplateColumns }}>
229
+ {headers.map((header, i) => {
230
+ if (context?.hideColumns?.includes?.(header)) return null;
231
+ return (
232
+ <div
233
+ key={header}
234
+ className="bg-white"
235
+ style={{ position: "relative" }}
236
+ >
237
+ <Header
238
+ padding={padding}
239
+ header={header}
240
+ context={context}
241
+ drag={{ gridTemplateColumns, startDrag, widths }}
242
+ />
243
+ {i < headers.length - 1 && (
244
+ <div
245
+ onMouseDown={(e) => startDrag(e, header)}
246
+ style={{
247
+ position: "absolute",
248
+ right: -3,
249
+ top: 0,
250
+ width: 6,
251
+ height: "100%",
252
+ cursor: "col-resize",
253
+ background: "transparent",
254
+ }}
255
+ onMouseEnter={(e) =>
256
+ (e.currentTarget.style.background =
257
+ "var(--border-strong, #ccc)")
258
+ }
259
+ onMouseLeave={(e) =>
260
+ (e.currentTarget.style.background = "transparent")
261
+ }
262
+ />
263
+ )}
264
+ </div>
265
+ );
266
+ })}
267
+ {searchedData.map((row, rowIndex) => {
268
+ const items: any[] = Object.entries(row);
269
+
270
+ return items.map(([key, item], i) => {
271
+ if (context?.hideColumns?.includes?.(key)) return null;
272
+
273
+ if (key == "__modal__") {
274
+ return (
275
+ <div
276
+ onMouseEnter={(e) => setCurrentIndex(rowIndex)}
277
+ key={row.id + i}
278
+ style={{
279
+ height: `${padding}px`,
280
+ }}
281
+ className={
282
+ (rowIndex == currentIndex
283
+ ? " bg-blue-100 "
284
+ : " bg-white ") +
285
+ " border-b p-2 flex items-center justify-center " +
286
+ (context.searchBy &&
287
+ String(item)
288
+ .toLowerCase()
289
+ .includes(context.searchBy.toLowerCase())
290
+ ? "bg-yellow-100"
291
+ : "")
292
+ }
293
+ >
294
+ {context?.modalButton || (
295
+ <button
296
+ className="border shadow rounded bg-blue-500 text-white p-2 "
297
+ onClick={(e) => {
298
+ setCurrentIndex(rowIndex);
299
+ modalRef.current?.click();
300
+ }}
301
+ >
302
+ <EditIcon />
303
+ </button>
304
+ )}
305
+ </div>
306
+ );
307
+ }
308
+
309
+ if (key == "__select__") {
310
+ return (
311
+ <div
312
+ key={row.id + i}
313
+ onMouseEnter={(e) => setCurrentIndex(rowIndex)}
314
+ style={{
315
+ height: `${padding}px`,
316
+ }}
317
+ className={
318
+ (rowIndex == currentIndex
319
+ ? " bg-blue-100 "
320
+ : " bg-white ") +
321
+ " border-b p-2 flex items-center justify-center " +
322
+ (context.searchBy &&
323
+ String(item)
324
+ .toLowerCase()
325
+ .includes(context.searchBy.toLowerCase())
326
+ ? "bg-yellow-100"
327
+ : "")
328
+ }
329
+ >
330
+ <input
331
+ type="checkbox"
332
+ className="w-5 h-5 accent-blue-600 transition-all duration-300 checked:scale-110 "
333
+ checked={Boolean(
334
+ context.selected.find((s) => s == row?.id),
335
+ )}
336
+ onChange={(e) => {
337
+ const newSelected = [...context.selected];
338
+ const index = newSelected.findIndex((s) => s == row?.id);
339
+ if (index >= 0) {
340
+ newSelected.splice(index, 1);
341
+ } else {
342
+ newSelected.push(row?.id);
343
+ }
344
+ context.setSelected(newSelected);
345
+ }}
346
+ />
347
+ </div>
348
+ );
349
+ }
350
+
351
+ return (
352
+ <div
353
+ onMouseEnter={(e) => setCurrentIndex(rowIndex)}
354
+ key={row.id + i}
355
+ style={{
356
+ fontSize: context.fontSize,
357
+ ...(context?.wrapText
358
+ ? { minHeight: `${padding}px` }
359
+ : { height: `${padding}px` }),
360
+ }}
361
+ className={
362
+ " flex justify-center items-center " +
363
+ (rowIndex == currentIndex ? " bg-blue-100 " : " bg-white ") +
364
+ (context.wrapText ? " text-wrap truncate " : " truncate ") +
365
+ " border-b " +
366
+ (context.searchBy &&
367
+ String(item)
368
+ .toLowerCase()
369
+ .includes(context.searchBy.toLowerCase())
370
+ ? rowIndex == currentIndex
371
+ ? " bg-yellow-200 "
372
+ : " bg-yellow-100 "
373
+ : "")
374
+ }
375
+ >
376
+ {context?.editableFields &&
377
+ context.editableFields.includes(key) ? (
378
+ <input
379
+ defaultValue={item}
380
+ className={
381
+ "w-full " +
382
+ (context.editions.includes(row?.id)
383
+ ? "bg-blue-400 text-white"
384
+ : "bg-blue-100")
385
+ }
386
+ onBlur={(e) => {
387
+ const id = row?.id;
388
+ const newData = [...context.data];
389
+ const index = newData.findIndex((d) => +d.id == +id);
390
+ newData[index][key] = e.target.value;
391
+ context.setData(newData);
392
+
393
+ const newEditions = [
394
+ ...new Set([...context.editions, id]),
395
+ ];
396
+ context.setEditions(newEditions);
397
+ }}
398
+ />
399
+ ) : context.buttons?.[key] ? (
400
+ React.cloneElement(context.buttons[key], {
401
+ // children: context.buttons[key]?.props?.children
402
+ // ? item
403
+ // : undefined,
404
+ value: item,
405
+ children: context.buttons[key]?.props?.children
406
+ ? item
407
+ : null,
408
+ onClick: async (e: any) => {
409
+ const ret =
410
+ (await context.buttons[key].props?.onClick?.(e)) || {};
411
+ const newData = [...context.data];
412
+ const index = newData.findIndex((f) => f.id == row?.id);
413
+ newData[index] = { ...newData[index], ...ret };
414
+ context.setData(newData);
415
+ },
416
+ onChange: async (e: { target: { value: string } }) => {
417
+ const value = e.target.value;
418
+ const ret =
419
+ (await context.buttons[key].props?.onChange?.(e)) || {};
420
+
421
+ const newData = [...context.data];
422
+ const index = newData.findIndex((f) => f.id == row?.id);
423
+
424
+ if (index >= 0) {
425
+ newData[index][key] = value;
426
+ }
427
+
428
+ for (let i in ret) {
429
+ if (newData[index]?.[i]) {
430
+ const v = ret[i];
431
+ newData[index][i] = v;
432
+ }
433
+ }
434
+
435
+ context.setData(newData);
436
+ },
437
+ className: `${context.buttons[key]?.props?.className} text-xs w-full`,
438
+ })
439
+ ) : ["number", "string"].includes(typeof item) ? (
440
+ valueFormatter({
441
+ value: item,
442
+ currentCoin: context.currentCoin,
443
+ })
444
+ ) : (
445
+ JSON.stringify(item)
446
+ )}
447
+ </div>
448
+ );
449
+ });
450
+ })}
451
+ {Object.keys(context?.footer || {}).length > 0 &&
452
+ headers.map((header) => {
453
+ return (
454
+ <div
455
+ key={header}
456
+ style={{
457
+ height: `${padding}px`,
458
+ maxHeight: "100px",
459
+ }}
460
+ className={
461
+ "border-b font-bold " +
462
+ " bg-white flex items-center justify-center"
463
+ }
464
+ >
465
+ {valueFormatter({
466
+ currentCoin: context.currentCoin,
467
+ value: context.footer?.[header]
468
+ ? context.footer[header] == "sum"
469
+ ? context.filteredData
470
+ .map((d) => +d[header])
471
+ .reduce((acc, h) => h + acc, 0)
472
+ : context.footer[header] == "count"
473
+ ? context.filteredData
474
+ .map((d) => (d[header] != "" ? 1 : 0))
475
+ .reduce((acc: number, h) => acc + h, 0)
476
+ : context.footer[header] == "avg"
477
+ ? context.filteredData
478
+ .map((d) => +d[header])
479
+ .reduce((acc, h) => h + acc, 0) /
480
+ context.filteredData
481
+ .map((d) => (d[header] != "" ? 1 : 0))
482
+ .reduce((acc: number, h) => acc + h, 0)
483
+ : ""
484
+ : "",
485
+ })}
486
+ </div>
487
+ );
488
+ })}
489
+ </div>
490
+
491
+ {context.modal && (
492
+ <Modal
493
+ button={<button ref={modalRef}></button>}
494
+ color={"white"}
495
+ onClose={async () => {
496
+ const c = await context?.onCloseModal?.(searchedData[currentIndex]);
497
+
498
+ return c;
499
+ }}
500
+ >
501
+ <div className="relative">
502
+ <div className="fixed top-0 left-0 p-10 ">
503
+ <button
504
+ className={
505
+ "p-2 border shadow rounded " +
506
+ (currentIndex > 0 ? "bg-white " : "bg-gray-100")
507
+ }
508
+ onClick={(e) => {
509
+ currentIndex > 0 && setCurrentIndex(currentIndex - 1);
510
+ }}
511
+ >
512
+ Anterior
513
+ </button>
514
+ <button
515
+ className={
516
+ "p-2 border shadow rounded " +
517
+ (currentIndex < searchedData.length - 1
518
+ ? "bg-white "
519
+ : "bg-gray-100")
520
+ }
521
+ onClick={(e) => {
522
+ currentIndex < searchedData.length - 1 &&
523
+ setCurrentIndex(currentIndex + 1);
524
+ }}
525
+ >
526
+ Siguiente
527
+ </button>
528
+ </div>
529
+ {React.cloneElement(context.modal, {
530
+ row: searchedData[currentIndex],
531
+ })}
532
+ </div>
533
+ </Modal>
534
+ )}
535
+ {isLoading && (
536
+ <div
537
+ className="bg-white/40 absolute inset-0 text-black flex items-center justify-center"
538
+ style={{ zIndex: 9999999 }}
539
+ >
540
+ Loading...
541
+ </div>
542
+ )}
543
+ </div>
544
+ );
545
+ }
546
+
547
+ function valueFormatter({
548
+ value,
549
+ currentCoin = "",
550
+ }: {
551
+ value: any;
552
+ currentCoin?: ReactNode;
553
+ }) {
554
+ if (value == null || value === "") return "";
555
+
556
+ const isDate = /(\d{4}-\d{2}-\d{2})(T[\d:,.+-]*(Z)?)?/;
557
+
558
+ if (`${value}`.match(isDate)) {
559
+ return value.toString().split("T")[0].split("-").reverse().join("/");
560
+ }
561
+
562
+ const splited = `${value}`.split(".");
563
+
564
+ const hasDecimals =
565
+ splited.length == 2 &&
566
+ splited.every((v: any) => `${v}`.match(regularExpresions.number));
567
+
568
+ if (hasDecimals) {
569
+ return [
570
+ currentCoin,
571
+ (+`${value}`).toLocaleString("en-US", {
572
+ minimumFractionDigits: 2,
573
+ maximumFractionDigits: 2,
574
+ }),
575
+ ].join(" ");
576
+ }
577
+
578
+ return value;
579
+ }