@addsign/moje-agenda-shared-lib 1.0.61 → 2.0.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/dist/assets/style.css +4369 -0
- package/dist/assets/tailwind.css +1249 -1231
- package/dist/components/Button.js +1 -1
- package/dist/components/datatable/DataTable.js +1 -1
- package/dist/components/datatable/DataTableServer.js +1 -1
- package/dist/components/form/AutocompleteSearchBar.js +1 -1
- package/dist/components/form/AutocompleteSearchBarServer.js +1 -1
- package/dist/components/form/FileInput.js +1 -1
- package/dist/components/form/FileInputMultiple.js +1 -1
- package/dist/components/form/FormField.js +1 -1
- package/dist/components/form/PositionsSelectorSingle.js +1 -1
- package/dist/components/form/SelectField.js +1 -1
- package/dist/components/profiles/ProfileOverview.js +1 -1
- package/dist/main.js +1 -1
- package/dist/tailwind-l0sNRNKZ.js +2 -0
- package/dist/tailwind-l0sNRNKZ.js.map +1 -0
- package/lib/components/Button.tsx +57 -0
- package/lib/components/Calendar.tsx +242 -0
- package/lib/components/ConfirmationModalDialog.tsx +115 -0
- package/lib/components/Modal.tsx +73 -0
- package/lib/components/ModalDialog.tsx +63 -0
- package/lib/components/Spinner.tsx +25 -0
- package/lib/components/SpinnerIcon.tsx +12 -0
- package/lib/components/datatable/DataTable.tsx +442 -0
- package/lib/components/datatable/DataTableServer.tsx +939 -0
- package/lib/components/datatable/DatatableSettings.tsx +48 -0
- package/lib/components/datatable/Resizable.tsx +99 -0
- package/lib/components/datatable/types.ts +33 -0
- package/lib/components/form/AutocompleteSearchBar.tsx +424 -0
- package/lib/components/form/AutocompleteSearchBarServer.tsx +257 -0
- package/lib/components/form/DateField.tsx +124 -0
- package/lib/components/form/DateRangeField.tsx +116 -0
- package/lib/components/form/FileInput.tsx +188 -0
- package/lib/components/form/FileInputMultiple.tsx +186 -0
- package/lib/components/form/FormField.tsx +371 -0
- package/lib/components/form/InputField.tsx +230 -0
- package/lib/components/form/PositionsSelectorSingle.tsx +266 -0
- package/lib/components/form/RadioGroup.tsx +64 -0
- package/lib/components/form/SelectField.tsx +267 -0
- package/lib/components/layout/IconInCircle.tsx +29 -0
- package/lib/components/layout/PageTitle.tsx +19 -0
- package/lib/components/layout/SectionTitle.tsx +22 -0
- package/lib/components/profiles/ProfileOverview.tsx +212 -0
- package/lib/components/ui/Calendar.tsx +68 -0
- package/lib/components/ui/Combobox.tsx +122 -0
- package/lib/components/ui/DatePicker.tsx +124 -0
- package/lib/components/ui/DateTimePicker.tsx +187 -0
- package/lib/components/ui/Dialog.tsx +118 -0
- package/lib/components/ui/ScrollArea.tsx +45 -0
- package/lib/components/ui/button.tsx +56 -0
- package/lib/components/ui/command.tsx +153 -0
- package/lib/components/ui/form.tsx +177 -0
- package/lib/components/ui/input.tsx +22 -0
- package/lib/components/ui/label.tsx +24 -0
- package/lib/components/ui/popover.tsx +31 -0
- package/lib/components/ui/radioGroup.tsx +44 -0
- package/lib/components/ui/select.tsx +158 -0
- package/lib/contexts/FederationContext.tsx +28 -0
- package/lib/contexts/useFederationContext.ts +4 -0
- package/lib/css/tailwind.css +10 -0
- package/lib/fonts/arial.ts +3 -0
- package/lib/fonts/arialBold.ts +4 -0
- package/lib/main.ts +64 -0
- package/lib/types.ts +492 -0
- package/lib/utils/PdfManager.ts +224 -0
- package/lib/utils/getFullName.tsx +83 -0
- package/lib/utils/getIntersectingDays.ts +28 -0
- package/lib/utils/handleErrors.ts +28 -0
- package/lib/utils/hasRightInModule.ts +17 -0
- package/lib/utils/hasRole.ts +12 -0
- package/lib/utils/utils.ts +6 -0
- package/lib/vite-env.d.ts +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { MdRefresh } from "react-icons/md";
|
|
3
|
+
import { PiFileXlsLight } from "react-icons/pi";
|
|
4
|
+
import { TiArrowBackOutline } from "react-icons/ti";
|
|
5
|
+
|
|
6
|
+
export interface IDatatableSettingsProps {
|
|
7
|
+
tableId: string; // id tabulky
|
|
8
|
+
onSuccess: () => void;
|
|
9
|
+
onExport: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function DatatableSettings({
|
|
13
|
+
tableId,
|
|
14
|
+
onSuccess,
|
|
15
|
+
onExport,
|
|
16
|
+
}: IDatatableSettingsProps) {
|
|
17
|
+
const refreshTable = React.useCallback(() => {
|
|
18
|
+
const storageKey = `datatable:${tableId}`;
|
|
19
|
+
localStorage.setItem(storageKey, "{}");
|
|
20
|
+
onSuccess();
|
|
21
|
+
}, [tableId]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex items-center gap-5 py-3">
|
|
25
|
+
<div
|
|
26
|
+
className=" text-xl h-full cursor-pointer text-gray-600 hover:text-black"
|
|
27
|
+
title="Obnovit Data"
|
|
28
|
+
onClick={onSuccess}
|
|
29
|
+
>
|
|
30
|
+
<MdRefresh />
|
|
31
|
+
</div>
|
|
32
|
+
<div
|
|
33
|
+
className=" text-xl h-full cursor-pointer text-gray-600 hover:text-black"
|
|
34
|
+
title="Obnovit výchozí nastavení tabulky"
|
|
35
|
+
onClick={() => refreshTable()}
|
|
36
|
+
>
|
|
37
|
+
<TiArrowBackOutline />
|
|
38
|
+
</div>
|
|
39
|
+
<div
|
|
40
|
+
className=" text-xl h-full cursor-pointer text-gray-600 hover:text-black"
|
|
41
|
+
title="Stáhnout jako XLS soubor"
|
|
42
|
+
onClick={onExport}
|
|
43
|
+
>
|
|
44
|
+
<PiFileXlsLight />
|
|
45
|
+
</div>{" "}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface IResizableProps {
|
|
4
|
+
tableId: string;
|
|
5
|
+
colKey: string;
|
|
6
|
+
children: any;
|
|
7
|
+
defaultWidth: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Resizable = ({
|
|
11
|
+
tableId,
|
|
12
|
+
colKey,
|
|
13
|
+
defaultWidth,
|
|
14
|
+
children,
|
|
15
|
+
}: IResizableProps) => {
|
|
16
|
+
const [node, setNode] = React.useState<HTMLElement | null>(null);
|
|
17
|
+
|
|
18
|
+
const [width, setWidth] = useState(() => {
|
|
19
|
+
const storageKey = `datatable:${tableId}`;
|
|
20
|
+
const storedData = localStorage.getItem(storageKey);
|
|
21
|
+
const data = storedData ? JSON.parse(storedData) : {};
|
|
22
|
+
|
|
23
|
+
const columnWidths = data.columnWidths || {};
|
|
24
|
+
const storedWidth = columnWidths[colKey];
|
|
25
|
+
return storedWidth ? storedWidth : defaultWidth;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
//storing sizes
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const storageKey = `datatable:${tableId}`;
|
|
31
|
+
const storedData = localStorage.getItem(storageKey);
|
|
32
|
+
const data = storedData ? JSON.parse(storedData) : {};
|
|
33
|
+
|
|
34
|
+
data.columnWidths = data.columnWidths || {};
|
|
35
|
+
data.columnWidths[colKey] = width;
|
|
36
|
+
|
|
37
|
+
localStorage.setItem(storageKey, JSON.stringify(data));
|
|
38
|
+
}, [width, tableId, colKey, defaultWidth]);
|
|
39
|
+
|
|
40
|
+
const ref = React.useCallback((nodeEle: HTMLElement) => {
|
|
41
|
+
setNode(nodeEle);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const handleMouseDown = React.useCallback(
|
|
45
|
+
(e: MouseEvent) => {
|
|
46
|
+
if (!node) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parent = node.parentElement;
|
|
51
|
+
const startPos = {
|
|
52
|
+
x: e.clientX,
|
|
53
|
+
y: e.clientY,
|
|
54
|
+
};
|
|
55
|
+
// const styles = window.getComputedStyle(parent);
|
|
56
|
+
const styles = parent ? window.getComputedStyle(parent) : null;
|
|
57
|
+
|
|
58
|
+
const w = parseInt(styles?.width || "0", 10);
|
|
59
|
+
|
|
60
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
61
|
+
const dx = e.clientX - startPos.x;
|
|
62
|
+
const newWidth = w + dx + "px";
|
|
63
|
+
|
|
64
|
+
parent?.style.setProperty("width", newWidth);
|
|
65
|
+
setWidth(newWidth);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleMouseUp = () => {
|
|
69
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
70
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
74
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
75
|
+
},
|
|
76
|
+
[node]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
if (!node) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const parent = node.parentElement;
|
|
85
|
+
if (node.parentNode !== node.parentNode?.parentNode?.lastChild) {
|
|
86
|
+
parent?.style.setProperty("width", `${width}`);
|
|
87
|
+
} else {
|
|
88
|
+
parent?.style.setProperty("width", `auto`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
node.addEventListener("mousedown", handleMouseDown);
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
node.removeEventListener("mousedown", handleMouseDown);
|
|
95
|
+
};
|
|
96
|
+
}, [node, handleMouseDown, tableId]);
|
|
97
|
+
|
|
98
|
+
return children({ ref });
|
|
99
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { IOptionItem } from "../../types";
|
|
2
|
+
|
|
3
|
+
export interface DataTableColumn<T> {
|
|
4
|
+
key: keyof T | "actions";
|
|
5
|
+
header: string;
|
|
6
|
+
render?: (item: T) => React.ReactNode;
|
|
7
|
+
actions?: DataTableAction<T>[];
|
|
8
|
+
filterType?: string;
|
|
9
|
+
classes?: string;
|
|
10
|
+
type?: "string" | "number" | "date";
|
|
11
|
+
filterOperator?: ">=" | "<=" | "==";
|
|
12
|
+
filterSource?: string;
|
|
13
|
+
filterValueKey?: string;
|
|
14
|
+
filterLabelKey?: string;
|
|
15
|
+
filterParam?: string;
|
|
16
|
+
filterParam2?: string;
|
|
17
|
+
filterOptions?: IOptionItem[];
|
|
18
|
+
sortParam?: string;
|
|
19
|
+
sumarize?: boolean;
|
|
20
|
+
width?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface DataTableAction<T> {
|
|
23
|
+
label: string;
|
|
24
|
+
rowAction?: boolean,
|
|
25
|
+
onClick: (item: T) => void;
|
|
26
|
+
visible?: (item: T) => boolean;
|
|
27
|
+
icon?: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type DataTableInternalItems = {
|
|
31
|
+
_isHighlighted?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { useClickAway } from "react-use";
|
|
3
|
+
import { IOptionItem, useFederationContext } from "../../main";
|
|
4
|
+
import { MdClose, MdExpandLess, MdExpandMore } from "react-icons/md";
|
|
5
|
+
|
|
6
|
+
interface OptionListProps {
|
|
7
|
+
options: IOptionItem[];
|
|
8
|
+
selectedOptionIndex: number;
|
|
9
|
+
handleOptionClick: (option: IOptionItem) => void;
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SearchInputProps {
|
|
14
|
+
value: string;
|
|
15
|
+
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
16
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
17
|
+
onFocus: () => void;
|
|
18
|
+
clearQuery: () => void;
|
|
19
|
+
inputRef: React.RefObject<HTMLInputElement>;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
isFocused?: boolean;
|
|
22
|
+
onBlur: () => void;
|
|
23
|
+
isLoading?: boolean;
|
|
24
|
+
clearable?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const AutocompleteSearchBar: React.FC<{
|
|
28
|
+
label: string;
|
|
29
|
+
name: string;
|
|
30
|
+
required?: boolean;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
description?: string;
|
|
33
|
+
fetchUrl?: string;
|
|
34
|
+
valueKey: string;
|
|
35
|
+
labelKey: string;
|
|
36
|
+
onChange: (value: any) => void;
|
|
37
|
+
onFocus: () => void;
|
|
38
|
+
onBlur: () => void;
|
|
39
|
+
placeholder?: string;
|
|
40
|
+
serverSearch?: boolean;
|
|
41
|
+
value?: string;
|
|
42
|
+
initOptions?: IOptionItem[];
|
|
43
|
+
showId?: boolean;
|
|
44
|
+
clearable?: boolean;
|
|
45
|
+
rounded?: boolean;
|
|
46
|
+
}> = ({
|
|
47
|
+
label,
|
|
48
|
+
name,
|
|
49
|
+
required,
|
|
50
|
+
disabled,
|
|
51
|
+
description,
|
|
52
|
+
fetchUrl,
|
|
53
|
+
valueKey,
|
|
54
|
+
labelKey,
|
|
55
|
+
onChange,
|
|
56
|
+
onFocus,
|
|
57
|
+
onBlur,
|
|
58
|
+
placeholder,
|
|
59
|
+
serverSearch = false,
|
|
60
|
+
value,
|
|
61
|
+
initOptions,
|
|
62
|
+
showId,
|
|
63
|
+
clearable = true,
|
|
64
|
+
rounded = true,
|
|
65
|
+
}) => {
|
|
66
|
+
const [query, setQuery] = useState("");
|
|
67
|
+
const [loading, setLoading] = useState(false);
|
|
68
|
+
const [options, setOptions] = useState<IOptionItem[]>([]);
|
|
69
|
+
const [selectedOptionIndex, setSelectedOptionIndex] = useState(-1);
|
|
70
|
+
const [selectedOption, setSelectedOption] = useState<IOptionItem | null>(
|
|
71
|
+
null
|
|
72
|
+
);
|
|
73
|
+
const [searchResults, setSearchResults] = useState<IOptionItem[]>([]);
|
|
74
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
75
|
+
const ref = useRef(null);
|
|
76
|
+
const federationContext = useFederationContext();
|
|
77
|
+
|
|
78
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
79
|
+
|
|
80
|
+
const [errors] = useState<any>({});
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const fetchOptions = async () => {
|
|
84
|
+
setLoading(true);
|
|
85
|
+
|
|
86
|
+
const { data } = await federationContext.apiClient.get(fetchUrl || "");
|
|
87
|
+
// osetreni obou variant navratu z BE
|
|
88
|
+
const tmpData = data.content || data;
|
|
89
|
+
|
|
90
|
+
setOptions(
|
|
91
|
+
tmpData.map((item: any) => ({
|
|
92
|
+
value: item[valueKey],
|
|
93
|
+
label: item[labelKey] + (showId ? " (" + item[valueKey] + ")" : ""),
|
|
94
|
+
}))
|
|
95
|
+
); // Adjust map according to actual API response
|
|
96
|
+
setLoading(false);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (fetchUrl) fetchOptions();
|
|
100
|
+
}, [fetchUrl, labelKey, showId, valueKey, federationContext.apiClient]);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (initOptions) setOptions(initOptions);
|
|
104
|
+
}, [initOptions]);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (value && options) {
|
|
108
|
+
const initOption = options.find((it) => it.value == value);
|
|
109
|
+
|
|
110
|
+
if (initOption) {
|
|
111
|
+
setQuery(initOption.label);
|
|
112
|
+
setSelectedOption(initOption);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}, [value, options]);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!value) {
|
|
119
|
+
setQuery("");
|
|
120
|
+
setSelectedOption(null);
|
|
121
|
+
}
|
|
122
|
+
}, [value]);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const fetchOptions = async () => {
|
|
126
|
+
setLoading(true);
|
|
127
|
+
const { data } = await federationContext.apiClient.get(fetchUrl + query);
|
|
128
|
+
setOptions(
|
|
129
|
+
data.map((item: any) => ({
|
|
130
|
+
value: item[valueKey],
|
|
131
|
+
label: item[labelKey] + (showId ? " (" + item[valueKey] + ")" : ""),
|
|
132
|
+
}))
|
|
133
|
+
); // Adjust map according to actual API response
|
|
134
|
+
setSearchResults(
|
|
135
|
+
data.map((item: any) => ({
|
|
136
|
+
value: item[valueKey],
|
|
137
|
+
label: item[labelKey] + (showId ? " (" + item[valueKey] + ")" : ""),
|
|
138
|
+
}))
|
|
139
|
+
); // Adjust map according to actual API response
|
|
140
|
+
|
|
141
|
+
setLoading(false);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (serverSearch) fetchOptions();
|
|
145
|
+
}, [
|
|
146
|
+
query,
|
|
147
|
+
serverSearch,
|
|
148
|
+
valueKey,
|
|
149
|
+
labelKey,
|
|
150
|
+
fetchUrl,
|
|
151
|
+
federationContext.apiClient,
|
|
152
|
+
showId,
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
useClickAway(ref, () => {
|
|
156
|
+
if (isFocused) {
|
|
157
|
+
setSearchResults([]);
|
|
158
|
+
if (!selectedOption) {
|
|
159
|
+
setQuery("");
|
|
160
|
+
onChange(null);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
handleBlur();
|
|
164
|
+
onBlur();
|
|
165
|
+
setIsFocused(false);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const handleQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
170
|
+
setQuery(event.target.value);
|
|
171
|
+
setSelectedOption(null);
|
|
172
|
+
setSelectedOptionIndex(-1);
|
|
173
|
+
setSearchResults(
|
|
174
|
+
options.filter((option) =>
|
|
175
|
+
option?.label
|
|
176
|
+
?.toLowerCase()
|
|
177
|
+
.includes(event.target?.value?.toLowerCase())
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
const handleFocus = () => {
|
|
182
|
+
setIsFocused(true);
|
|
183
|
+
onFocus();
|
|
184
|
+
setSearchResults(
|
|
185
|
+
options /*.filter((option) =>
|
|
186
|
+
option.label.toLowerCase().includes(query.toLowerCase()),
|
|
187
|
+
),*/
|
|
188
|
+
);
|
|
189
|
+
inputRef.current?.select(); // Add this line
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const handleClearQuery = () => {
|
|
193
|
+
setQuery("");
|
|
194
|
+
onChange(null);
|
|
195
|
+
setSelectedOption(null);
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
setSelectedOptionIndex(-1);
|
|
198
|
+
setSearchResults(options);
|
|
199
|
+
}, 1000);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
203
|
+
if (event.key === "ArrowUp" && searchResults.length > 0) {
|
|
204
|
+
setSelectedOptionIndex((prevIndex) =>
|
|
205
|
+
prevIndex > 0 ? prevIndex - 1 : searchResults.length - 1
|
|
206
|
+
);
|
|
207
|
+
} else if (event.key === "ArrowDown" && searchResults.length > 0) {
|
|
208
|
+
setSelectedOptionIndex((prevIndex) =>
|
|
209
|
+
prevIndex < searchResults.length - 1 ? prevIndex + 1 : 0
|
|
210
|
+
);
|
|
211
|
+
} else if (event.key === "Enter" && selectedOptionIndex !== -1) {
|
|
212
|
+
handleOptionClick(searchResults[selectedOptionIndex]);
|
|
213
|
+
} else if (event.key === "Escape" && selectedOptionIndex !== -1) {
|
|
214
|
+
handleBlur();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleOptionClick = (option: IOptionItem) => {
|
|
219
|
+
setQuery(option.label);
|
|
220
|
+
setSelectedOption(option);
|
|
221
|
+
setSelectedOptionIndex(-1);
|
|
222
|
+
|
|
223
|
+
onChange(option.value);
|
|
224
|
+
|
|
225
|
+
handleBlur();
|
|
226
|
+
};
|
|
227
|
+
const handleBlur = () => {
|
|
228
|
+
setSearchResults([]);
|
|
229
|
+
onBlur();
|
|
230
|
+
setIsFocused(false);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div className="w-full flex-col justify-start items-start gap-1.5 inline-flex">
|
|
235
|
+
<div
|
|
236
|
+
className="self-stretch flex-col justify-start items-start gap-1.5 flex relative"
|
|
237
|
+
ref={ref}
|
|
238
|
+
>
|
|
239
|
+
{label && (
|
|
240
|
+
<label
|
|
241
|
+
className="text-slate-700 text-sm leading-tight font-medium"
|
|
242
|
+
htmlFor={name}
|
|
243
|
+
>
|
|
244
|
+
{label} {required ? "*" : ""}
|
|
245
|
+
</label>
|
|
246
|
+
)}
|
|
247
|
+
<div
|
|
248
|
+
className={`self-stretch px-3 py-1.5 justify-start items-center gap-2 inline-flex outline-none border bg-white
|
|
249
|
+
${
|
|
250
|
+
isFocused
|
|
251
|
+
? "outline-4 outline-indigo-200 outline-offset-0 border-indigo-300"
|
|
252
|
+
: ""
|
|
253
|
+
}
|
|
254
|
+
${
|
|
255
|
+
isFocused && errors[name]?.message
|
|
256
|
+
? "outline-4 outline-red-200 outline-offset-0 border-none"
|
|
257
|
+
: ""
|
|
258
|
+
}
|
|
259
|
+
${!isFocused && errors[name]?.message ? "border-red-200" : ""}
|
|
260
|
+
${disabled ? "bg-gray-100" : "bg-transparent"}
|
|
261
|
+
${rounded ? " rounded-lg " : " rounded-none "}`}
|
|
262
|
+
>
|
|
263
|
+
<div className="Content grow shrink basis-0 min-h-5 lg:min-h-7 justify-start items-start gap-2 flex relative flex-col w-full mx-auto">
|
|
264
|
+
<SearchInput
|
|
265
|
+
value={query}
|
|
266
|
+
onChange={handleQueryChange}
|
|
267
|
+
onKeyDown={handleKeyDown}
|
|
268
|
+
onFocus={handleFocus} // Pass the onFocus handler
|
|
269
|
+
clearQuery={handleClearQuery} // Pass the onFocus handler
|
|
270
|
+
inputRef={inputRef}
|
|
271
|
+
placeholder={placeholder}
|
|
272
|
+
isFocused={isFocused}
|
|
273
|
+
onBlur={handleBlur}
|
|
274
|
+
isLoading={loading}
|
|
275
|
+
clearable={clearable}
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
{searchResults.length > 0 && isFocused && (
|
|
280
|
+
<OptionList
|
|
281
|
+
options={searchResults}
|
|
282
|
+
selectedOptionIndex={selectedOptionIndex}
|
|
283
|
+
handleOptionClick={handleOptionClick}
|
|
284
|
+
label={label}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
{description && (
|
|
289
|
+
<div className="HintText self-stretch text-slate-600 text-sm font-normal leading-tight">
|
|
290
|
+
{description}
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const OptionList: React.FC<OptionListProps> = ({
|
|
298
|
+
options,
|
|
299
|
+
selectedOptionIndex,
|
|
300
|
+
handleOptionClick,
|
|
301
|
+
label,
|
|
302
|
+
}) => {
|
|
303
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
// Ensure the selected option is scrolled into view
|
|
307
|
+
if (selectedOptionIndex >= 0 && selectedOptionIndex < options.length) {
|
|
308
|
+
const selectedElement = containerRef.current?.children[
|
|
309
|
+
selectedOptionIndex
|
|
310
|
+
] as HTMLDivElement;
|
|
311
|
+
selectedElement?.scrollIntoView({
|
|
312
|
+
behavior: "instant",
|
|
313
|
+
block: "nearest",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}, [selectedOptionIndex, options.length]);
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div
|
|
320
|
+
ref={containerRef}
|
|
321
|
+
className={`overflow-y-auto resultOptionContainer w-full max-h-96 outline-indigo-200 absolute outline-4 bg-white border border-gray-200 drop-shadow-xl z-50 ${label ? "top-[68px]" : "top-[40px]"} *:
|
|
322
|
+
|
|
323
|
+
`}
|
|
324
|
+
>
|
|
325
|
+
{options?.map((option, index) => (
|
|
326
|
+
<div
|
|
327
|
+
key={option.value as string}
|
|
328
|
+
className={`py-2 px-4 flex items-center justify-between gap-8 hover:bg-gray-200 cursor-pointer text-sm ${
|
|
329
|
+
selectedOptionIndex === index ? "bg-gray-200" : ""
|
|
330
|
+
}`}
|
|
331
|
+
onClick={() => handleOptionClick(option)}
|
|
332
|
+
>
|
|
333
|
+
<p className="font-normal">{option.label}</p>
|
|
334
|
+
</div>
|
|
335
|
+
))}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const SearchInput: React.FC<SearchInputProps> = ({
|
|
341
|
+
value,
|
|
342
|
+
onChange,
|
|
343
|
+
onKeyDown,
|
|
344
|
+
onFocus,
|
|
345
|
+
onBlur,
|
|
346
|
+
inputRef,
|
|
347
|
+
placeholder,
|
|
348
|
+
clearQuery,
|
|
349
|
+
isFocused,
|
|
350
|
+
isLoading,
|
|
351
|
+
clearable = true,
|
|
352
|
+
}) => {
|
|
353
|
+
const handleExpandMoreClick = () => {
|
|
354
|
+
inputRef.current?.focus();
|
|
355
|
+
onFocus();
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const handleClearQuery = () => {
|
|
359
|
+
clearQuery();
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<div className="relative flex items-center w-full sharedResource text-sm bg-white">
|
|
364
|
+
<input
|
|
365
|
+
type="text"
|
|
366
|
+
className={`pl-1 py-1 border-gray-800 bg-white w-full ${
|
|
367
|
+
isLoading ? "pr-10 " : "pr-20"
|
|
368
|
+
}`}
|
|
369
|
+
value={value}
|
|
370
|
+
onChange={onChange}
|
|
371
|
+
onKeyDown={onKeyDown}
|
|
372
|
+
onFocus={onFocus}
|
|
373
|
+
ref={inputRef}
|
|
374
|
+
placeholder={placeholder}
|
|
375
|
+
disabled={isLoading}
|
|
376
|
+
/>
|
|
377
|
+
{isLoading && (
|
|
378
|
+
<div className="loader absolute right-0 text-gray-500">
|
|
379
|
+
{/* You can replace this div with an SVG or another icon of your choice */}
|
|
380
|
+
<svg
|
|
381
|
+
className="animate-spin h-5 w-5"
|
|
382
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
383
|
+
fill="none"
|
|
384
|
+
viewBox="0 0 24 24"
|
|
385
|
+
>
|
|
386
|
+
<circle
|
|
387
|
+
className="opacity-25"
|
|
388
|
+
cx="12"
|
|
389
|
+
cy="12"
|
|
390
|
+
r="10"
|
|
391
|
+
stroke="currentColor"
|
|
392
|
+
strokeWidth="4"
|
|
393
|
+
/>
|
|
394
|
+
<path
|
|
395
|
+
className="opacity-75"
|
|
396
|
+
fill="currentColor"
|
|
397
|
+
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
|
398
|
+
/>
|
|
399
|
+
</svg>
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
{isFocused && !isLoading && (
|
|
403
|
+
<MdExpandLess
|
|
404
|
+
className="material-icons absolute right-0 cursor-pointer text-gray-900 lext-lg"
|
|
405
|
+
onClick={onBlur}
|
|
406
|
+
/>
|
|
407
|
+
)}
|
|
408
|
+
{!isFocused && !isLoading && (
|
|
409
|
+
<MdExpandMore
|
|
410
|
+
className="material-icons absolute right-0 cursor-pointer text-gray-900 lext-lg"
|
|
411
|
+
onClick={handleExpandMoreClick}
|
|
412
|
+
/>
|
|
413
|
+
)}
|
|
414
|
+
{value && clearable && !isLoading && (
|
|
415
|
+
<MdClose
|
|
416
|
+
className="material-icons absolute right-6 cursor-pointer text-gray-900 lext-lg"
|
|
417
|
+
onClick={handleClearQuery}
|
|
418
|
+
/>
|
|
419
|
+
)}
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
export default AutocompleteSearchBar;
|