next-helios-fe 1.9.18 → 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-helios-fe",
3
- "version": "1.9.18",
3
+ "version": "1.10.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,7 +21,7 @@ import {
21
21
  MultipleSelect,
22
22
  type MultipleSelectProps,
23
23
  } from "./other/multipleSelect";
24
- import { Autocomplete, type AutocompleteProps } from "./other/autocomplete";
24
+ import { ModalSelect, type ModalSelectProps } from "./other/modalSelect";
25
25
  import { Rating, type RatingProps } from "./other/rating";
26
26
  import { Emoji, type EmojiProps } from "./other/emoji";
27
27
 
@@ -48,7 +48,7 @@ interface FormComponent extends React.FC<FormProps> {
48
48
  Textarea: React.FC<TextareaProps>;
49
49
  Select: React.FC<SelectProps>;
50
50
  MultipleSelect: React.FC<MultipleSelectProps>;
51
- Autocomplete: React.FC<AutocompleteProps>;
51
+ ModalSelect: React.FC<ModalSelectProps>;
52
52
  Rating: React.FC<RatingProps>;
53
53
  Emoji: React.FC<EmojiProps>;
54
54
  }
@@ -85,6 +85,6 @@ Form.Pin = Pin;
85
85
  Form.Textarea = Textarea;
86
86
  Form.Select = Select;
87
87
  Form.MultipleSelect = MultipleSelect;
88
- Form.Autocomplete = Autocomplete;
88
+ Form.ModalSelect = ModalSelect;
89
89
  Form.Rating = Rating;
90
90
  Form.Emoji = Emoji;
