@engagebay/engagebay-form-module 1.0.0-beta.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/README.md +126 -0
- package/link.sh +2 -0
- package/package.json +30 -0
- package/src/api/index.ts +25 -0
- package/src/form/Form.tsx +157 -0
- package/src/form/FormField.tsx +80 -0
- package/src/form/FormFieldUtils.ts +241 -0
- package/src/form/FormFields.tsx +41 -0
- package/src/form/context/FormContext.tsx +66 -0
- package/src/form/formfields/ArrayField.tsx +169 -0
- package/src/form/formfields/BusinessHoursField.tsx +204 -0
- package/src/form/formfields/CheckboxButtonsField.tsx +97 -0
- package/src/form/formfields/CheckboxField.tsx +118 -0
- package/src/form/formfields/ColorPickerField.tsx +59 -0
- package/src/form/formfields/ComboMultiSelect.tsx +290 -0
- package/src/form/formfields/ComboSelect.tsx +278 -0
- package/src/form/formfields/DatePickerField.tsx +89 -0
- package/src/form/formfields/DateRangePickerField.tsx +104 -0
- package/src/form/formfields/DynamicMultiSelect.tsx +189 -0
- package/src/form/formfields/DynamicSelect.tsx +187 -0
- package/src/form/formfields/Error.tsx +15 -0
- package/src/form/formfields/ErrorContextHandler.tsx +77 -0
- package/src/form/formfields/FileUploadField.tsx +196 -0
- package/src/form/formfields/IframeField.tsx +65 -0
- package/src/form/formfields/InputField.tsx +67 -0
- package/src/form/formfields/InputGroupField.tsx +44 -0
- package/src/form/formfields/MultipleSelectField.tsx +98 -0
- package/src/form/formfields/NumberField.tsx +61 -0
- package/src/form/formfields/PasswordField.tsx +93 -0
- package/src/form/formfields/PhoneNumberField.tsx +163 -0
- package/src/form/formfields/RadioField.tsx +104 -0
- package/src/form/formfields/RadioGroupComponent.tsx +94 -0
- package/src/form/formfields/RangeField.tsx +53 -0
- package/src/form/formfields/SelectField.tsx +82 -0
- package/src/form/formfields/SwitchField.tsx +131 -0
- package/src/form/formfields/TextAreaField.tsx +48 -0
- package/src/form/formfields/TimeField.tsx +53 -0
- package/src/form/formfields/Typeahead.tsx +211 -0
- package/src/form/formfields/TypeaheadMultiSelect.tsx +203 -0
- package/src/form/formfields/UrlField.tsx +53 -0
- package/src/form/hooks/useDynamicReducer.tsx +42 -0
- package/src/form/schema/CustomValidators.ts +63 -0
- package/src/form/schema/FormFieldSchema.ts +342 -0
- package/src/form/util/RenderFormField.tsx +149 -0
- package/src/form/util/RenderListOptions.tsx +424 -0
- package/src/form/util/index.ts +185 -0
- package/src/util/LoaderWithText.tsx +28 -0
- package/src/util/svg/HELPER_ICONS.ts +16 -0
- package/src/util/svg/SVGIcon.tsx +23 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Description, Field, Label } from "@headlessui/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { FieldAlignType, FormFieldSchema } from "../schema/FormFieldSchema";
|
|
4
|
+
import ErrorContextHandler from "../formfields/ErrorContextHandler";
|
|
5
|
+
import SVGIcon from "../../util/svg/SVGIcon";
|
|
6
|
+
/**
|
|
7
|
+
* RenderFormField Component
|
|
8
|
+
*
|
|
9
|
+
* This component renders a form field based on the configuration provided in `fieldConfig`.
|
|
10
|
+
* It supports different layouts (horizontal or vertical) and can be customized with optional
|
|
11
|
+
* classes, labels, and error handling.
|
|
12
|
+
*
|
|
13
|
+
* @param {Object} props - The properties for the RenderFormField component.
|
|
14
|
+
* @param {FormFieldSchema} props.fieldConfig - The configuration object for the form field. This object can include:
|
|
15
|
+
* @param {string} [props.fieldConfig.name] - The name of the form field.
|
|
16
|
+
* @param {string} [props.fieldConfig.label] - The label text for the form field.
|
|
17
|
+
* @param {boolean} [props.fieldConfig.required] - Whether the field is required.
|
|
18
|
+
* @param {boolean} [props.fieldConfig.disabled] - Whether the field is disabled.
|
|
19
|
+
* @param {string} [props.fieldConfig.align] - The alignment type of the form field. Can be `FieldAlignType.HORIZONTAL` or `FieldAlignType.VERTICAL`.
|
|
20
|
+
* @param {string} [props.fieldConfig.helpText] - The help text for the form field.
|
|
21
|
+
* @param {Object} [props.fieldConfig.customClassNames] - An object containing custom class names for different parts of the field:
|
|
22
|
+
* @param {string} [props.fieldConfig.customClassNames.wrapperClassName] - Custom class for the wrapper element.
|
|
23
|
+
* @param {string} [props.fieldConfig.customClassNames.labelClassName] - Custom class for the label element.
|
|
24
|
+
* @param {boolean} [props.fieldConfig.disableDefaultWrapper] - If true, the default wrapper will be disabled.
|
|
25
|
+
* @param {React.ElementType} [props.fieldConfig.wrapper] - Custom wrapper component if `disableDefaultWrapper` is true.
|
|
26
|
+
* @param {boolean} [props.fieldConfig.submitOnChange] - If true, the form will submit on field change.
|
|
27
|
+
*
|
|
28
|
+
* @param {Function} props.getInput - A function that returns the input element to be rendered.
|
|
29
|
+
*
|
|
30
|
+
* @returns {JSX.Element} The rendered form field component.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Example usage:
|
|
34
|
+
* const fieldConfig = {
|
|
35
|
+
* name: "username",
|
|
36
|
+
* label: "Username",
|
|
37
|
+
* required: true,
|
|
38
|
+
* align: FieldAlignType.HORIZONTAL,
|
|
39
|
+
* customClassNames: {
|
|
40
|
+
* wrapperClassName: "my-custom-wrapper",
|
|
41
|
+
* labelClassName: "my-custom-label"
|
|
42
|
+
* },
|
|
43
|
+
* helpText: "Please enter your username.",
|
|
44
|
+
* };
|
|
45
|
+
*
|
|
46
|
+
* const getInput = () => <input type="text" name="username" />;
|
|
47
|
+
*
|
|
48
|
+
* <RenderFormField fieldConfig={fieldConfig} getInput={getInput} />;
|
|
49
|
+
*/
|
|
50
|
+
const RenderFormField = ({
|
|
51
|
+
fieldConfig,
|
|
52
|
+
getInput,
|
|
53
|
+
}: {
|
|
54
|
+
fieldConfig: FormFieldSchema;
|
|
55
|
+
getInput: Function;
|
|
56
|
+
}): JSX.Element => {
|
|
57
|
+
function formTypeHorizontal() {
|
|
58
|
+
return (
|
|
59
|
+
<Field
|
|
60
|
+
className={`${
|
|
61
|
+
fieldConfig.customClassNames?.wrapperClassName ??
|
|
62
|
+
"form-group flex sm:flex-row flex-col"
|
|
63
|
+
}`}
|
|
64
|
+
disabled={fieldConfig.disabled}>
|
|
65
|
+
{fieldConfig.label && (
|
|
66
|
+
<Label
|
|
67
|
+
htmlFor={fieldConfig.name}
|
|
68
|
+
className={`${
|
|
69
|
+
fieldConfig.customClassNames?.labelClassName ??
|
|
70
|
+
"form-label sm:w-1/4 sm:ltr:mr-2 rtl:ml-2"
|
|
71
|
+
}`}>
|
|
72
|
+
{fieldConfig.label}
|
|
73
|
+
{fieldConfig.required && (
|
|
74
|
+
<span className="field-required">
|
|
75
|
+
<SVGIcon name="starIcon" height={6} width={6} />
|
|
76
|
+
</span>
|
|
77
|
+
)}
|
|
78
|
+
</Label>
|
|
79
|
+
)}
|
|
80
|
+
{getInput()}
|
|
81
|
+
|
|
82
|
+
<ErrorContextHandler fieldConfig={fieldConfig} />
|
|
83
|
+
|
|
84
|
+
{fieldConfig && fieldConfig.helpText && (
|
|
85
|
+
<Description className="mt-2 text-tiny text-gray-500">
|
|
86
|
+
{fieldConfig.helpText}
|
|
87
|
+
</Description>
|
|
88
|
+
)}
|
|
89
|
+
</Field>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formTypeVertical() {
|
|
94
|
+
return (
|
|
95
|
+
<Field
|
|
96
|
+
disabled={fieldConfig.disabled}
|
|
97
|
+
className={`${
|
|
98
|
+
fieldConfig.customClassNames?.wrapperClassName ??
|
|
99
|
+
"form-group"
|
|
100
|
+
}`}>
|
|
101
|
+
{fieldConfig.label && (
|
|
102
|
+
<Label
|
|
103
|
+
htmlFor={fieldConfig.name}
|
|
104
|
+
className={`${
|
|
105
|
+
fieldConfig.customClassNames?.labelClassName ??
|
|
106
|
+
"form-label"
|
|
107
|
+
}`}>
|
|
108
|
+
{fieldConfig.label}
|
|
109
|
+
{fieldConfig.required && (
|
|
110
|
+
<span className="field-required">
|
|
111
|
+
<SVGIcon name="starIcon" height={6} width={6} />
|
|
112
|
+
</span>
|
|
113
|
+
)}
|
|
114
|
+
</Label>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{getInput()}
|
|
118
|
+
|
|
119
|
+
<ErrorContextHandler fieldConfig={fieldConfig} />
|
|
120
|
+
|
|
121
|
+
{fieldConfig && fieldConfig.helpText && (
|
|
122
|
+
<Description className="mt-2 text-tiny text-gray-500">
|
|
123
|
+
{fieldConfig.helpText}
|
|
124
|
+
</Description>
|
|
125
|
+
)}
|
|
126
|
+
</Field>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return fieldConfig.disableDefaultWrapper ? (
|
|
131
|
+
fieldConfig.wrapper ? (
|
|
132
|
+
<fieldConfig.wrapper>
|
|
133
|
+
{getInput()}
|
|
134
|
+
<ErrorContextHandler fieldConfig={fieldConfig} />
|
|
135
|
+
</fieldConfig.wrapper>
|
|
136
|
+
) : (
|
|
137
|
+
<>
|
|
138
|
+
{getInput()}
|
|
139
|
+
<ErrorContextHandler fieldConfig={fieldConfig} />
|
|
140
|
+
</>
|
|
141
|
+
)
|
|
142
|
+
) : fieldConfig.align === FieldAlignType.HORIZONTAL ? (
|
|
143
|
+
formTypeHorizontal()
|
|
144
|
+
) : (
|
|
145
|
+
formTypeVertical()
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export default RenderFormField;
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
8
|
+
|
|
9
|
+
import { ListboxOption, ListboxOptions } from "@headlessui/react";
|
|
10
|
+
import { handleChange } from ".";
|
|
11
|
+
import { LoaderWithText } from "../../util/LoaderWithText";
|
|
12
|
+
import SVGIcon from "../../util/svg/SVGIcon";
|
|
13
|
+
import { FormContextType } from "../context/FormContext";
|
|
14
|
+
import {
|
|
15
|
+
FieldOptionsSchema,
|
|
16
|
+
FormFieldSchema,
|
|
17
|
+
FormFieldType,
|
|
18
|
+
OutputFormatType,
|
|
19
|
+
} from "../schema/FormFieldSchema";
|
|
20
|
+
|
|
21
|
+
type RenderListOptionsProps = {
|
|
22
|
+
formContext: FormContextType;
|
|
23
|
+
fieldConfig: FormFieldSchema;
|
|
24
|
+
onChange?: (value: any) => void;
|
|
25
|
+
listOptions: FieldOptionsSchema[];
|
|
26
|
+
setListOptions: (value: any) => void;
|
|
27
|
+
loading?: boolean;
|
|
28
|
+
formField: FormFieldType;
|
|
29
|
+
setLoading?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
30
|
+
createCallback?: (value: any) => void;
|
|
31
|
+
queryCallback?: (value: any) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
|
|
35
|
+
(
|
|
36
|
+
{
|
|
37
|
+
formContext,
|
|
38
|
+
fieldConfig,
|
|
39
|
+
listOptions,
|
|
40
|
+
setListOptions,
|
|
41
|
+
setLoading,
|
|
42
|
+
loading,
|
|
43
|
+
formField,
|
|
44
|
+
onChange,
|
|
45
|
+
queryCallback,
|
|
46
|
+
createCallback,
|
|
47
|
+
},
|
|
48
|
+
ref
|
|
49
|
+
) => {
|
|
50
|
+
const [query, setQuery] = useState<string>("");
|
|
51
|
+
const [createdListItems, setCreatedListItems] = useState<
|
|
52
|
+
FieldOptionsSchema[]
|
|
53
|
+
>([]);
|
|
54
|
+
// const inputRef = useRef<HTMLInputElement>(null);
|
|
55
|
+
const createItem = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
56
|
+
if (fieldConfig.onCreateTrigger && createCallback) {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
e.stopPropagation();
|
|
59
|
+
fieldConfig.onCreateTrigger(query, createCallback);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setLoading && setLoading(true);
|
|
64
|
+
try {
|
|
65
|
+
if (!fieldConfig.postUrl) {
|
|
66
|
+
const data: FieldOptionsSchema = {
|
|
67
|
+
label: query,
|
|
68
|
+
value: query,
|
|
69
|
+
};
|
|
70
|
+
setCreatedListItems((prevList) => [...prevList, data]);
|
|
71
|
+
}
|
|
72
|
+
} catch (e: any) {
|
|
73
|
+
console.error("Error in Creating the object ", e);
|
|
74
|
+
} finally {
|
|
75
|
+
resetToDefault();
|
|
76
|
+
setLoading && setLoading(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const resetToDefault = () => {
|
|
80
|
+
if (query != "") {
|
|
81
|
+
setQuery("");
|
|
82
|
+
if (isTypeahead) {
|
|
83
|
+
queryCallback && queryCallback("");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
let resultList = [...createdListItems, ...listOptions];
|
|
88
|
+
resultList = [...resultList.filter((result) => result.label).filter((option, index, self) => index === self.findIndex((obj) => obj.value === option.value))]; // removing duplicate values
|
|
89
|
+
|
|
90
|
+
const caseSensitive =
|
|
91
|
+
query && fieldConfig.dropdownFieldConfig?.isCaseSensitive
|
|
92
|
+
? query
|
|
93
|
+
: query?.toLowerCase();
|
|
94
|
+
|
|
95
|
+
const filteredList = query
|
|
96
|
+
? resultList.filter((item) => {
|
|
97
|
+
const normalizedLabel = fieldConfig.dropdownFieldConfig
|
|
98
|
+
?.isCaseSensitive
|
|
99
|
+
? item.label
|
|
100
|
+
: item.label.toLowerCase();
|
|
101
|
+
|
|
102
|
+
return normalizedLabel
|
|
103
|
+
.replace(/\s+/g, "")
|
|
104
|
+
.includes(caseSensitive.replace(/\s+/g, ""));
|
|
105
|
+
})
|
|
106
|
+
: resultList;
|
|
107
|
+
|
|
108
|
+
let nullGroupOptions: any[] = [];
|
|
109
|
+
let groupedOptions: any = filteredList.reduce((acc: any, option: any) => {
|
|
110
|
+
if (!option.groupName) {
|
|
111
|
+
nullGroupOptions.push(option);
|
|
112
|
+
} else {
|
|
113
|
+
if (!acc[option.groupName]) {
|
|
114
|
+
acc[option.groupName] = [];
|
|
115
|
+
}
|
|
116
|
+
acc[option.groupName].push(option);
|
|
117
|
+
}
|
|
118
|
+
return acc;
|
|
119
|
+
}, {});
|
|
120
|
+
|
|
121
|
+
const handleQueryCallback = useCallback(() => {
|
|
122
|
+
if (filteredList.length == 0 && isTypeahead) {
|
|
123
|
+
queryCallback && queryCallback(query);
|
|
124
|
+
}
|
|
125
|
+
}, [filteredList]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
handleQueryCallback();
|
|
129
|
+
}, [query]);
|
|
130
|
+
|
|
131
|
+
let enableCreateFlag =
|
|
132
|
+
formField != FormFieldType.SELECT &&
|
|
133
|
+
formField != FormFieldType.MULTI_SELECT &&
|
|
134
|
+
fieldConfig.dropdownFieldConfig?.showCreateOption;
|
|
135
|
+
|
|
136
|
+
let validTypeaheadFields = [
|
|
137
|
+
FormFieldType.TYPEAHEAD,
|
|
138
|
+
FormFieldType.TYPEAHEAD_MULTI_SELECT,
|
|
139
|
+
];
|
|
140
|
+
let isTypeahead: boolean = validTypeaheadFields.indexOf(formField) > -1;
|
|
141
|
+
let isNotMatched: boolean =
|
|
142
|
+
query != "" && filteredList.findIndex((v) => v.label === query) === -1;
|
|
143
|
+
|
|
144
|
+
const renderOption = (option: FieldOptionsSchema) => {
|
|
145
|
+
let selected = false;
|
|
146
|
+
if (Array.isArray(formContext.getValues(fieldConfig.name))) {
|
|
147
|
+
selected = formContext
|
|
148
|
+
.getValues(fieldConfig.name)
|
|
149
|
+
.includes(option.value);
|
|
150
|
+
} else {
|
|
151
|
+
const formValue = formContext.getValues(fieldConfig.name);
|
|
152
|
+
// const defaultValue = fieldConfig.defaultValue;
|
|
153
|
+
// selected = formValue ? option.value == formValue : defaultValue == option.value;
|
|
154
|
+
selected = option.value == formValue;
|
|
155
|
+
}
|
|
156
|
+
return (
|
|
157
|
+
<ListboxOption
|
|
158
|
+
key={option.value}
|
|
159
|
+
disabled={option.isDisabled}
|
|
160
|
+
onClick={() => setTimeout(resetToDefault, 300)}
|
|
161
|
+
className={`form-listbox-option ${
|
|
162
|
+
selected ? " bg-gray-100 text-gray-900" : "bg-white text-gray-700"
|
|
163
|
+
}`}
|
|
164
|
+
value={option.value}
|
|
165
|
+
>
|
|
166
|
+
{fieldConfig.fieldOptionWrapper ? (
|
|
167
|
+
<fieldConfig.fieldOptionWrapper data={option} selected={selected} />
|
|
168
|
+
) : (
|
|
169
|
+
<>
|
|
170
|
+
<div className="flex items-center justify-between gap-x-1.5">
|
|
171
|
+
<span
|
|
172
|
+
className={`block truncate w-full !max-w-[150px] space-x-2 ${
|
|
173
|
+
selected ? "font-medium" : "font-normal"
|
|
174
|
+
}`}
|
|
175
|
+
>
|
|
176
|
+
{option.icon && (
|
|
177
|
+
<span className="listbox-svg">{option.icon}</span>
|
|
178
|
+
)}
|
|
179
|
+
{option.label}
|
|
180
|
+
</span>
|
|
181
|
+
{isTypeahead ? (
|
|
182
|
+
<input
|
|
183
|
+
type="checkbox"
|
|
184
|
+
className="form-checkbox"
|
|
185
|
+
checked={selected}
|
|
186
|
+
/>
|
|
187
|
+
) : (
|
|
188
|
+
selected && (
|
|
189
|
+
<SVGIcon
|
|
190
|
+
name="checkIcon"
|
|
191
|
+
className="h-5 w-5"
|
|
192
|
+
aria-hidden="true"
|
|
193
|
+
/>
|
|
194
|
+
)
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
{option.helpText && (
|
|
198
|
+
<div className="mt-0 text-sm text-gray-500 font-normal truncate w-full !max-w-[150px]">
|
|
199
|
+
{option.helpText}
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
</>
|
|
203
|
+
)}
|
|
204
|
+
</ListboxOption>
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const renderList = useMemo(() => {
|
|
209
|
+
if (filteredList.length === 0 && (!enableCreateFlag || query === ""))
|
|
210
|
+
return (
|
|
211
|
+
<div className="form-listbox-option text-center">
|
|
212
|
+
<span className="empty-content text-gray-600 font-normal">
|
|
213
|
+
No Data Available
|
|
214
|
+
</span>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<>
|
|
220
|
+
{nullGroupOptions.length > 0 && nullGroupOptions.map(renderOption)}
|
|
221
|
+
|
|
222
|
+
{query !== "" && enableCreateFlag && isNotMatched && (
|
|
223
|
+
<ListboxOption
|
|
224
|
+
key="create_new"
|
|
225
|
+
className="form-combobox-option"
|
|
226
|
+
value={query}
|
|
227
|
+
onClick={createItem}
|
|
228
|
+
>
|
|
229
|
+
<div className="flex items-center justify-between">
|
|
230
|
+
<span className="truncate overflow-x-auto">{query}</span>
|
|
231
|
+
<span className="text-primary flex items-center gap-x-1 cursor-pointer">
|
|
232
|
+
<SVGIcon name="plus" className="h-3 w-3" />
|
|
233
|
+
{fieldConfig.dropdownFieldConfig?.createLabelText || "Select"}
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
</ListboxOption>
|
|
237
|
+
)}
|
|
238
|
+
</>
|
|
239
|
+
);
|
|
240
|
+
}, [filteredList, nullGroupOptions]);
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<ListboxOptions
|
|
244
|
+
anchor="bottom start"
|
|
245
|
+
transition
|
|
246
|
+
portal={false}
|
|
247
|
+
modal={false}
|
|
248
|
+
onBlur={() => setTimeout(resetToDefault, 300)}
|
|
249
|
+
className={`form-listbox-options ${fieldConfig?.customClassNames?.optionsWrapperClassName}`}
|
|
250
|
+
>
|
|
251
|
+
{fieldConfig.dropdownFieldConfig?.showSearchBox && (
|
|
252
|
+
<div className="relative">
|
|
253
|
+
<input
|
|
254
|
+
onKeyDown={(e: any) => e.stopPropagation()}
|
|
255
|
+
onChange={(event) => {
|
|
256
|
+
setQuery(event.target.value);
|
|
257
|
+
if (event.target.value == "" && isTypeahead) {
|
|
258
|
+
queryCallback && queryCallback("");
|
|
259
|
+
}
|
|
260
|
+
}}
|
|
261
|
+
className="form-input !pr-[36px] Search-field"
|
|
262
|
+
placeholder="Search"
|
|
263
|
+
value={query}
|
|
264
|
+
autoFocus
|
|
265
|
+
type="text"
|
|
266
|
+
/>
|
|
267
|
+
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center mr-3">
|
|
268
|
+
<svg
|
|
269
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
270
|
+
viewBox="0 0 16 16"
|
|
271
|
+
fill="currentColor"
|
|
272
|
+
aria-hidden="true"
|
|
273
|
+
data-slot="icon"
|
|
274
|
+
className="h-5 w-5 text-gray-400"
|
|
275
|
+
>
|
|
276
|
+
<path
|
|
277
|
+
fillRule="evenodd"
|
|
278
|
+
d="M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z"
|
|
279
|
+
clipRule="evenodd"
|
|
280
|
+
></path>
|
|
281
|
+
</svg>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
<div className="form-listbox-options-container">
|
|
287
|
+
{loading ? (
|
|
288
|
+
<div className="form-listbox-option">
|
|
289
|
+
<div className="flex items-center justify-center p-3">
|
|
290
|
+
<LoaderWithText />
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
) : (
|
|
294
|
+
renderList
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
{Object.keys(groupedOptions).length > 0 &&
|
|
298
|
+
Object.keys(groupedOptions).map((groupName) => (
|
|
299
|
+
<div key={groupName}>
|
|
300
|
+
<h2 className="pb-1 pt-2 font-bold">{groupName}</h2>
|
|
301
|
+
{groupedOptions[groupName].map(renderOption)}
|
|
302
|
+
</div>
|
|
303
|
+
))}
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
</ListboxOptions>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
export default RenderListOptions;
|
|
313
|
+
export function renderListBoxValue(
|
|
314
|
+
formContext: FormContextType,
|
|
315
|
+
fieldConfig: FormFieldSchema,
|
|
316
|
+
listOptions: FieldOptionsSchema[],
|
|
317
|
+
onChange?: (value: any) => void
|
|
318
|
+
): JSX.Element {
|
|
319
|
+
let value = formContext.getValues(fieldConfig.name);
|
|
320
|
+
const renderAsString = () => {
|
|
321
|
+
// if (!listOptions) {
|
|
322
|
+
// return value;
|
|
323
|
+
// }
|
|
324
|
+
let icon = listOptions.find((option) => option.value == value)?.icon,
|
|
325
|
+
label = listOptions.find((option) => option.value == value)?.label;
|
|
326
|
+
return icon ? (
|
|
327
|
+
<span className="flex items-center fs-8 whitespace-nowrap text-gray-500">
|
|
328
|
+
<span>{icon}</span>
|
|
329
|
+
<span>{label} </span>
|
|
330
|
+
</span>
|
|
331
|
+
) : (
|
|
332
|
+
label || getPlaceholder()
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
const renderAsArray = () => {
|
|
336
|
+
if (value.length == 0 || (value.length == 1 && value[0] == "")) {
|
|
337
|
+
return getPlaceholder();
|
|
338
|
+
}
|
|
339
|
+
// if (!listOptions) {
|
|
340
|
+
// return value;
|
|
341
|
+
// }
|
|
342
|
+
const values = value;
|
|
343
|
+
return fieldConfig.dropdownFieldConfig?.showSelectedCount ? (
|
|
344
|
+
<span className="form-selected-badge">
|
|
345
|
+
<span className="form-selected-badge-name">
|
|
346
|
+
{values.length > 1
|
|
347
|
+
? `${values.length} selected`
|
|
348
|
+
: listOptions.find((option) => option.value == value[0])?.label ||
|
|
349
|
+
values[0]}
|
|
350
|
+
</span>
|
|
351
|
+
{getDeleteButton()}
|
|
352
|
+
</span>
|
|
353
|
+
) : (
|
|
354
|
+
Array.isArray(values) &&
|
|
355
|
+
values.map((opt: any) => {
|
|
356
|
+
const option = listOptions.find((option) => option.value == opt);
|
|
357
|
+
if (!option && values.length == 0) return getPlaceholder();
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<span key={option?.value} className="form-selected-badge">
|
|
361
|
+
<span className="form-selected-badge-name">{option?.label}</span>
|
|
362
|
+
{getDeleteButton(opt)}
|
|
363
|
+
</span>
|
|
364
|
+
);
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
};
|
|
368
|
+
const getDeleteButton = (option?: string) => {
|
|
369
|
+
return (
|
|
370
|
+
fieldConfig.dropdownFieldConfig?.showDeleteOption && (
|
|
371
|
+
<button
|
|
372
|
+
type="button"
|
|
373
|
+
className="group relative -mr-1 h-3.5 w-3.5 rounded-sm hover:bg-gray-500/20"
|
|
374
|
+
onClick={(e) => {
|
|
375
|
+
e.preventDefault();
|
|
376
|
+
if (fieldConfig.dropdownFieldConfig?.showSelectedCount) {
|
|
377
|
+
handleChange(null, formContext, fieldConfig, onChange);
|
|
378
|
+
} else {
|
|
379
|
+
let options: string[] = value as string[];
|
|
380
|
+
options = options.filter((i) => i != option);
|
|
381
|
+
handleChange(options, formContext, fieldConfig, onChange);
|
|
382
|
+
}
|
|
383
|
+
}}
|
|
384
|
+
>
|
|
385
|
+
<span className="sr-only">Remove</span>
|
|
386
|
+
<svg
|
|
387
|
+
viewBox="0 0 14 14"
|
|
388
|
+
className="h-3.5 w-3.5 stroke-gray-600/50 group-hover:stroke-gray-600/75"
|
|
389
|
+
>
|
|
390
|
+
<path d="M4 4l6 6m0-6l-6 6" />
|
|
391
|
+
</svg>
|
|
392
|
+
<span className="absolute -inset-1" />
|
|
393
|
+
</button>
|
|
394
|
+
)
|
|
395
|
+
);
|
|
396
|
+
};
|
|
397
|
+
let outputFormat = fieldConfig.outputFormat
|
|
398
|
+
? fieldConfig.outputFormat === OutputFormatType.ARRAY
|
|
399
|
+
: false;
|
|
400
|
+
const getPlaceholder = () => (
|
|
401
|
+
<span className="form-placeholder">
|
|
402
|
+
{fieldConfig.placeholder || "Select any option"}
|
|
403
|
+
</span>
|
|
404
|
+
);
|
|
405
|
+
const renderValue = () => {
|
|
406
|
+
if (!value && !listOptions) {
|
|
407
|
+
return getPlaceholder();
|
|
408
|
+
}
|
|
409
|
+
if (!outputFormat && Array.isArray(value)) {
|
|
410
|
+
return renderAsArray();
|
|
411
|
+
}
|
|
412
|
+
return renderAsString();
|
|
413
|
+
};
|
|
414
|
+
return (
|
|
415
|
+
<>
|
|
416
|
+
<span className="form-selected-option w-full">{renderValue()}</span>
|
|
417
|
+
<SVGIcon
|
|
418
|
+
name="chevronDown"
|
|
419
|
+
className="h-5 w-5 text-gray-400"
|
|
420
|
+
aria-hidden="true"
|
|
421
|
+
/>
|
|
422
|
+
</>
|
|
423
|
+
);
|
|
424
|
+
}
|