gbs-add-block 0.0.1

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.
Files changed (37) hide show
  1. package/index.js +108 -0
  2. package/package.json +28 -0
  3. package/source/components/button/index.tsx +25 -0
  4. package/source/components/checkbox/index.tsx +9 -0
  5. package/source/components/darkmode/index.tsx +48 -0
  6. package/source/components/datepicker/DatePickerHelper.ts +42 -0
  7. package/source/components/datepicker/index.tsx +258 -0
  8. package/source/components/datepicker/types.ts +9 -0
  9. package/source/components/dialog/index.tsx +44 -0
  10. package/source/components/dialog/types.ts +12 -0
  11. package/source/components/grid/FilterPopup.tsx +85 -0
  12. package/source/components/grid/Grid.tsx +615 -0
  13. package/source/components/grid/GridHelperFunctions.ts +218 -0
  14. package/source/components/grid/index.tsx +26 -0
  15. package/source/components/grid/type.ts +28 -0
  16. package/source/components/input/index.tsx +105 -0
  17. package/source/components/input/types.ts +9 -0
  18. package/source/components/modal/index.tsx +58 -0
  19. package/source/components/modal/types.ts +11 -0
  20. package/source/components/multiselect/index.tsx +213 -0
  21. package/source/components/multiselect/types.ts +14 -0
  22. package/source/components/select/index.tsx +220 -0
  23. package/source/components/select/types.ts +13 -0
  24. package/source/components/spinner/Stroke.tsx +89 -0
  25. package/source/components/spinner/index.tsx +54 -0
  26. package/source/components/spinner/types.ts +9 -0
  27. package/source/components/toast/Toast.tsx +61 -0
  28. package/source/components/toast/index.tsx +36 -0
  29. package/source/components/toast/toastAtom.ts +23 -0
  30. package/source/components/toast/types.ts +24 -0
  31. package/source/components/toast/useToast.ts +24 -0
  32. package/source/fallback/index.tsx +11 -0
  33. package/source/icon/Icon.tsx +55 -0
  34. package/source/icon/iconPaths.ts +92 -0
  35. package/source/index.ts +15 -0
  36. package/source/types.ts +108 -0
  37. package/source/utils.ts +10 -0
