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.
- package/index.js +108 -0
- package/package.json +28 -0
- package/source/components/button/index.tsx +25 -0
- package/source/components/checkbox/index.tsx +9 -0
- package/source/components/darkmode/index.tsx +48 -0
- package/source/components/datepicker/DatePickerHelper.ts +42 -0
- package/source/components/datepicker/index.tsx +258 -0
- package/source/components/datepicker/types.ts +9 -0
- package/source/components/dialog/index.tsx +44 -0
- package/source/components/dialog/types.ts +12 -0
- package/source/components/grid/FilterPopup.tsx +85 -0
- package/source/components/grid/Grid.tsx +615 -0
- package/source/components/grid/GridHelperFunctions.ts +218 -0
- package/source/components/grid/index.tsx +26 -0
- package/source/components/grid/type.ts +28 -0
- package/source/components/input/index.tsx +105 -0
- package/source/components/input/types.ts +9 -0
- package/source/components/modal/index.tsx +58 -0
- package/source/components/modal/types.ts +11 -0
- package/source/components/multiselect/index.tsx +213 -0
- package/source/components/multiselect/types.ts +14 -0
- package/source/components/select/index.tsx +220 -0
- package/source/components/select/types.ts +13 -0
- package/source/components/spinner/Stroke.tsx +89 -0
- package/source/components/spinner/index.tsx +54 -0
- package/source/components/spinner/types.ts +9 -0
- package/source/components/toast/Toast.tsx +61 -0
- package/source/components/toast/index.tsx +36 -0
- package/source/components/toast/toastAtom.ts +23 -0
- package/source/components/toast/types.ts +24 -0
- package/source/components/toast/useToast.ts +24 -0
- package/source/fallback/index.tsx +11 -0
- package/source/icon/Icon.tsx +55 -0
- package/source/icon/iconPaths.ts +92 -0
- package/source/index.ts +15 -0
- package/source/types.ts +108 -0
- package/source/utils.ts +10 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import * as XLSX from "xlsx";
|
|
2
|
+
import jsPDF from "jspdf";
|
|
3
|
+
import "jspdf-autotable";
|
|
4
|
+
|
|
5
|
+
let activeFilterArray: any[] = [];
|
|
6
|
+
|
|
7
|
+
interface PDFExportOptions {
|
|
8
|
+
layout: "portrait" | "landscape";
|
|
9
|
+
paperSize:
|
|
10
|
+
| "a3"
|
|
11
|
+
| "a4"
|
|
12
|
+
| "letter"
|
|
13
|
+
| "legal"
|
|
14
|
+
| "tabloid"
|
|
15
|
+
| "statement"
|
|
16
|
+
| "executive";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function handleApplyFilterHelper(
|
|
20
|
+
event: any,
|
|
21
|
+
columns: any,
|
|
22
|
+
dataSource: any
|
|
23
|
+
) {
|
|
24
|
+
let workingDataSource: any[] = [...dataSource];
|
|
25
|
+
let filterValue = event.filterValue.toLowerCase();
|
|
26
|
+
let filterCondition = event.filterType;
|
|
27
|
+
let filterColumn = event.columnHeader;
|
|
28
|
+
|
|
29
|
+
// Add the new filter to activeFilterArray
|
|
30
|
+
activeFilterArray.push({
|
|
31
|
+
filterValue: filterValue,
|
|
32
|
+
filterCondition: filterCondition,
|
|
33
|
+
filterColumn: filterColumn,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
columns = columns.map((column: any) => {
|
|
37
|
+
if (column.field === filterColumn) {
|
|
38
|
+
return { ...column, isFilterActive: true, showFilterPopup: false };
|
|
39
|
+
}
|
|
40
|
+
return column;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Apply all active filters
|
|
44
|
+
activeFilterArray.forEach((filter: any) => {
|
|
45
|
+
workingDataSource = dataSource.filter((item: any) => {
|
|
46
|
+
let columnValue = item[filter.filterColumn].toString().toLowerCase();
|
|
47
|
+
switch (filter.filterCondition) {
|
|
48
|
+
case "contains":
|
|
49
|
+
return columnValue.includes(filter.filterValue);
|
|
50
|
+
case "equals":
|
|
51
|
+
return columnValue === filter.filterValue;
|
|
52
|
+
case "starts_with":
|
|
53
|
+
return columnValue.startsWith(filter.filterValue);
|
|
54
|
+
case "ends_with":
|
|
55
|
+
return columnValue.endsWith(filter.filterValue);
|
|
56
|
+
default:
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return { columns, workingDataSource, activeFilterArray };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Function For Clearing the Filter
|
|
66
|
+
export function clearFilterHelper(
|
|
67
|
+
event: any,
|
|
68
|
+
columns: any[],
|
|
69
|
+
dataSource: any[]
|
|
70
|
+
) {
|
|
71
|
+
let workingDataSource = [...dataSource];
|
|
72
|
+
let filterColumn = event.columnHeader;
|
|
73
|
+
|
|
74
|
+
// Remove the cleared filter from activeFilterArray
|
|
75
|
+
activeFilterArray = activeFilterArray.filter(
|
|
76
|
+
(filter: any) => filter.filterColumn !== filterColumn
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
let hasActiveFilters = activeFilterArray.length > 0;
|
|
80
|
+
|
|
81
|
+
// Reset the isFilterActive and showFilterPopup flags for the cleared filter
|
|
82
|
+
columns = columns.map((column: any) => {
|
|
83
|
+
if (column.field === filterColumn) {
|
|
84
|
+
return { ...column, isFilterActive: false, showFilterPopup: false };
|
|
85
|
+
}
|
|
86
|
+
return column;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Reapply all remaining active filters
|
|
90
|
+
if (hasActiveFilters) {
|
|
91
|
+
activeFilterArray.forEach((filter: any) => {
|
|
92
|
+
workingDataSource = workingDataSource.filter((item: any) => {
|
|
93
|
+
let columnValue = item[filter.filterColumn].toString().toLowerCase();
|
|
94
|
+
switch (filter.filterCondition) {
|
|
95
|
+
case "contains":
|
|
96
|
+
return columnValue.includes(filter.filterValue);
|
|
97
|
+
case "equals":
|
|
98
|
+
return columnValue === filter.filterValue;
|
|
99
|
+
case "starts_with":
|
|
100
|
+
return columnValue.startsWith(filter.filterValue);
|
|
101
|
+
case "ends_with":
|
|
102
|
+
return columnValue.endsWith(filter.filterValue);
|
|
103
|
+
default:
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { columns, workingDataSource, hasActiveFilters, activeFilterArray };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function exportToExcelHelper(
|
|
114
|
+
dataSource: any[],
|
|
115
|
+
columns: any[],
|
|
116
|
+
excelName: string
|
|
117
|
+
) {
|
|
118
|
+
// This will remove the column template.
|
|
119
|
+
const templateRemovedColumn = columns.filter((column) => !column.template);
|
|
120
|
+
const dataToExport = dataSource.map((row) => {
|
|
121
|
+
const rowData: any = {};
|
|
122
|
+
templateRemovedColumn.forEach((column: any) => {
|
|
123
|
+
rowData[column.field] = row[column.field];
|
|
124
|
+
});
|
|
125
|
+
return rowData;
|
|
126
|
+
});
|
|
127
|
+
const ws = XLSX.utils.json_to_sheet(dataToExport);
|
|
128
|
+
const wb = XLSX.utils.book_new();
|
|
129
|
+
XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
|
|
130
|
+
XLSX.writeFile(wb, `${excelName}.xlsx`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function exportToPDFHelper(
|
|
134
|
+
dataSource: any[],
|
|
135
|
+
columns: any[],
|
|
136
|
+
pdfName: string,
|
|
137
|
+
pdfoptions?: PDFExportOptions
|
|
138
|
+
) {
|
|
139
|
+
const { layout = "portrait", paperSize = "a4" }: any = pdfoptions;
|
|
140
|
+
|
|
141
|
+
const doc: any = new jsPDF({
|
|
142
|
+
orientation: layout,
|
|
143
|
+
unit: "mm",
|
|
144
|
+
format: paperSize,
|
|
145
|
+
});
|
|
146
|
+
// This will remove the columns with template.
|
|
147
|
+
const printableColumns = columns.filter(
|
|
148
|
+
(column) => !column.template || column.showInPdf
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const dataToExport = dataSource.map((row) => {
|
|
152
|
+
const rowData: any = {};
|
|
153
|
+
printableColumns.forEach((column: any) => {
|
|
154
|
+
rowData[column.field] = row[column.field];
|
|
155
|
+
});
|
|
156
|
+
return Object.values(rowData);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const columnNames = printableColumns.map((column: any) => column.field);
|
|
160
|
+
doc.autoTable({
|
|
161
|
+
head: [columnNames],
|
|
162
|
+
body: dataToExport,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
doc.save(`${pdfName}.pdf`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function handleEditActionHelper(
|
|
169
|
+
e: any,
|
|
170
|
+
isEditModeActive: boolean,
|
|
171
|
+
actionMode: string,
|
|
172
|
+
newEntry: any,
|
|
173
|
+
workingDataSource: any[],
|
|
174
|
+
goToFirstPage: () => void
|
|
175
|
+
) {
|
|
176
|
+
const { mode } = e.detail;
|
|
177
|
+
let dataSourceUpdate = [...workingDataSource];
|
|
178
|
+
let isEditModeActiveUpdate: boolean = isEditModeActive;
|
|
179
|
+
let actionModeUpdate: string = actionMode;
|
|
180
|
+
let newEntryUpdate: any = { ...newEntry };
|
|
181
|
+
|
|
182
|
+
function resetEditMode() {
|
|
183
|
+
isEditModeActiveUpdate = false;
|
|
184
|
+
actionModeUpdate = "";
|
|
185
|
+
newEntryUpdate = {};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function addNewEntry() {
|
|
189
|
+
if (Object.keys(newEntryUpdate).length > 0) {
|
|
190
|
+
dataSourceUpdate = [newEntryUpdate, ...workingDataSource];
|
|
191
|
+
newEntryUpdate = {};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
switch (mode) {
|
|
196
|
+
case "add":
|
|
197
|
+
isEditModeActiveUpdate = true;
|
|
198
|
+
actionModeUpdate = mode;
|
|
199
|
+
goToFirstPage();
|
|
200
|
+
break;
|
|
201
|
+
case "cancel":
|
|
202
|
+
resetEditMode();
|
|
203
|
+
break;
|
|
204
|
+
case "update":
|
|
205
|
+
addNewEntry();
|
|
206
|
+
resetEditMode();
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
dataSourceUpdate,
|
|
214
|
+
isEditModeActiveUpdate,
|
|
215
|
+
actionModeUpdate,
|
|
216
|
+
newEntryUpdate,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
* Here We Will Re-Render Grid as a Memoized Component For Better Performance.
|
|
7
|
+
* Extended Documentation for Grid can be found at
|
|
8
|
+
* https://psychedelic-step-e70.notion.site/Data-GRID-by-GBS-R-D-20ff97c899d24bc590215a6196435fa3
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Grid as GridComponent } from "./Grid";
|
|
12
|
+
import React, { memo } from "react";
|
|
13
|
+
|
|
14
|
+
const Grid = memo(GridComponent, (prevProps, nextProps) => {
|
|
15
|
+
// Custom comparison function
|
|
16
|
+
return (
|
|
17
|
+
prevProps.dataSource === nextProps.dataSource &&
|
|
18
|
+
prevProps.columns === nextProps.columns &&
|
|
19
|
+
prevProps.pageSettings.pageNumber === nextProps.pageSettings.pageNumber &&
|
|
20
|
+
prevProps.enableSearch === nextProps.enableSearch &&
|
|
21
|
+
prevProps.enablePdfExport === nextProps.enablePdfExport &&
|
|
22
|
+
prevProps.enableExcelExport === nextProps.enableExcelExport
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export { Grid };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
interface PageSettingsProps {
|
|
2
|
+
pageNumber: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type GridProps = {
|
|
6
|
+
dataSource: any[] | string;
|
|
7
|
+
columns?: any[];
|
|
8
|
+
pageSettings: PageSettingsProps;
|
|
9
|
+
enableSearch?: boolean;
|
|
10
|
+
lazy?: boolean;
|
|
11
|
+
enableExcelExport?: boolean;
|
|
12
|
+
excelName?: string;
|
|
13
|
+
enablePdfExport?: boolean;
|
|
14
|
+
pdfName?: string;
|
|
15
|
+
gridContainerClass?: string;
|
|
16
|
+
gridButtonClass?: string;
|
|
17
|
+
gridHeaderClass?: string;
|
|
18
|
+
gridGlobalSearchButtonClass?: string;
|
|
19
|
+
gridPaginationButtonClass?: string;
|
|
20
|
+
pdfOptions?: any;
|
|
21
|
+
isFetching?: boolean;
|
|
22
|
+
showPagination?: boolean;
|
|
23
|
+
selectAll?: boolean;
|
|
24
|
+
onSelectRow?: (value: any) => void;
|
|
25
|
+
tableHeaderStyle?: string;
|
|
26
|
+
gridColumnStyleSelectAll?: string;
|
|
27
|
+
gridColumnStyle?: string;
|
|
28
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useRef } from "react";
|
|
9
|
+
import { twMerge } from "tailwind-merge";
|
|
10
|
+
import Icon from "../icon/Icon";
|
|
11
|
+
import { eye } from "../icon/iconPaths";
|
|
12
|
+
import type { InputProps } from "./types";
|
|
13
|
+
|
|
14
|
+
export const Input = ({
|
|
15
|
+
OTPField = false,
|
|
16
|
+
OTPValue = "",
|
|
17
|
+
OTPLength = 4,
|
|
18
|
+
OTPClass = "w-8 h-10 m-1 border border-gray-600 rounded-lg text-center",
|
|
19
|
+
onOTPValueChange,
|
|
20
|
+
...props
|
|
21
|
+
}: InputProps) => {
|
|
22
|
+
const [otpValues, setOtpValues] = useState(new Array(OTPLength).fill(""));
|
|
23
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
24
|
+
const inputRefs = useRef<any>([]);
|
|
25
|
+
|
|
26
|
+
const defaultClass = "bg-gray-100 p-2 rounded-lg";
|
|
27
|
+
|
|
28
|
+
// This will update the OTP Field Value
|
|
29
|
+
const updateOtpValue = (index: number, e: any) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
const input = e.target;
|
|
32
|
+
if (input) {
|
|
33
|
+
const newOtpValues = [...otpValues];
|
|
34
|
+
newOtpValues[index] = input.value;
|
|
35
|
+
setOtpValues(newOtpValues);
|
|
36
|
+
if (index < OTPLength - 1 && input.value) {
|
|
37
|
+
inputRefs.current[index + 1].focus();
|
|
38
|
+
}
|
|
39
|
+
OTPValue = newOtpValues.join("");
|
|
40
|
+
if (onOTPValueChange) onOTPValueChange(OTPValue);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Function for handling keyboard navigation in OTP Field
|
|
45
|
+
const handleKeydown = (index: number, e: any) => {
|
|
46
|
+
if (e.key === "Backspace" && otpValues[index] === "" && index > 0) {
|
|
47
|
+
inputRefs.current[index - 1].focus();
|
|
48
|
+
} else if (e.key === "ArrowLeft" && index > 0) {
|
|
49
|
+
inputRefs.current[index - 1].focus();
|
|
50
|
+
} else if (e.key === "ArrowRight" && index < OTPLength - 1) {
|
|
51
|
+
inputRefs.current[index + 1].focus();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Password Feild Toggler
|
|
56
|
+
const toggleTypeForPassword = () => {
|
|
57
|
+
setShowPassword(!showPassword);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div>
|
|
62
|
+
{!OTPField ? (
|
|
63
|
+
<div className="text-input-container w-60 relative">
|
|
64
|
+
<input
|
|
65
|
+
className={twMerge(defaultClass, "w-full focus:outline-blue-400")}
|
|
66
|
+
{...props}
|
|
67
|
+
type={
|
|
68
|
+
props.type === "password" && showPassword ? "text" : props.type
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
{/* If Type is password, this will show a password visible toggle button */}
|
|
72
|
+
{props.type && props.type === "password" && (
|
|
73
|
+
<button
|
|
74
|
+
className="absolute inset-y-0 right-0 flex items-center pr-2"
|
|
75
|
+
onClick={toggleTypeForPassword}
|
|
76
|
+
>
|
|
77
|
+
<Icon
|
|
78
|
+
elements={eye}
|
|
79
|
+
svgClass={"h-5 w-5 stroke-gray-500 fill-none dark:stroke-white"}
|
|
80
|
+
/>
|
|
81
|
+
</button>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
) : (
|
|
85
|
+
<div className="otp-container">
|
|
86
|
+
{Array(OTPLength)
|
|
87
|
+
.fill("")
|
|
88
|
+
.map((_, index) => (
|
|
89
|
+
<input
|
|
90
|
+
key={index}
|
|
91
|
+
type="text"
|
|
92
|
+
maxLength={1}
|
|
93
|
+
pattern="[0-9]*"
|
|
94
|
+
value={otpValues[index]}
|
|
95
|
+
onChange={(e) => updateOtpValue(index, e)}
|
|
96
|
+
onKeyDown={(e) => handleKeydown(index, e)}
|
|
97
|
+
ref={(ref) => (inputRefs.current[index] = ref)}
|
|
98
|
+
className={OTPClass}
|
|
99
|
+
/>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState } from "react";
|
|
9
|
+
import { twMerge } from "tailwind-merge";
|
|
10
|
+
import type { ModalProps } from "./types";
|
|
11
|
+
|
|
12
|
+
export const Modal = ({
|
|
13
|
+
showModal = false,
|
|
14
|
+
modalTitle = "Modal Title",
|
|
15
|
+
autoclose = false,
|
|
16
|
+
modalClass = "fixed z-10 overflow-y-auto inset-0 flex items-center justify-center bg-gray-500 bg-opacity-75 transition-opacity",
|
|
17
|
+
modalContentClass = "bg-white m-10 md:w-[80vh] rounded-xl",
|
|
18
|
+
classModalContent = "",
|
|
19
|
+
modalTitleClass = "p-4 text-lg leading-6 font-medium text-gray-900 flex justify-between",
|
|
20
|
+
classModalTitle = "",
|
|
21
|
+
children,
|
|
22
|
+
}: ModalProps) => {
|
|
23
|
+
const [show, setShow] = useState(showModal);
|
|
24
|
+
|
|
25
|
+
const autoCloseHandler = () => {
|
|
26
|
+
if (autoclose) {
|
|
27
|
+
setShow(false);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
{show && (
|
|
34
|
+
<div
|
|
35
|
+
className={modalClass}
|
|
36
|
+
aria-labelledby="modal-title"
|
|
37
|
+
role="dialog"
|
|
38
|
+
aria-modal="true"
|
|
39
|
+
onClick={autoCloseHandler}
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
className={twMerge(modalContentClass, classModalContent)}
|
|
43
|
+
onClick={(e: any) => {
|
|
44
|
+
e.stopPropagation();
|
|
45
|
+
}}
|
|
46
|
+
role="dialog"
|
|
47
|
+
>
|
|
48
|
+
<div className={twMerge(modalTitleClass, classModalTitle)}>
|
|
49
|
+
{modalTitle}
|
|
50
|
+
</div>
|
|
51
|
+
<hr />
|
|
52
|
+
<div className="p-4">{children}</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ModalProps = {
|
|
2
|
+
showModal?: boolean;
|
|
3
|
+
modalTitle?: string;
|
|
4
|
+
autoclose?: boolean;
|
|
5
|
+
modalClass?: string;
|
|
6
|
+
modalContentClass?: string;
|
|
7
|
+
classModalContent?: string;
|
|
8
|
+
modalTitleClass?: string;
|
|
9
|
+
classModalTitle?: string;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, {
|
|
9
|
+
forwardRef,
|
|
10
|
+
memo,
|
|
11
|
+
useEffect,
|
|
12
|
+
useImperativeHandle,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
} from "react";
|
|
16
|
+
import Icon from "../icon/Icon";
|
|
17
|
+
import { check, search, upDown, x } from "../icon/iconPaths";
|
|
18
|
+
import { getSourceData } from "../utils";
|
|
19
|
+
import type { MultiSelectProps } from "./types";
|
|
20
|
+
|
|
21
|
+
interface ItemsProps {
|
|
22
|
+
value: string;
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MultiSelect = forwardRef<any, MultiSelectProps>((props, ref) => {
|
|
27
|
+
const {
|
|
28
|
+
placeholder = "Select an Item...",
|
|
29
|
+
items,
|
|
30
|
+
lazy = false,
|
|
31
|
+
showSearch = true,
|
|
32
|
+
onSelect,
|
|
33
|
+
truncate = true,
|
|
34
|
+
selectedItems = [],
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
const [showPopover, setShowPopover] = useState(false);
|
|
38
|
+
const [workingDataSource, setWorkingDataSource] = useState<ItemsProps[]>([]);
|
|
39
|
+
const [filteredItems, setFilteredItems] = useState<ItemsProps[]>([]);
|
|
40
|
+
const [selected, setSelected] = useState<string[]>(selectedItems);
|
|
41
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
42
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
43
|
+
const selectRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
inputRef.current?.focus();
|
|
47
|
+
}, [showPopover]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
handleDataSource();
|
|
51
|
+
}, [items]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (onSelect) onSelect(selected);
|
|
55
|
+
}, [selected, onSelect]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
59
|
+
if (
|
|
60
|
+
selectRef.current &&
|
|
61
|
+
!selectRef.current.contains(event.target as Node)
|
|
62
|
+
) {
|
|
63
|
+
setShowPopover(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
67
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const getSelectItems = async (itemsApi: string) => {
|
|
71
|
+
const itemsData = await getSourceData(itemsApi);
|
|
72
|
+
setWorkingDataSource(itemsData.sourcedata);
|
|
73
|
+
setFilteredItems(itemsData.sourcedata);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleDataSource = async () => {
|
|
77
|
+
if (Array.isArray(items)) {
|
|
78
|
+
setWorkingDataSource(items);
|
|
79
|
+
setFilteredItems(items);
|
|
80
|
+
} else if (typeof items === "string") {
|
|
81
|
+
await getSelectItems(items);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const togglePopover = (e: React.MouseEvent) => {
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
setShowPopover(!showPopover);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleSelect = (value: string) => {
|
|
91
|
+
setSelected((prev) =>
|
|
92
|
+
prev.includes(value)
|
|
93
|
+
? prev.filter((item) => item !== value)
|
|
94
|
+
: [...prev, value]
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const getSelectedDisplay = () => {
|
|
99
|
+
if (selected.length === 0) return placeholder;
|
|
100
|
+
const displayedItems = selected
|
|
101
|
+
.slice(0, 3)
|
|
102
|
+
.map(
|
|
103
|
+
(value) =>
|
|
104
|
+
workingDataSource.find((item) => item.value === value)?.label || ""
|
|
105
|
+
);
|
|
106
|
+
let display = displayedItems.join(", ");
|
|
107
|
+
if (selected.length > 3 && truncate) {
|
|
108
|
+
display += `, +${selected.length - 3} more`;
|
|
109
|
+
}
|
|
110
|
+
return display;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const inputSearchHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
114
|
+
const value = e.target.value.toLowerCase();
|
|
115
|
+
setSearchTerm(value);
|
|
116
|
+
if (!lazy) {
|
|
117
|
+
setFilteredItems(
|
|
118
|
+
workingDataSource.filter((item) =>
|
|
119
|
+
Object.values(item).some((val) =>
|
|
120
|
+
val.toString().toLowerCase().includes(value)
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const clearSelected = () => {
|
|
128
|
+
setSelected([]);
|
|
129
|
+
setShowPopover(false);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
useImperativeHandle(ref, () => ({
|
|
133
|
+
workingDataSource,
|
|
134
|
+
items,
|
|
135
|
+
clearSelected,
|
|
136
|
+
togglePopover,
|
|
137
|
+
getSelectItems,
|
|
138
|
+
selectedDisplay: getSelectedDisplay(),
|
|
139
|
+
selected,
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="relative w-[200px]" ref={selectRef}>
|
|
144
|
+
<div className="relative border w-[200px] flex items-center px-4 py-2 rounded-lg">
|
|
145
|
+
<button onClick={togglePopover} className="flex-grow text-sm text-left">
|
|
146
|
+
{getSelectedDisplay()}
|
|
147
|
+
</button>
|
|
148
|
+
<div className="flex items-center space-x-2">
|
|
149
|
+
{selected.length > 0 && (
|
|
150
|
+
<button className="flex items-center px-2" onClick={clearSelected}>
|
|
151
|
+
<Icon
|
|
152
|
+
elements={x}
|
|
153
|
+
svgClass="h-4 w-4 stroke-gray-500 fill-none dark:stroke-white"
|
|
154
|
+
/>
|
|
155
|
+
</button>
|
|
156
|
+
)}
|
|
157
|
+
<button onClick={togglePopover}>
|
|
158
|
+
<Icon
|
|
159
|
+
elements={upDown}
|
|
160
|
+
svgClass="h-4 w-4 stroke-gray-500 fill-none dark:stroke-white"
|
|
161
|
+
/>
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{showPopover && (
|
|
167
|
+
<div className="w-[200px] absolute overflow-y-auto border px-2 rounded-lg mt-[1px] scrollbar bg-white z-50 scrollbar h-auto dark:bg-black dark:text-white">
|
|
168
|
+
{showSearch && (
|
|
169
|
+
<div className="flex p-2 gap-1 items-center sticky top-0 bg-white border-b dark:bg-black">
|
|
170
|
+
<Icon
|
|
171
|
+
elements={search}
|
|
172
|
+
svgClass="stroke-gray-500 fill-none dark:stroke-white"
|
|
173
|
+
/>
|
|
174
|
+
<input
|
|
175
|
+
autoComplete="off"
|
|
176
|
+
type="text"
|
|
177
|
+
name="search"
|
|
178
|
+
id="search"
|
|
179
|
+
placeholder="Search a value"
|
|
180
|
+
className="w-full outline-none dark:bg-black"
|
|
181
|
+
ref={inputRef}
|
|
182
|
+
value={searchTerm}
|
|
183
|
+
onChange={inputSearchHandler}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{filteredItems.length > 0 ? (
|
|
188
|
+
filteredItems.map(({ value, label }) => (
|
|
189
|
+
<button
|
|
190
|
+
key={value}
|
|
191
|
+
className="flex items-center w-full px-2 py-1 text-left hover:bg-blue-100 gap-2 rounded-lg mt-1 text-sm dark:hover:bg-blue-600"
|
|
192
|
+
onClick={() => handleSelect(value)}
|
|
193
|
+
>
|
|
194
|
+
<Icon
|
|
195
|
+
elements={check}
|
|
196
|
+
svgClass={`h-4 w-4 fill-none ${
|
|
197
|
+
selected.includes(value) ? "stroke-gray-500" : ""
|
|
198
|
+
}`}
|
|
199
|
+
/>
|
|
200
|
+
{label}
|
|
201
|
+
</button>
|
|
202
|
+
))
|
|
203
|
+
) : (
|
|
204
|
+
<div className="text-sm text-center">No Data Found</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const MemoizedMultiSelect = memo(MultiSelect);
|
|
213
|
+
export { MemoizedMultiSelect as MultiSelect };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface ItemsProps {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type MultiSelectProps = {
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
items?: ItemsProps[] | string;
|
|
9
|
+
lazy?: boolean;
|
|
10
|
+
showSearch?: boolean;
|
|
11
|
+
onSelect?: any;
|
|
12
|
+
truncate?: boolean;
|
|
13
|
+
selectedItems?: string[];
|
|
14
|
+
};
|