@@ -0,0 +1,395 @@
1
+ "use client";
2
+ import React, { useState, useEffect, useRef } from "react";
3
+ import { Modal, Form, Button } from "../../../components";
4
+ import { Icon } from "@iconify/react";
5
+ import { useDebouncedCallback } from "use-debounce";
6
+
7
+ export interface ModalSelectProps {
8
+ type: "select" | "multipleSelect";
9
+ data: {
10
+ searchableColumns?: { label: string; value: string }[];
11
+ menus: {
12
+ label: string;
13
+ subLabel?: string;
14
+ value: string;
15
+ disabled?: boolean;
16
+ [key: string]: any;
17
+ }[];
18
+ };
19
+ label?: string;
20
+ placeholder?: string;
21
+ description?: string;
22
+ max?: number;
23
+ // labelComponent?: (e?: any) => React.ReactNode;
24
+ options?: {
25
+ width?: "full" | "fit";
26
+ height?: "short" | "medium" | "high";
27
+ };
28
+ disabled?: boolean;
29
+ required?: boolean;
30
+ loading?: boolean;
31
+ value?: string | string[];
32
+ onChange?: (e: {
33
+ target: {
34
+ value: any;
35
+ };
36
+ }) => void;
37
+ dynamicSelect?: {
38
+ getValue?: (value: { filter: any[]; maxRow: number }) => void;
39
+ setValue?: {
40
+ totalData?: number;
41
+ };
42
+ };
43
+ }
44
+
45
+ export const ModalSelect: React.FC<ModalSelectProps> = ({
46
+ type,
47
+ data,
48
+ label,
49
+ placeholder,
50
+ description,
51
+ max,
52
+ // labelComponent,
53
+ options,
54
+ disabled,
55
+ required,
56
+ loading,
57
+ value,
58
+ onChange,
59
+ dynamicSelect,
60
+ }) => {
61
+ const optionContainerRef = useRef<HTMLDivElement>(null);
62
+ const [selectedMenuHistory, setSelectedMenuHistory] = useState<any>([]);
63
+ const [tempValue, setTempValue] = useState<any>();
64
+ const [openModal, setOpenModal] = useState<boolean>(false);
65
+ const [searchBy, setSearchBy] = useState<string>(
66
+ data.searchableColumns ? data.searchableColumns[0].value : ""
67
+ );
68
+ const [maxData, setMaxData] = useState<number>(20);
69
+ const [search, setSearch] = useState<string>("");
70
+ const [delayedSearch, setDelayedSearch] = useState<string>("");
71
+
72
+ useEffect(() => {
73
+ if (value) {
74
+ setTempValue(value);
75
+ }
76
+ }, [value]);
77
+
78
+ useEffect(() => {
79
+ if (!loading) {
80
+ if (openModal) {
81
+ const container = optionContainerRef.current;
82
+
83
+ if (container) {
84
+ const handleScroll = () => {
85
+ if (
86
+ container.scrollTop + container.offsetHeight - 3 >=
87
+ container.scrollHeight
88
+ ) {
89
+ if (
90
+ dynamicSelect?.setValue?.totalData &&
91
+ data.menus.length !== dynamicSelect?.setValue?.totalData
92
+ ) {
93
+ setMaxData((prev: any) => prev + 20);
94
+ if (dynamicSelect?.getValue) {
95
+ dynamicSelect?.getValue({
96
+ filter: [{ key: searchBy, value: delayedSearch }],
97
+ maxRow: maxData + 20,
98
+ });
99
+ }
100
+ }
101
+ }
102
+ };
103
+
104
+ container.addEventListener("scroll", handleScroll);
105
+
106
+ return () => {
107
+ container.removeEventListener("scroll", handleScroll);
108
+ };
109
+ }
110
+ }
111
+ }
112
+ }, [
113
+ optionContainerRef,
114
+ loading,
115
+ data.menus,
116
+ openModal,
117
+ searchBy,
118
+ delayedSearch,
119
+ maxData,
120
+ dynamicSelect?.setValue?.totalData,
121
+ ]);
122
+
123
+ useEffect(() => {
124
+ if (dynamicSelect?.getValue) {
125
+ dynamicSelect.getValue({
126
+ filter: [{ key: searchBy, value: delayedSearch }],
127
+ maxRow: maxData,
128
+ });
129
+ }
130
+ }, [delayedSearch]);
131
+
132
+ const debouncedUpdate = useDebouncedCallback((value: string) => {
133
+ setDelayedSearch(value);
134
+ }, 1250);
135
+
136
+ return (
137
+ <>
138
+ <div
139
+ className={`${options?.width === "fit" ? "w-fit" : "w-full"}`}
140
+ onClick={() => {
141
+ if (!disabled) {
142
+ setOpenModal(true);
143
+ }
144
+ }}
145
+ >
146
+ {type === "select" ? (
147
+ <Form.Select
148
+ menus={
149
+ dynamicSelect?.getValue
150
+ ? [
151
+ ...selectedMenuHistory,
152
+ ...data.menus.filter(
153
+ (prev: any) =>
154
+ !selectedMenuHistory
155
+ .map((historyPrev: any) => historyPrev.value)
156
+ .includes(prev.value)
157
+ ),
158
+ ]
159
+ : data.menus
160
+ }
161
+ label={label}
162
+ placeholder={placeholder}
163
+ description={description}
164
+ options={options}
165
+ disabled={disabled}
166
+ required={required}
167
+ value={tempValue || ""}
168
+ readOnly
169
+ />
170
+ ) : (
171
+ <Form.MultipleSelect
172
+ menus={
173
+ dynamicSelect?.getValue
174
+ ? [
175
+ ...selectedMenuHistory,
176
+ ...data.menus.filter(
177
+ (prev: any) =>
178
+ !selectedMenuHistory
179
+ .map((historyPrev: any) => historyPrev.value)
180
+ .includes(prev.value)
181
+ ),
182
+ ]
183
+ : data.menus
184
+ }
185
+ label={label}
186
+ placeholder={placeholder}
187
+ description={description}
188
+ options={options}
189
+ disabled={disabled}
190
+ required={required}
191
+ value={tempValue || []}
192
+ readOnly
193
+ />
194
+ )}
195
+ </div>
196
+ <Modal
197
+ title={label || ""}
198
+ open={openModal}
199
+ onClose={() => {
200
+ setSearch("");
201
+ setOpenModal(false);
202
+ if (dynamicSelect?.getValue) {
203
+ setDelayedSearch("");
204
+ }
205
+ }}
206
+ >
207
+ <div className="flex flex-col gap-4 w-full h-full overflow-hidden">
208
+ <div className="flex items-end gap-4">
209
+ {data.searchableColumns && (
210
+ <div className="w-36">
211
+ <Form.Select
212
+ menus={data.searchableColumns || []}
213
+ placeholder="Search By"
214
+ value={searchBy}
215
+ onChange={(e) => {
216
+ setSearchBy(e.target.value);
217
+ setSearch("");
218
+ setDelayedSearch("");
219
+ if (dynamicSelect?.getValue) {
220
+ dynamicSelect.getValue({
221
+ filter: [{ key: e.target.value, value: "" }],
222
+ maxRow: maxData,
223
+ });
224
+ }
225
+ }}
226
+ />
227
+ </div>
228
+ )}
229
+ <div className="flex-1">
230
+ <Form.Search
231
+ placeholder="Search..."
232
+ disabled={data.searchableColumns && !searchBy}
233
+ value={search || ""}
234
+ onChange={(e) => {
235
+ setSearch(e.target.value);
236
+ debouncedUpdate(e.target.value);
237
+ }}
238
+ />
239
+ </div>
240
+ </div>
241
+ <div
242
+ ref={optionContainerRef}
243
+ className="relative flex-1 flex flex-col border rounded-md overflow-auto"
244
+ >
245
+ {(dynamicSelect?.getValue
246
+ ? delayedSearch
247
+ ? data.menus
248
+ : [
249
+ ...selectedMenuHistory,
250
+ ...data.menus.filter(
251
+ (prev: any) =>
252
+ !selectedMenuHistory
253
+ .map((historyPrev: any) => historyPrev.value)
254
+ .includes(prev.value)
255
+ ),
256
+ ]
257
+ : data.menus.filter((optionItem: any) =>
258
+ optionItem.label
259
+ .toLowerCase()
260
+ .includes(search.toLocaleLowerCase())
261
+ )
262
+ )?.length === 0 ? (
263
+ <div className="flex-1 flex justify-center items-center">
264
+ <span>No data found</span>
265
+ </div>
266
+ ) : (
267
+ (dynamicSelect?.getValue
268
+ ? delayedSearch
269
+ ? data.menus
270
+ : [
271
+ ...selectedMenuHistory,
272
+ ...data.menus.filter(
273
+ (prev: any) =>
274
+ !selectedMenuHistory
275
+ .map((historyPrev: any) => historyPrev.value)
276
+ .includes(prev.value)
277
+ ),
278
+ ]
279
+ : data.menus.filter((optionItem: any) =>
280
+ optionItem.label
281
+ .toLowerCase()
282
+ .includes(search.toLocaleLowerCase())
283
+ )
284
+ ).map((optionItem: any, index: number) => {
285
+ return (
286
+ <Button
287
+ key={index}
288
+ type="button"
289
+ className="disabled:bg-primary-transparent"
290
+ disabled={
291
+ type === "select" && tempValue === optionItem.value
292
+ }
293
+ onClick={() => {
294
+ if (type === "select") {
295
+ setTempValue(optionItem.value);
296
+ setOpenModal(false);
297
+ setSearch("");
298
+ if (dynamicSelect?.getValue) {
299
+ setSelectedMenuHistory([optionItem]);
300
+ setDelayedSearch("");
301
+ }
302
+ if (onChange) {
303
+ onChange({
304
+ target: {
305
+ value: optionItem.value,
306
+ } as any,
307
+ } as any);
308
+ }
309
+ } else {
310
+ if (tempValue?.includes(optionItem.value)) {
311
+ setTempValue((prev: any) =>
312
+ prev.filter(
313
+ (prevItem: any) => prevItem !== optionItem.value
314
+ )
315
+ );
316
+ if (dynamicSelect?.getValue) {
317
+ setSelectedMenuHistory((prev: any) =>
318
+ prev.filter(
319
+ (prevItem: any) =>
320
+ prevItem.value !== optionItem.value
321
+ )
322
+ );
323
+ }
324
+ if (onChange) {
325
+ onChange({
326
+ target: {
327
+ value: tempValue.filter(
328
+ (prevItem: any) =>
329
+ prevItem !== optionItem.value
330
+ ),
331
+ } as any,
332
+ } as any);
333
+ }
334
+ } else {
335
+ if (!max || tempValue.length < max) {
336
+ setTempValue((prev: any) => [
337
+ ...prev,
338
+ optionItem.value,
339
+ ]);
340
+ if (dynamicSelect?.getValue) {
341
+ setSelectedMenuHistory((prev: any) => [
342
+ ...prev,
343
+ optionItem,
344
+ ]);
345
+ }
346
+ if (onChange) {
347
+ onChange({
348
+ target: {
349
+ value: [...tempValue, optionItem.value],
350
+ } as any,
351
+ } as any);
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }}
357
+ >
358
+ {type === "select" ? (
359
+ <div className="flex flex-col">
360
+ <span className="text-sm">{optionItem.label}</span>
361
+ <span className="text-xs">{optionItem.subLabel}</span>
362
+ </div>
363
+ ) : (
364
+ <div className="flex gap-4 pointer-events-none">
365
+ <Form.Checkbox
366
+ options={{ disableHover: true }}
367
+ checked={
368
+ tempValue?.includes(optionItem.value) ?? false
369
+ }
370
+ readOnly
371
+ />
372
+ <div className="flex flex-col">
373
+ <span className="text-sm">{optionItem.label}</span>
374
+ <span className="text-xs">{optionItem.subLabel}</span>
375
+ </div>
376
+ </div>
377
+ )}
378
+ </Button>
379
+ );
380
+ })
381
+ )}
382
+ {loading && (
383
+ <div className="absolute left-0 top-0 flex justify-center items-center w-full h-full backdrop-blur-sm">
384
+ <Icon
385
+ icon="mingcute:loading-fill"
386
+ className="text-xl text-primary animate-spin"
387
+ />
388
+ </div>
389
+ )}
390
+ </div>
391
+ </div>
392
+ </Modal>
393
+ </>
394
+ );
395
+ };
@@ -179,12 +179,12 @@ export const MultipleSelect: React.FC<MultipleSelectProps> = ({
179
179
  value={tempValue.join(", ")}
180
180
  onChange={(e) => {}}
181
181
  onClick={(e) => {
182
- e.preventDefault();
183
- dropdownTriggerRef.current?.click();
184
- setDropdownWidth(
185
- inputRef?.current?.getBoundingClientRect()?.width || 0
186
- );
187
182
  if (!readOnly) {
183
+ e.preventDefault();
184
+ dropdownTriggerRef.current?.click();
185
+ setDropdownWidth(
186
+ inputRef?.current?.getBoundingClientRect()?.width || 0
187
+ );
188
188
  setOpenDropdown(true);
189
189
  }
190
190
  }}
@@ -116,12 +116,12 @@ export const Select: React.FC<SelectProps> = ({
116
116
  }
117
117
  onChange={(e) => {}}
118
118
  onClick={(e) => {
119
- e.preventDefault();
120
- dropdownTriggerRef.current?.click();
121
- setDropdownWidth(
122
- inputRef?.current?.getBoundingClientRect()?.width || 0
123
- );
124
119
  if (!readOnly) {
120
+ e.preventDefault();
121
+ dropdownTriggerRef.current?.click();
122
+ setDropdownWidth(
123
+ inputRef?.current?.getBoundingClientRect()?.width || 0
124
+ );
125
125
  setOpenDropdown(true);
126
126
  }
127
127
  }}
@@ -1,217 +0,0 @@
1
- "use client";
2
- import React, { useState, useEffect, useRef } from "react";
3
- import { Tooltip } from "../../../components";
4
- import { createPortal } from "react-dom";
5
- import { Icon } from "@iconify/react";
6
-
7
- export interface AutocompleteProps
8
- extends React.SelectHTMLAttributes<HTMLSelectElement> {
9
- menus: {
10
- label: string;
11
- value: string;
12
- [key: string]: any;
13
- }[];
14
- label?: string;
15
- placeholder?: string;
16
- description?: string;
17
- options?: {
18
- width?: "full" | "fit";
19
- height?: "short" | "medium" | "high";
20
- };
21
- }
22
-
23
- export const Autocomplete: React.FC<AutocompleteProps> = ({
24
- menus,
25
- label,
26
- placeholder,
27
- description,
28
- options,
29
- ...rest
30
- }) => {
31
- const [tempValue, setTempValue] = useState("");
32
- const [tempFilter, setTempFilter] = useState("");
33
- const [openDropdown, setOpenDropdown] = useState(false);
34
- const [position, setPosition] = useState<{
35
- top: number;
36
- left: number;
37
- } | null>(null);
38
- const [dropdownWidth, setDropdownWidth] = useState<number>(0);
39
- const triggerRef = useRef<HTMLDivElement>(null);
40
- const dropdownRef = useRef<HTMLDivElement>(null);
41
- const width = options?.width === "fit" ? "w-fit" : "w-full";
42
- const height =
43
- options?.height === "short"
44
- ? "py-1"
45
- : options?.height === "high"
46
- ? "py-2"
47
- : "py-1.5";
48
-
49
- useEffect(() => {
50
- const handleClickOutside = (e: MouseEvent) => {
51
- if (
52
- dropdownRef.current &&
53
- !dropdownRef.current.contains(e.target as Node) &&
54
- !triggerRef.current?.contains(e.target as Node)
55
- ) {
56
- setOpenDropdown(false);
57
- }
58
- };
59
-
60
- document.addEventListener("mousedown", handleClickOutside);
61
- return () => {
62
- document.removeEventListener("mousedown", handleClickOutside);
63
- };
64
- }, []);
65
-
66
- useEffect(() => {
67
- if (triggerRef.current) {
68
- const rect = triggerRef.current.getBoundingClientRect();
69
- const dropdownHeight = dropdownRef.current?.offsetHeight || 0;
70
- const windowHeight = window.innerHeight;
71
-
72
- setPosition({
73
- top: rect.bottom + window.scrollY + 10,
74
- left: rect.left + window.scrollX,
75
- });
76
-
77
- setDropdownWidth(rect.width);
78
-
79
- if (rect.bottom + dropdownHeight > windowHeight) {
80
- setPosition((prev) =>
81
- prev
82
- ? { ...prev, top: rect.top + window.scrollY - dropdownHeight - 10 }
83
- : null
84
- );
85
- }
86
- }
87
-
88
- if (openDropdown) {
89
- document.getElementById("body")!.style.overflow = "hidden";
90
- } else {
91
- document.getElementById("body")!.style.overflow = "auto";
92
- }
93
- }, [openDropdown, tempValue]);
94
-
95
- useEffect(() => {
96
- if (rest.value || rest.value === "") {
97
- setTempValue(rest.value as string);
98
- setTempFilter(
99
- menus.find((item) => item.value === rest.value)?.label as string
100
- );
101
- return;
102
- } else if (rest.defaultValue || rest.defaultValue === "") {
103
- setTempValue(rest.defaultValue as string);
104
- setTempFilter(
105
- menus.find((item) => item.value === rest.defaultValue)?.label as string
106
- );
107
- return;
108
- }
109
- }, [rest.value, rest.defaultValue]);
110
-
111
- return (
112
- <label className={`flex flex-col gap-2 ${width}`}>
113
- {(label || description) && (
114
- <div className="flex justify-between items-center gap-2">
115
- {label && (
116
- <span
117
- className={`text-sm select-none ${
118
- rest.required &&
119
- "after:content-['*'] after:ml-1 after:text-danger"
120
- }`}
121
- >
122
- {label}
123
- </span>
124
- )}
125
- {description && (
126
- <Tooltip content={description}>
127
- <Icon
128
- icon="octicon:info-16"
129
- className="text-sm text-primary-dark"
130
- />
131
- </Tooltip>
132
- )}
133
- </div>
134
- )}
135
- <div className="relative" ref={triggerRef}>
136
- <div
137
- className="relative flex items-center cursor-pointer"
138
- onClick={() => setOpenDropdown(!openDropdown)}
139
- >
140
- <input
141
- type="text"
142
- className={`w-full ps-4 pe-14 border-default border rounded-md bg-secondary-bg placeholder:duration-300 placeholder:translate-x-0 focus:placeholder:translate-x-1 placeholder:text-silent focus:outline-none focus:ring-1 focus:ring-primary focus:shadow focus:shadow-primary focus:border-primary-dark disabled:bg-secondary-light disabled:text-disabled ${height}`}
143
- placeholder={placeholder}
144
- required={rest.required}
145
- disabled={rest.disabled}
146
- value={tempFilter || ""}
147
- onChange={(e) => {
148
- setTempFilter(e.target.value);
149
- }}
150
- onClick={() => {
151
- setOpenDropdown(true);
152
- setTempValue("");
153
- setTempFilter("");
154
- }}
155
- />
156
- <div className="absolute right-4 text-xl text-disabled pointer-events-none">
157
- <Icon icon={`gravity-ui:chevron-${openDropdown ? "up" : "down"}`} />
158
- </div>
159
- </div>
160
- {openDropdown &&
161
- position &&
162
- createPortal(
163
- <div
164
- ref={dropdownRef}
165
- className="absolute max-h-40 p-1 z-50 pointer-events-auto bg-secondary-bg shadow border rounded-md overflow-auto"
166
- style={{
167
- top: position.top,
168
- left: position.left,
169
- width: dropdownWidth,
170
- }}
171
- >
172
- {menus.filter((item) =>
173
- item.label.toLowerCase().includes(tempFilter.toLowerCase())
174
- ).length === 0 ? (
175
- <div className="flex justify-center">
176
- <span className="px-4 py-1">No data found</span>
177
- </div>
178
- ) : (
179
- menus
180
- .filter((item) =>
181
- item.label.toLowerCase().includes(tempFilter.toLowerCase())
182
- )
183
- .map((item, index) => (
184
- <button
185
- key={index}
186
- type="button"
187
- className="min-w-40 w-full my-0.5 px-4 py-2 rounded-md text-sm text-left text-default hover:bg-secondary-light disabled:bg-primary-transparent"
188
- disabled={tempValue === item.value}
189
- onMouseDown={() => {
190
- setTempValue(item.value);
191
- if (rest.onChange) {
192
- rest.onChange({
193
- target: { value: item.value } as HTMLSelectElement,
194
- } as any);
195
- }
196
- setTempFilter(item.label);
197
- setOpenDropdown(false);
198
- }}
199
- >
200
- {item.label}
201
- </button>
202
- ))
203
- )}
204
- </div>,
205
- document.body
206
- )}
207
- </div>
208
- <select className="hidden" {...rest}>
209
- {menus.map((item, index) => (
210
- <option key={index} value={item.value}>
211
- {item.label}
212
- </option>
213
- ))}
214
- </select>
215
- </label>
216
- );
217
- };