@@ -0,0 +1,615 @@
1
+ /**
2
+ * Copyright (c) Grampro Business Services and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * Extended Documentation for Grid can be found at
7
+ * https://psychedelic-step-e70.notion.site/Data-GRID-by-GBS-R-D-20ff97c899d24bc590215a6196435fa3
8
+ */
9
+
10
+ import React, {
11
+ forwardRef,
12
+ useCallback,
13
+ useEffect,
14
+ useImperativeHandle,
15
+ useState,
16
+ } from "react";
17
+ import FilterPopup from "./FilterPopup";
18
+ import {
19
+ clearFilterHelper,
20
+ exportToExcelHelper,
21
+ exportToPDFHelper,
22
+ } from "./GridHelperFunctions";
23
+ import { handleApplyFilterHelper } from "./GridHelperFunctions";
24
+ import Icon from "../icon/Icon";
25
+ import {
26
+ leftArrows,
27
+ leftArrow,
28
+ rightArrow,
29
+ rightArrows,
30
+ search,
31
+ listFilter,
32
+ } from "../icon/iconPaths";
33
+ import type { GridProps } from "./type";
34
+ import { getSourceData } from "../utils";
35
+
36
+ export const Grid = forwardRef((props: GridProps, ref) => {
37
+ const {
38
+ dataSource,
39
+ columns = [],
40
+ pageSettings,
41
+ enableSearch = false,
42
+ lazy = false,
43
+ enableExcelExport = false,
44
+ excelName = "data",
45
+ enablePdfExport = false,
46
+ pdfName = "data",
47
+ pdfOptions = {},
48
+ gridButtonClass = "px-1 py-2 bg-white border rounded-lg text-xs text-black dark:bg-black dark:text-white",
49
+ selectAll = false,
50
+ onSelectRow,
51
+ isFetching,
52
+ tableHeaderStyle = "text-left border-b border-t bg-gray-50 px-2 py-4 dark:bg-gray-700 dark:text-white",
53
+ gridContainerClass = "flex flex-col min-w-screen border rounded-md overflow-hidden dark:bg-black",
54
+ gridColumnStyleSelectAll = "border-b px-4 text-sm dark:text-white",
55
+ gridColumnStyle = "border-b p-2 text-sm dark:text-white",
56
+ } = props;
57
+ const [workingDataSource, setWorkingDataSource] = useState<any>([]);
58
+ const [fallbackSourceData, setFallbackSourceData] = useState([]);
59
+ const [workingColumns, setWorkingColumns] = useState<any>([]);
60
+ const [currentPage, setCurrentPage] = useState(0);
61
+ const [pageStart, setPageStart] = useState(0);
62
+ const [pageEnd, setPageEnd] = useState(10);
63
+ const [totalPages, setTotalPages] = useState(0);
64
+ const [searchParam, setSearchParam] = useState("");
65
+ const [activeFilterArray, setActiveFilterArray] = useState<any>([]);
66
+ const [selectedRows, setSelectedRows] = useState<any[]>([]);
67
+
68
+ // Function to handle API datasource
69
+ const getGridData = useCallback(async () => {
70
+ try {
71
+ const sourceData = await getSourceData(dataSource);
72
+ const gridData = sourceData.sourcedata;
73
+ setFallbackSourceData(gridData);
74
+ setWorkingDataSource(gridData);
75
+ const totalPages = Math.ceil(gridData.length / pageSettings.pageNumber);
76
+ setTotalPages(totalPages);
77
+ setPageEnd(Math.min(10, totalPages));
78
+ } catch (error) {
79
+ console.error("Error fetching grid source data:", error);
80
+ }
81
+ }, [dataSource, pageSettings.pageNumber]);
82
+
83
+ // Calculates total pages and determine dataSource type
84
+ useEffect(() => {
85
+ const handleDataSource = async () => {
86
+ if (Array.isArray(dataSource) && dataSource.length > 0) {
87
+ setWorkingDataSource(dataSource);
88
+ const totalPages = Math.ceil(
89
+ dataSource.length / pageSettings.pageNumber
90
+ );
91
+ setTotalPages(totalPages);
92
+ setPageEnd(Math.min(10, totalPages));
93
+ } else if (typeof dataSource === "string") {
94
+ await getGridData();
95
+ }
96
+ };
97
+
98
+ handleDataSource();
99
+ }, [dataSource, pageSettings]);
100
+
101
+ // Adds Filter Column
102
+ useEffect(() => {
103
+ if (columns && columns.length > 0) {
104
+ const filteredColumns = columns.map((column) => ({
105
+ ...column,
106
+ showFilterPopup: false,
107
+ isFilterActive: false,
108
+ }));
109
+ setWorkingColumns(filteredColumns);
110
+ }
111
+ }, [columns]);
112
+
113
+ useEffect(() => {
114
+ if (!columns || columns.length === 0) {
115
+ if (
116
+ workingDataSource.length > 0 &&
117
+ Object.keys(workingDataSource[0]).length > 0
118
+ ) {
119
+ const inferredColumns = Object.keys(workingDataSource[0]).map(
120
+ (key) => ({
121
+ field: key,
122
+ headerText: key.charAt(0).toUpperCase() + key.slice(1),
123
+ width: 150,
124
+ })
125
+ );
126
+ setWorkingColumns((prevColumns: any) => {
127
+ if (JSON.stringify(prevColumns) !== JSON.stringify(inferredColumns)) {
128
+ return inferredColumns;
129
+ }
130
+ return prevColumns;
131
+ });
132
+ }
133
+ }
134
+ }, [columns, workingDataSource]);
135
+
136
+ // *** Page Navigation Helper Methods Starts Here
137
+ const nextPage = () => {
138
+ setCurrentPage((prevPage) => {
139
+ if (prevPage < totalPages - 1) {
140
+ const newPage = prevPage + 1;
141
+ updatePageRange(newPage);
142
+ return newPage;
143
+ }
144
+ return prevPage;
145
+ });
146
+ };
147
+
148
+ const prevPage = () => {
149
+ setCurrentPage((prevPage) => {
150
+ if (prevPage > 0) {
151
+ const newPage = prevPage - 1;
152
+ updatePageRange(newPage);
153
+ return newPage;
154
+ }
155
+ return prevPage;
156
+ });
157
+ };
158
+
159
+ const goToEndPage = () => {
160
+ const lastPage = totalPages - 1;
161
+ setCurrentPage(lastPage);
162
+ updatePageRange(lastPage);
163
+ };
164
+
165
+ const goToFirstPage = () => {
166
+ setCurrentPage(0);
167
+ updatePageRange(0);
168
+ };
169
+
170
+ const goToPage = (page: number) => {
171
+ setCurrentPage(page);
172
+ updatePageRange(page);
173
+ };
174
+
175
+ const updatePageRange = (page: number) => {
176
+ const start = Math.floor(page / 10) * 10;
177
+ setPageStart(start);
178
+ setPageEnd(Math.min(start + 10, totalPages));
179
+ };
180
+ // Page Navigation Helper Methods Ends Here ***
181
+
182
+ // *** Functions handling global search starts here
183
+ const handleSearchInput = (e: any) => {
184
+ if (!lazy) {
185
+ const searchValue = e.target.value;
186
+ setSearchParam(searchValue);
187
+ if (searchValue === "") {
188
+ setWorkingDataSource(
189
+ fallbackSourceData.length > 0 ? fallbackSourceData : dataSource
190
+ );
191
+ const fallback =
192
+ fallbackSourceData.length > 0 ? fallbackSourceData : dataSource;
193
+ const totalPages = Math.ceil(fallback.length / pageSettings.pageNumber);
194
+ setTotalPages(totalPages);
195
+ setPageEnd(Math.min(pageStart + 10, totalPages));
196
+ }
197
+ }
198
+ };
199
+
200
+ const handleSearch = (searchParam: string) => {
201
+ const filteredData = workingDataSource.filter((item: any) =>
202
+ Object.values(item).some((val: any) => {
203
+ const trimmedVal = val.toString().toLowerCase().trim();
204
+ const trimmedSearchParam = searchParam.toLowerCase().trim();
205
+ return trimmedVal.includes(trimmedSearchParam);
206
+ })
207
+ );
208
+ setWorkingDataSource(filteredData);
209
+ const newTotalPages = Math.ceil(
210
+ filteredData.length / pageSettings.pageNumber
211
+ );
212
+ setTotalPages(newTotalPages);
213
+ const newCurrentPage = 0;
214
+ setCurrentPage(newCurrentPage);
215
+ const newPageStart = Math.floor(newCurrentPage / 10) * 10;
216
+ setPageStart(newPageStart);
217
+ setPageEnd(Math.min(newPageStart + 10, newTotalPages));
218
+ };
219
+ // Functions handling global search ends here ***
220
+
221
+ // This will render template passed to Grid
222
+ const renderCell = (rowData: any, column: any, rowIndex: number) => {
223
+ if (column.template) {
224
+ return (
225
+ <column.template
226
+ rowData={rowData}
227
+ rowIndex={rowIndex + currentPage * pageSettings.pageNumber}
228
+ />
229
+ );
230
+ }
231
+ return rowData[column.field];
232
+ };
233
+
234
+ // *** Filter Handling Functions Starts Here
235
+ const toggleFilterPopup = (index: number) => {
236
+ setWorkingColumns((prevColumns: any) =>
237
+ prevColumns.map((column: any, i: any) =>
238
+ i === index
239
+ ? { ...column, showFilterPopup: !column.showFilterPopup }
240
+ : column
241
+ )
242
+ );
243
+ };
244
+
245
+ // Resetting pagination params for updating pagination
246
+ function resetPage(dataSource: any) {
247
+ const newTotalPages = Math.ceil(
248
+ dataSource.length / pageSettings.pageNumber
249
+ );
250
+
251
+ setTotalPages(newTotalPages);
252
+ setCurrentPage(0);
253
+ setPageStart(0);
254
+ setPageEnd(Math.min(10, newTotalPages));
255
+ }
256
+
257
+ // Applying Filter
258
+ function handleApplyFilter(event: any) {
259
+ const {
260
+ columns: updatedColumns,
261
+ workingDataSource: updatedFullDataSource,
262
+ activeFilterArray: updatedActiveFilterArray,
263
+ } = handleApplyFilterHelper(event, columns, workingDataSource);
264
+
265
+ setWorkingColumns(updatedColumns);
266
+ setWorkingDataSource(updatedFullDataSource);
267
+ setActiveFilterArray(updatedActiveFilterArray);
268
+
269
+ setActiveFilterArray(updatedActiveFilterArray);
270
+ resetPage(updatedFullDataSource);
271
+ }
272
+
273
+ // Clearing Filter
274
+ function clearFilter(event: any) {
275
+ const clearDataSource = Array.isArray(dataSource)
276
+ ? dataSource
277
+ : fallbackSourceData;
278
+ const {
279
+ columns: updatedColumns,
280
+ workingDataSource: updatedDataSource,
281
+ activeFilterArray: updatedActiveFilterArray,
282
+ } = clearFilterHelper(event, workingColumns, clearDataSource);
283
+
284
+ setWorkingColumns(updatedColumns);
285
+ setWorkingDataSource(updatedDataSource);
286
+ setActiveFilterArray(updatedActiveFilterArray);
287
+ resetPage(updatedDataSource);
288
+ }
289
+
290
+ const handleFilterAction = (action: any, colIndex: number) => {
291
+ switch (action.type) {
292
+ case "cancel":
293
+ toggleFilterPopup(colIndex);
294
+ break;
295
+ case "applyFilter":
296
+ handleApplyFilter(action);
297
+ break;
298
+ case "clearFilter":
299
+ clearFilter(action);
300
+ break;
301
+ default:
302
+ break;
303
+ }
304
+ };
305
+ // *** Filter Handling Functions Ends Here
306
+
307
+ // *** RowSelection Handling Logic Starts Here
308
+ const handleSelectAll = () => {
309
+ setSelectedRows(workingDataSource);
310
+ if (onSelectRow) onSelectRow(workingDataSource);
311
+ };
312
+
313
+ const handleSelect = (rowData: any) => {
314
+ setSelectedRows((prevData) => {
315
+ const isSelected = prevData.includes(rowData);
316
+ let updatedData;
317
+ if (isSelected) {
318
+ updatedData = prevData.filter((item) => item !== rowData);
319
+ } else {
320
+ updatedData = [...prevData, rowData];
321
+ }
322
+ if (onSelectRow) onSelectRow(updatedData);
323
+ return updatedData;
324
+ });
325
+ };
326
+
327
+ const isRowSelected = (rowData: any) => {
328
+ return selectedRows.includes(rowData);
329
+ };
330
+ // RowSelection Handling Logic Ends Here ***
331
+
332
+ // Making Grid Functions Accessible in Parent
333
+ useImperativeHandle(ref, () => ({
334
+ goToPage,
335
+ nextPage,
336
+ prevPage,
337
+ goToFirstPage,
338
+ goToEndPage,
339
+ handleSearch,
340
+ handleApplyFilter,
341
+ clearFilter,
342
+ workingDataSource,
343
+ dataSource,
344
+ }));
345
+
346
+ return (
347
+ <div className={gridContainerClass}>
348
+ <React.Fragment>
349
+ {/* Global utility logic */}
350
+ <div
351
+ className={`min-w-full flex justify-between items-center ${
352
+ enableExcelExport || enableSearch || enablePdfExport
353
+ ? "block"
354
+ : "hidden"
355
+ }`}
356
+ >
357
+ <div className="flex justify-end gap-2 px-1 py-3 flex-grow">
358
+ {enableExcelExport && (
359
+ <button
360
+ className={gridButtonClass}
361
+ onClick={() => {
362
+ exportToExcelHelper(workingDataSource, columns, excelName);
363
+ }}
364
+ >
365
+ Export as Excel
366
+ </button>
367
+ )}
368
+ {enablePdfExport && (
369
+ <button
370
+ className={gridButtonClass}
371
+ onClick={() => {
372
+ exportToPDFHelper(
373
+ workingDataSource,
374
+ columns,
375
+ pdfName,
376
+ pdfOptions
377
+ );
378
+ }}
379
+ >
380
+ Export as PDF
381
+ </button>
382
+ )}
383
+ {enableSearch && (
384
+ <div className="flex gap-1">
385
+ <input
386
+ type="search"
387
+ value={searchParam}
388
+ onChange={handleSearchInput}
389
+ placeholder="search"
390
+ className="outline-none p-2 text-sm font-normal bg-gray-50 rounded-lg max-sm:hidden dark:bg-black dark:border dark:text-white"
391
+ />
392
+ <button
393
+ className="bg-white border rounded-lg text-black w-10 flex items-center justify-center dark:bg-black dark:text-white"
394
+ onClick={() => handleSearch(searchParam)}
395
+ >
396
+ <Icon
397
+ elements={search}
398
+ svgClass={"stroke-gray-500 fill-none dark:stroke-white"}
399
+ />
400
+ </button>
401
+ </div>
402
+ )}
403
+ </div>
404
+ </div>
405
+
406
+ <div className="overflow-x-auto">
407
+ <table className="min-w-full">
408
+ <thead>
409
+ <tr>
410
+ {selectAll && (
411
+ <th className="text-left border-b border-t bg-gray-50 px-4 py-4 dark:bg-gray-700 dark:text-white">
412
+ <input type="checkbox" onChange={handleSelectAll} />
413
+ </th>
414
+ )}
415
+ {/* Rendering column headers */}
416
+ {workingColumns.map((columnHeader: any, colIndex: number) => (
417
+ <th
418
+ key={colIndex}
419
+ className={tableHeaderStyle}
420
+ style={{
421
+ width: columnHeader.width
422
+ ? `${columnHeader.width}px`
423
+ : "auto",
424
+ }}
425
+ >
426
+ <div className="flex items-center relative">
427
+ {columnHeader.headerText
428
+ ? columnHeader.headerText
429
+ : columnHeader.field}
430
+ {columnHeader.filter && !columnHeader.template && (
431
+ <React.Fragment>
432
+ <button
433
+ onClick={() => {
434
+ toggleFilterPopup(colIndex);
435
+ }}
436
+ >
437
+ <Icon
438
+ dimensions={{ width: "12", height: "12" }}
439
+ elements={listFilter}
440
+ svgClass={`ml-2 fill-none dark:stroke-white ${
441
+ activeFilterArray &&
442
+ activeFilterArray.some(
443
+ (filter: any) =>
444
+ filter.filterColumn === columnHeader.field
445
+ )
446
+ ? "stroke-red-400"
447
+ : "stroke-black"
448
+ }`}
449
+ />
450
+ </button>
451
+ <FilterPopup
452
+ show={columnHeader.showFilterPopup}
453
+ columnHeader={columnHeader.field}
454
+ filterAction={(action: any) => {
455
+ handleFilterAction(action, colIndex);
456
+ }}
457
+ />
458
+ </React.Fragment>
459
+ )}
460
+ </div>
461
+ </th>
462
+ ))}
463
+ </tr>
464
+ </thead>
465
+ <tbody>
466
+ {/* rendering body */}
467
+ {workingDataSource.length > 0 && !isFetching ? (
468
+ workingDataSource
469
+ .slice(
470
+ currentPage * pageSettings.pageNumber,
471
+ (currentPage + 1) * pageSettings.pageNumber
472
+ )
473
+ .map((rowData: any, rowIndex: number) => (
474
+ <tr
475
+ key={rowIndex}
476
+ className="hover:bg-gray-50 dark:hover:bg-gray-900 cursor-pointer"
477
+ >
478
+ {selectAll && (
479
+ <td className={gridColumnStyleSelectAll}>
480
+ <input
481
+ type="checkbox"
482
+ checked={isRowSelected(rowData)}
483
+ onChange={() => handleSelect(rowData)}
484
+ />
485
+ </td>
486
+ )}
487
+ {workingColumns.map((column: any, colIndex: number) => (
488
+ <td
489
+ key={colIndex}
490
+ className={gridColumnStyle}
491
+ style={{
492
+ width: column.width ? `${column.width}px` : "auto",
493
+ }}
494
+ >
495
+ {renderCell(rowData, column, rowIndex)}
496
+ </td>
497
+ ))}
498
+ </tr>
499
+ ))
500
+ ) : (
501
+ <tr>
502
+ <td
503
+ colSpan={workingColumns.length}
504
+ className="text-center p-4 border-b text-sm"
505
+ >
506
+ {isFetching
507
+ ? "Fetching Data Please Wait..."
508
+ : "No Data Found"}
509
+ </td>
510
+ </tr>
511
+ )}
512
+ </tbody>
513
+ </table>
514
+ </div>
515
+ {/* Pagination logic */}
516
+ <div className="flex p-2 justify-between dark:text-white">
517
+ <div className="flex gap-4">
518
+ <button
519
+ onClick={goToFirstPage}
520
+ className={`${
521
+ currentPage === 0 ? "text-gray-200 dark:text-gray-700" : ""
522
+ }`}
523
+ >
524
+ <Icon
525
+ elements={leftArrows}
526
+ svgClass={`${
527
+ currentPage === 0
528
+ ? "stroke-gray-200 fill-none dark:stroke-gray-700"
529
+ : "stroke-black fill-none dark:stroke-white"
530
+ }`}
531
+ />
532
+ </button>
533
+ <button onClick={prevPage}>
534
+ <Icon
535
+ elements={leftArrow}
536
+ svgClass={`${
537
+ currentPage === 0
538
+ ? "stroke-gray-200 fill-none dark:stroke-gray-700"
539
+ : "stroke-black fill-none dark:stroke-white"
540
+ }`}
541
+ />
542
+ </button>
543
+ <div className="flex flex-row gap-3 items-center">
544
+ {pageStart > 0 && (
545
+ <button
546
+ className="p-1 w-5 h-5 flex items-center justify-center rounded-full"
547
+ onClick={() => goToPage(pageStart - 1)}
548
+ >
549
+ ...
550
+ </button>
551
+ )}
552
+ {workingDataSource.length > 0 &&
553
+ Array.from({ length: pageEnd - pageStart }).map((_, i) => (
554
+ <button
555
+ key={i}
556
+ onClick={() => goToPage(pageStart + i)}
557
+ className={`${
558
+ pageStart + i === currentPage
559
+ ? "font-bold text-white p-2 h-6 bg-black flex items-center justify-center rounded-md w-auto dark:bg-white dark:text-black"
560
+ : ""
561
+ }`}
562
+ >
563
+ {pageStart + i + 1}
564
+ </button>
565
+ ))}
566
+ {pageEnd < totalPages && (
567
+ <button onClick={() => goToPage(pageEnd)}>...</button>
568
+ )}
569
+ </div>
570
+ <button
571
+ onClick={nextPage}
572
+ className={`${
573
+ currentPage === totalPages - 1 || workingDataSource.length <= 0
574
+ ? "text-gray-200 dark:text-gray-700"
575
+ : ""
576
+ }`}
577
+ >
578
+ <Icon
579
+ elements={rightArrow}
580
+ svgClass={`${
581
+ currentPage === totalPages - 1 ||
582
+ workingDataSource.length <= 0
583
+ ? "stroke-gray-200 fill-none dark:stroke-gray-700"
584
+ : "stroke-black fill-none dark:stroke-white"
585
+ }`}
586
+ />
587
+ </button>
588
+ <button
589
+ onClick={goToEndPage}
590
+ className={`${
591
+ currentPage === totalPages - 1 || workingDataSource.length <= 0
592
+ ? "text-gray-200 dark:text-gray-700"
593
+ : ""
594
+ }`}
595
+ >
596
+ <Icon
597
+ elements={rightArrows}
598
+ svgClass={`${
599
+ currentPage === totalPages - 1 ||
600
+ workingDataSource.length <= 0
601
+ ? "stroke-gray-200 fill-none dark:stroke-gray-700"
602
+ : "stroke-black fill-none dark:stroke-white"
603
+ }`}
604
+ />
605
+ </button>
606
+ </div>
607
+ <div className="flex text-sm">
608
+ {currentPage + 1} of {totalPages} pages ({workingDataSource.length})
609
+ items
610
+ </div>
611
+ </div>
612
+ </React.Fragment>
613
+ </div>
614
+ );
615
+ });