next-helios-fe 1.9.17 → 1.10.0

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.17",
3
+ "version": "1.10.0",
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,399 @@
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
+ console.log(selectedMenuHistory);
74
+ }, [selectedMenuHistory]);
75
+
76
+ useEffect(() => {
77
+ if (value) {
78
+ setTempValue(value);
79
+ }
80
+ }, [value]);
81
+
82
+ useEffect(() => {
83
+ if (!loading) {
84
+ if (openModal) {
85
+ const container = optionContainerRef.current;
86
+
87
+ if (container) {
88
+ const handleScroll = () => {
89
+ if (
90
+ container.scrollTop + container.offsetHeight - 3 >=
91
+ container.scrollHeight
92
+ ) {
93
+ if (
94
+ dynamicSelect?.setValue?.totalData &&
95
+ data.menus.length !== dynamicSelect?.setValue?.totalData
96
+ ) {
97
+ setMaxData((prev: any) => prev + 20);
98
+ if (dynamicSelect?.getValue) {
99
+ dynamicSelect?.getValue({
100
+ filter: [{ key: searchBy, value: delayedSearch }],
101
+ maxRow: maxData + 20,
102
+ });
103
+ }
104
+ }
105
+ }
106
+ };
107
+
108
+ container.addEventListener("scroll", handleScroll);
109
+
110
+ return () => {
111
+ container.removeEventListener("scroll", handleScroll);
112
+ };
113
+ }
114
+ }
115
+ }
116
+ }, [
117
+ optionContainerRef,
118
+ loading,
119
+ data.menus,
120
+ openModal,
121
+ searchBy,
122
+ delayedSearch,
123
+ maxData,
124
+ dynamicSelect?.setValue?.totalData,
125
+ ]);
126
+
127
+ useEffect(() => {
128
+ if (dynamicSelect?.getValue) {
129
+ dynamicSelect.getValue({
130
+ filter: [{ key: searchBy, value: delayedSearch }],
131
+ maxRow: maxData,
132
+ });
133
+ }
134
+ }, [delayedSearch]);
135
+
136
+ const debouncedUpdate = useDebouncedCallback((value: string) => {
137
+ setDelayedSearch(value);
138
+ }, 1250);
139
+
140
+ return (
141
+ <>
142
+ <div
143
+ className={`${options?.width === "fit" ? "w-fit" : "w-full"}`}
144
+ onClick={() => {
145
+ if (!disabled) {
146
+ setOpenModal(true);
147
+ }
148
+ }}
149
+ >
150
+ {type === "select" ? (
151
+ <Form.Select
152
+ menus={
153
+ dynamicSelect?.getValue
154
+ ? [
155
+ ...selectedMenuHistory,
156
+ ...data.menus.filter(
157
+ (prev: any) =>
158
+ !selectedMenuHistory
159
+ .map((historyPrev: any) => historyPrev.value)
160
+ .includes(prev.value)
161
+ ),
162
+ ]
163
+ : data.menus
164
+ }
165
+ label={label}
166
+ placeholder={placeholder}
167
+ description={description}
168
+ options={options}
169
+ disabled={disabled}
170
+ required={required}
171
+ value={tempValue || ""}
172
+ readOnly
173
+ />
174
+ ) : (
175
+ <Form.MultipleSelect
176
+ menus={
177
+ dynamicSelect?.getValue
178
+ ? [
179
+ ...selectedMenuHistory,
180
+ ...data.menus.filter(
181
+ (prev: any) =>
182
+ !selectedMenuHistory
183
+ .map((historyPrev: any) => historyPrev.value)
184
+ .includes(prev.value)
185
+ ),
186
+ ]
187
+ : data.menus
188
+ }
189
+ label={label}
190
+ placeholder={placeholder}
191
+ description={description}
192
+ options={options}
193
+ disabled={disabled}
194
+ required={required}
195
+ value={tempValue || []}
196
+ readOnly
197
+ />
198
+ )}
199
+ </div>
200
+ <Modal
201
+ title={label || ""}
202
+ open={openModal}
203
+ onClose={() => {
204
+ setSearch("");
205
+ setOpenModal(false);
206
+ if (dynamicSelect?.getValue) {
207
+ setDelayedSearch("");
208
+ }
209
+ }}
210
+ >
211
+ <div className="flex flex-col gap-4 w-full h-full overflow-hidden">
212
+ <div className="flex items-end gap-4">
213
+ {data.searchableColumns && (
214
+ <div className="w-36">
215
+ <Form.Select
216
+ menus={data.searchableColumns || []}
217
+ placeholder="Search By"
218
+ value={searchBy}
219
+ onChange={(e) => {
220
+ setSearchBy(e.target.value);
221
+ setSearch("");
222
+ setDelayedSearch("");
223
+ if (dynamicSelect?.getValue) {
224
+ dynamicSelect.getValue({
225
+ filter: [{ key: e.target.value, value: "" }],
226
+ maxRow: maxData,
227
+ });
228
+ }
229
+ }}
230
+ />
231
+ </div>
232
+ )}
233
+ <div className="flex-1">
234
+ <Form.Search
235
+ placeholder="Search..."
236
+ disabled={data.searchableColumns && !searchBy}
237
+ value={search || ""}
238
+ onChange={(e) => {
239
+ setSearch(e.target.value);
240
+ debouncedUpdate(e.target.value);
241
+ }}
242
+ />
243
+ </div>
244
+ </div>
245
+ <div
246
+ ref={optionContainerRef}
247
+ className="relative flex-1 flex flex-col border rounded-md overflow-auto"
248
+ >
249
+ {(dynamicSelect?.getValue
250
+ ? delayedSearch
251
+ ? data.menus
252
+ : [
253
+ ...selectedMenuHistory,
254
+ ...data.menus.filter(
255
+ (prev: any) =>
256
+ !selectedMenuHistory
257
+ .map((historyPrev: any) => historyPrev.value)
258
+ .includes(prev.value)
259
+ ),
260
+ ]
261
+ : data.menus.filter((optionItem: any) =>
262
+ optionItem.label
263
+ .toLowerCase()
264
+ .includes(search.toLocaleLowerCase())
265
+ )
266
+ )?.length === 0 ? (
267
+ <div className="flex-1 flex justify-center items-center">
268
+ <span>No data found</span>
269
+ </div>
270
+ ) : (
271
+ (dynamicSelect?.getValue
272
+ ? delayedSearch
273
+ ? data.menus
274
+ : [
275
+ ...selectedMenuHistory,
276
+ ...data.menus.filter(
277
+ (prev: any) =>
278
+ !selectedMenuHistory
279
+ .map((historyPrev: any) => historyPrev.value)
280
+ .includes(prev.value)
281
+ ),
282
+ ]
283
+ : data.menus.filter((optionItem: any) =>
284
+ optionItem.label
285
+ .toLowerCase()
286
+ .includes(search.toLocaleLowerCase())
287
+ )
288
+ ).map((optionItem: any, index: number) => {
289
+ return (
290
+ <Button
291
+ key={index}
292
+ type="button"
293
+ className="disabled:bg-primary-transparent"
294
+ disabled={
295
+ type === "select" && tempValue === optionItem.value
296
+ }
297
+ onClick={() => {
298
+ if (type === "select") {
299
+ setTempValue(optionItem.value);
300
+ setOpenModal(false);
301
+ setSearch("");
302
+ if (dynamicSelect?.getValue) {
303
+ setSelectedMenuHistory([optionItem]);
304
+ setDelayedSearch("");
305
+ }
306
+ if (onChange) {
307
+ onChange({
308
+ target: {
309
+ value: optionItem.value,
310
+ } as any,
311
+ } as any);
312
+ }
313
+ } else {
314
+ if (tempValue?.includes(optionItem.value)) {
315
+ setTempValue((prev: any) =>
316
+ prev.filter(
317
+ (prevItem: any) => prevItem !== optionItem.value
318
+ )
319
+ );
320
+ if (dynamicSelect?.getValue) {
321
+ setSelectedMenuHistory((prev: any) =>
322
+ prev.filter(
323
+ (prevItem: any) =>
324
+ prevItem.value !== optionItem.value
325
+ )
326
+ );
327
+ }
328
+ if (onChange) {
329
+ onChange({
330
+ target: {
331
+ value: tempValue.filter(
332
+ (prevItem: any) =>
333
+ prevItem !== optionItem.value
334
+ ),
335
+ } as any,
336
+ } as any);
337
+ }
338
+ } else {
339
+ if (!max || tempValue.length < max) {
340
+ setTempValue((prev: any) => [
341
+ ...prev,
342
+ optionItem.value,
343
+ ]);
344
+ if (dynamicSelect?.getValue) {
345
+ setSelectedMenuHistory((prev: any) => [
346
+ ...prev,
347
+ optionItem,
348
+ ]);
349
+ }
350
+ if (onChange) {
351
+ onChange({
352
+ target: {
353
+ value: [...tempValue, optionItem.value],
354
+ } as any,
355
+ } as any);
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }}
361
+ >
362
+ {type === "select" ? (
363
+ <div className="flex flex-col">
364
+ <span className="text-sm">{optionItem.label}</span>
365
+ <span className="text-xs">{optionItem.subLabel}</span>
366
+ </div>
367
+ ) : (
368
+ <div className="flex gap-4 pointer-events-none">
369
+ <Form.Checkbox
370
+ options={{ disableHover: true }}
371
+ checked={
372
+ tempValue?.includes(optionItem.value) ?? false
373
+ }
374
+ readOnly
375
+ />
376
+ <div className="flex flex-col">
377
+ <span className="text-sm">{optionItem.label}</span>
378
+ <span className="text-xs">{optionItem.subLabel}</span>
379
+ </div>
380
+ </div>
381
+ )}
382
+ </Button>
383
+ );
384
+ })
385
+ )}
386
+ {loading && (
387
+ <div className="absolute left-0 top-0 flex justify-center items-center w-full h-full backdrop-blur-sm">
388
+ <Icon
389
+ icon="mingcute:loading-fill"
390
+ className="text-xl text-primary animate-spin"
391
+ />
392
+ </div>
393
+ )}
394
+ </div>
395
+ </div>
396
+ </Modal>
397
+ </>
398
+ );
399
+ };
@@ -178,14 +178,15 @@ export const MultipleSelect: React.FC<MultipleSelectProps> = ({
178
178
  disabled={disabled}
179
179
  value={tempValue.join(", ")}
180
180
  onChange={(e) => {}}
181
- readOnly={readOnly ?? false}
182
181
  onClick={(e) => {
183
182
  e.preventDefault();
184
- setOpenDropdown(true);
185
183
  dropdownTriggerRef.current?.click();
186
184
  setDropdownWidth(
187
185
  inputRef?.current?.getBoundingClientRect()?.width || 0
188
186
  );
187
+ if (!readOnly) {
188
+ setOpenDropdown(true);
189
+ }
189
190
  }}
190
191
  onKeyDown={(e) => {
191
192
  if (e.key === "Enter") {
@@ -114,15 +114,16 @@ export const Select: React.FC<SelectProps> = ({
114
114
  ? menus.find((item) => item.value === tempValue)?.label
115
115
  : ""
116
116
  }
117
- readOnly={readOnly ?? false}
118
117
  onChange={(e) => {}}
119
118
  onClick={(e) => {
120
119
  e.preventDefault();
121
- setOpenDropdown(true);
122
120
  dropdownTriggerRef.current?.click();
123
121
  setDropdownWidth(
124
122
  inputRef?.current?.getBoundingClientRect()?.width || 0
125
123
  );
124
+ if (!readOnly) {
125
+ setOpenDropdown(true);
126
+ }
126
127
  }}
127
128
  onKeyDown={(e) => {
128
129
  if (e.key === "Enter") {
@@ -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
- };