@engagebay/engagebay-form-module 1.0.3 → 1.0.6

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": "@engagebay/engagebay-form-module",
3
- "version": "1.0.3",
3
+ "version": "1.0.6",
4
4
  "description": "Provide base form components to reacho",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -28,6 +28,7 @@ import TimeField from "./formfields/TimeField";
28
28
  import Typeahead from "./formfields/Typeahead";
29
29
  import TypeaheadMultiSelect from "./formfields/TypeaheadMultiSelect";
30
30
  import UrlField from "./formfields/UrlField";
31
+ import Typeahead2 from "./formfields/Typeahead2";
31
32
  import {
32
33
  FieldOptionsSchema,
33
34
  FormFieldComponentPropSchema,
@@ -136,6 +137,9 @@ const formFieldComponents: FormComponentSchema = {
136
137
  [FormFieldType[FormFieldType.TYPEAHEAD]]: {
137
138
  component: Typeahead,
138
139
  },
140
+ [FormFieldType[FormFieldType.TYPEAHEAD_2]]: {
141
+ component: Typeahead2,
142
+ },
139
143
  [FormFieldType[FormFieldType.TYPEAHEAD_MULTI_SELECT]]: {
140
144
  component: TypeaheadMultiSelect,
141
145
  },
@@ -35,14 +35,28 @@ const DatePicker: React.FC<FormFieldComponentPropSchema> = (
35
35
  let hookProps = formContext.register(props.fieldConfig.name, registerOptions);
36
36
 
37
37
  function getInput() {
38
+ const rawStart =
39
+ initialDate?.startDate ||
40
+ props.fieldConfig.defaultValue ||
41
+ "";
42
+ const rawEnd =
43
+ initialDate?.endDate ||
44
+ props.fieldConfig.defaultValue ||
45
+ "";
46
+ const toDate = (v: string | Date | null | undefined): Date | null => {
47
+ if (v == null || v === "") return null;
48
+ return moment(v).isValid() ? moment(v).toDate() : null;
49
+ };
50
+ const value = {
51
+ startDate: toDate(rawStart),
52
+ endDate: toDate(rawEnd),
53
+ };
54
+ // Calendar opens on selected date's month/year (must be a Date instance)
55
+ const startFrom = value.startDate instanceof Date ? value.startDate : new Date();
38
56
  return (
39
57
  <Datepicker
40
- value={
41
- initialDate || {
42
- startDate: props.fieldConfig.defaultValue,
43
- endDate: props.fieldConfig.defaultValue,
44
- }
45
- }
58
+ value={value}
59
+ startFrom={startFrom}
46
60
  {...hookProps}
47
61
  placeholder={
48
62
  props.fieldConfig.placeholder
@@ -53,13 +67,7 @@ const DatePicker: React.FC<FormFieldComponentPropSchema> = (
53
67
  popoverDirection="down"
54
68
  useRange={false}
55
69
  inputName={props.fieldConfig.name}
56
- key={
57
- props.fieldConfig.name +
58
- "_" +
59
- initialDate.startDate +
60
- "_" +
61
- initialDate.endDate
62
- }
70
+ key={props.fieldConfig.name + "_" + rawStart + "_" + rawEnd}
63
71
  containerClassName={"relative"}
64
72
  minDate={props.fieldConfig.minDate}
65
73
  maxDate={props.fieldConfig.maxDate}
@@ -1,61 +1,66 @@
1
- import React, {useContext} from "react";
2
- import {RegisterOptions} from "react-hook-form";
3
- import {FormContext} from "../context/FormContext";
4
- import {FormFieldComponentPropSchema} from "../schema/FormFieldSchema";
5
- import {handleChange, registerFormField} from "../util";
1
+ import React, { useContext } from "react";
2
+ import { RegisterOptions } from "react-hook-form";
3
+ import { FormContext } from "../context/FormContext";
4
+ import { FormFieldComponentPropSchema } from "../schema/FormFieldSchema";
5
+ import { handleChange, registerFormField } from "../util";
6
6
  import RenderFormField from "../util/RenderFormField";
7
7
 
8
8
  const NumberField: React.FC<FormFieldComponentPropSchema> = (
9
- props: FormFieldComponentPropSchema
9
+ props: FormFieldComponentPropSchema,
10
10
  ) => {
11
- const formContext = useContext(FormContext);
11
+ const formContext = useContext(FormContext);
12
12
 
13
- let registerOptions: RegisterOptions = registerFormField(props.fieldConfig);
13
+ let registerOptions: RegisterOptions = registerFormField(props.fieldConfig);
14
14
 
15
- let hookProps = formContext.register(props.fieldConfig.name, registerOptions);
16
-
17
- function getInput() {
18
- return (
19
- <input
20
- type="number"
21
- {...hookProps}
22
- placeholder={props.fieldConfig?.placeholder}
23
- readOnly={props.fieldConfig?.readOnly}
24
- disabled={props.fieldConfig?.disabled}
25
- autoComplete={props.fieldConfig?.autoComplete}
26
- min={props.fieldConfig?.min}
27
- max={props.fieldConfig?.max}
28
- step={props.fieldConfig.decimalAllowed ? 0.01 : 1}
29
- defaultValue={props.fieldConfig.defaultValue as string}
30
- className={`form-input ${
31
- props.fieldConfig.customClassNames?.fieldClassName || "flex-1 !w-60"
32
- }`}
33
- onKeyDown={(e) => {
34
- props.fieldConfig.onKeyDown && props.fieldConfig.onKeyDown(e);
35
- }}
36
- onChange={(e) => {
37
- let value = e.target.value;
38
- let numericValue = parseFloat(value);
39
- if (props.fieldConfig.max !== undefined && numericValue > props.fieldConfig.max) {
40
- numericValue = props.fieldConfig.max;
41
- }
42
- if (props.fieldConfig.min !== undefined && numericValue < props.fieldConfig.min) {
43
- numericValue = props.fieldConfig.min;
44
- }
45
- value = numericValue.toString();
46
- handleChange(
47
- value,
48
- formContext,
49
- props.fieldConfig,
50
- props.onChange
51
- );
52
- }}
53
- />
54
- );
55
- }
15
+ let hookProps = formContext.register(props.fieldConfig.name, registerOptions);
56
16
 
17
+ function getInput() {
57
18
  return (
58
- <RenderFormField fieldConfig={props.fieldConfig} getInput={getInput}/>
19
+ <input
20
+ type="number"
21
+ {...hookProps}
22
+ placeholder={props.fieldConfig?.placeholder}
23
+ readOnly={props.fieldConfig?.readOnly}
24
+ disabled={props.fieldConfig?.disabled}
25
+ autoComplete={props.fieldConfig?.autoComplete}
26
+ min={props.fieldConfig?.min}
27
+ max={props.fieldConfig?.max}
28
+ step={props.fieldConfig.decimalAllowed ? 0.01 : 1}
29
+ defaultValue={props.fieldConfig.defaultValue as string}
30
+ className={`form-input ${
31
+ props.fieldConfig.customClassNames?.fieldClassName || "flex-1 !w-60"
32
+ }`}
33
+ onKeyDown={(e) => {
34
+ props.fieldConfig.onKeyDown && props.fieldConfig.onKeyDown(e);
35
+ }}
36
+ onChange={(e) => {
37
+ let value = e.target.value;
38
+ let numericValue = parseFloat(value);
39
+ if (
40
+ props.fieldConfig.max !== undefined &&
41
+ numericValue > props.fieldConfig.max
42
+ ) {
43
+ numericValue = props.fieldConfig.max;
44
+ }
45
+ if (
46
+ props.fieldConfig.min !== undefined &&
47
+ numericValue < props.fieldConfig.min
48
+ ) {
49
+ numericValue = props.fieldConfig.min;
50
+ }
51
+ handleChange(
52
+ numericValue,
53
+ formContext,
54
+ props.fieldConfig,
55
+ props.onChange,
56
+ );
57
+ }}
58
+ />
59
59
  );
60
+ }
61
+
62
+ return (
63
+ <RenderFormField fieldConfig={props.fieldConfig} getInput={getInput} />
64
+ );
60
65
  };
61
66
  export default NumberField;
@@ -0,0 +1,284 @@
1
+ import React, {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+
10
+ import { Listbox, ListboxButton } from "@headlessui/react";
11
+ import { RegisterOptions } from "react-hook-form";
12
+
13
+ import { FormContext } from "../context/FormContext";
14
+ import { getListOption, getListOptions } from "../FormFieldUtils";
15
+ import {
16
+ FieldOptionsSchema,
17
+ FormFieldComponentPropSchema,
18
+ FormFieldType,
19
+ } from "../schema/FormFieldSchema";
20
+ import { handleChange, registerFormField } from "../util";
21
+ import RenderFormField from "../util/RenderFormField";
22
+ import RenderListOptions, {
23
+ renderListBoxValue,
24
+ } from "../util/RenderListOptions";
25
+ import axios from "axios";
26
+ import { getAxiosInstance } from "../../api";
27
+
28
+ const Typeahead2: React.FC<FormFieldComponentPropSchema> = (
29
+ props: FormFieldComponentPropSchema,
30
+ ) => {
31
+ const dynamicSelectRef = useRef<HTMLUListElement>(null);
32
+ const abortControllerRef = useRef<AbortController | null>(null);
33
+ const formContext = useContext(FormContext);
34
+ let registerOptions: RegisterOptions = registerFormField(props.fieldConfig);
35
+ let hookProps = formContext.register(props.fieldConfig.name, registerOptions);
36
+ const [listOptions, setListOptions] = useState<FieldOptionsSchema[]>([]);
37
+ const [selectedValues, setSelectedValues] = useState<FieldOptionsSchema[]>(
38
+ [],
39
+ );
40
+ const [loading, setLoading] = useState<boolean>(false);
41
+
42
+ useEffect(() => {
43
+ if (
44
+ !formContext.getValues(props.fieldConfig.name) &&
45
+ props.fieldConfig.defaultValue
46
+ ) {
47
+ formContext.setValue(
48
+ props.fieldConfig.name,
49
+ props.fieldConfig.defaultValue,
50
+ );
51
+ }
52
+ let values: any | any[] | undefined =
53
+ formContext.getValues(props.fieldConfig.name) ||
54
+ props.fieldConfig.defaultValue;
55
+
56
+ if (values && selectedValues.length < 1) {
57
+ if (props.fieldConfig.fetchSavedDataUrl) {
58
+ fetchValue(values);
59
+ } else {
60
+ setSelectedValues(
61
+ Array.isArray(values)
62
+ ? values.map((val: any) => ({ label: val, value: val }))
63
+ : [values],
64
+ );
65
+ }
66
+ }
67
+ }, []);
68
+
69
+
70
+
71
+ const fetchData = useMemo(() => {
72
+ let timeoutId: ReturnType<typeof setTimeout>;
73
+
74
+ return (_query: string | undefined) => {
75
+ // Clear the previous timer
76
+ if (timeoutId) clearTimeout(timeoutId);
77
+
78
+ // Set a new timer
79
+ timeoutId = setTimeout(async () => {
80
+ // 1. Cancel the previous request if it's still pending
81
+ if (abortControllerRef.current) {
82
+ abortControllerRef.current.abort();
83
+ }
84
+
85
+ // 2. Create a new controller for the current request
86
+ const controller = new AbortController();
87
+ abortControllerRef.current = controller;
88
+ const minLength = props.fieldConfig?.allowedMinQueryLength ?? 3;
89
+
90
+ if (!_query || _query.length < minLength) {
91
+ setListOptions([]);
92
+ setLoading(false);
93
+ return;
94
+ }
95
+ setLoading(true);
96
+ try {
97
+ if (!props.fieldConfig.fetchUrl) return;
98
+
99
+ let url = props.fieldConfig.fetchUrl;
100
+
101
+ url = url.includes("?") ? url + "&q=" + _query : url + "?q=" + _query;
102
+
103
+ const axiosInstance = getAxiosInstance(
104
+ formContext.axiosInstance,
105
+ props.fieldConfig,
106
+ );
107
+ const response = await axiosInstance.get(url);
108
+
109
+ if (controller === abortControllerRef.current) {
110
+ if (response?.data) {
111
+ const data: FieldOptionsSchema[] = getListOptions(
112
+ response?.data,
113
+ props.fieldConfig.optionsConfig,
114
+ );
115
+ setListOptions([...data]);
116
+
117
+ if (props.fieldConfig.fetchCallback) {
118
+ props.fieldConfig.fetchCallback(response);
119
+ }
120
+ }
121
+ }
122
+ } catch (err) {
123
+ console.error("Fetch error:", err);
124
+ } finally {
125
+ // Only stop loading if this was the "active" request
126
+ if (controller === abortControllerRef.current) {
127
+ setLoading(false);
128
+ }
129
+ }
130
+ }, 250);
131
+ };
132
+ }, []);
133
+
134
+ // Cleanup on unmount
135
+ useEffect(() => {
136
+ return () => {
137
+ abortControllerRef.current?.abort();
138
+ };
139
+ }, []);
140
+
141
+ const fetchValue = async (value: string | any | any[]) => {
142
+ try {
143
+ if (
144
+ props.fieldConfig.ignoreFetchValue ||
145
+ !props.fieldConfig.fetchSavedDataUrl
146
+ ) {
147
+ return;
148
+ }
149
+
150
+ let url = props.fieldConfig.fetchSavedDataUrl;
151
+
152
+ // url = url.includes("?")
153
+ // ? url + "&values=" + value
154
+ // : url + "?values=" + value;
155
+
156
+ let response = await (props.fieldConfig.disableHeaderInFetch
157
+ ? axios.post(url, Array.isArray(value) ? value : [value])
158
+ : formContext.axiosInstance?.post(
159
+ url,
160
+ Array.isArray(value) ? value : [value],
161
+ ));
162
+ if (response?.data) {
163
+ const data: FieldOptionsSchema[] = getListOptions(
164
+ response?.data,
165
+ props.fieldConfig.optionsConfig,
166
+ );
167
+ let values: any[] = formContext.getValues(props.fieldConfig.name) || [];
168
+ setSelectedValues(
169
+ [
170
+ ...data,
171
+ props.fieldConfig.dropdownFieldConfig?.isSuggestionBox &&
172
+ values &&
173
+ Array.isArray(values)
174
+ ? values
175
+ .filter(
176
+ (value) =>
177
+ data.some((d) => d.value !== value) || data.length == 0,
178
+ )
179
+ .map((val) => ({ label: val, value: val }))
180
+ : [],
181
+ ].flat(),
182
+ );
183
+ }
184
+ } catch (err) { }
185
+ };
186
+
187
+ const updateListOptions = (data: any) => {
188
+ const resData: FieldOptionsSchema = getListOption(
189
+ data,
190
+ props.fieldConfig.optionsConfig,
191
+ );
192
+ setListOptions([]);
193
+ setSelectedValues((prev) =>
194
+ props.fieldConfig.isMultiple ? [...prev, resData] : [resData],
195
+ );
196
+ let result = formContext.getValues(props.fieldConfig.name) || [];
197
+ result = !props.fieldConfig.isMultiple
198
+ ? resData.value
199
+ : result.includes(resData.value)
200
+ ? [...result.filter((v: string) => v != resData.value), resData.value]
201
+ : [...result, resData.value];
202
+ formContext.setValue(props.fieldConfig.name, result);
203
+ };
204
+
205
+ const getInput = () => {
206
+ return (
207
+ <Listbox
208
+ as={"div"}
209
+ {...hookProps}
210
+ value={
211
+ props.fieldConfig.isMultiple
212
+ ? formContext.getValues(props.fieldConfig.name)
213
+ ? formContext.getValues(props.fieldConfig.name)
214
+ : []
215
+ : formContext.getValues(props.fieldConfig.name)
216
+ }
217
+ name={props.fieldConfig.name}
218
+ defaultValue={props.fieldConfig.defaultValue}
219
+ key={props.fieldConfig.name}
220
+ className={"relative form-listbox flex-1 overflow-hidden"}
221
+ onChange={(selectedOptions) => {
222
+ let currentValue = formContext.getValues(props.fieldConfig.name);
223
+ const newValue =
224
+ currentValue === selectedOptions ? null : selectedOptions;
225
+
226
+ if (props.fieldConfig.isMultiple) {
227
+ currentValue = listOptions.filter((op) =>
228
+ selectedOptions.includes(op.value),
229
+ );
230
+ setSelectedValues((prev) => [...prev, ...currentValue]);
231
+ } else {
232
+ const selected = listOptions.find((o) => o.value == newValue);
233
+ selected && setSelectedValues([selected]);
234
+ }
235
+ setListOptions([]);
236
+ handleChange(
237
+ newValue,
238
+ formContext,
239
+ props.fieldConfig,
240
+ props.onChange,
241
+ );
242
+ }}
243
+ multiple={props.fieldConfig.isMultiple}
244
+ >
245
+ <ListboxButton
246
+ className={
247
+ props.fieldConfig.customClassNames?.fieldClassName
248
+ ? "form-listbox-select " +
249
+ props.fieldConfig.customClassNames?.fieldClassName
250
+ : "form-listbox-select"
251
+ }
252
+ >
253
+ {renderListBoxValue(
254
+ formContext,
255
+ props.fieldConfig,
256
+ [...selectedValues, ...listOptions],
257
+ props.onChange,
258
+ )}
259
+ </ListboxButton>
260
+ <RenderListOptions
261
+ formContext={formContext}
262
+ onChange={props.onChange}
263
+ formField={FormFieldType.TYPEAHEAD_2}
264
+ ref={dynamicSelectRef}
265
+ fieldConfig={props.fieldConfig}
266
+ listOptions={listOptions}
267
+ setListOptions={setListOptions}
268
+ loading={loading}
269
+ setLoading={setLoading}
270
+ createCallback={(data) => updateListOptions(data)}
271
+ queryCallback={(query) => fetchData(query)}
272
+ />
273
+ </Listbox>
274
+ );
275
+ };
276
+ if (props.fieldConfig.hideWhenNoResults && listOptions.length == 0) {
277
+ return <></>;
278
+ }
279
+
280
+ return (
281
+ <RenderFormField fieldConfig={props.fieldConfig} getInput={getInput} />
282
+ );
283
+ };
284
+ export default Typeahead2;
@@ -37,6 +37,7 @@ export enum FormFieldType {
37
37
  DYNAMIC_MULTI_SELECT = "DYNAMIC_MULTI_SELECT",
38
38
 
39
39
  TYPEAHEAD = "TYPEAHEAD",
40
+ TYPEAHEAD_2 = "TYPEAHEAD_2",
40
41
  TYPEAHEAD_MULTI_SELECT = "TYPEAHEAD_MULTI_SELECT",
41
42
  PHONE_NUMBER_INPUT = "PHONE_NUMBER_INPUT",
42
43
  SWITCH = "SWITCH",
@@ -105,6 +106,7 @@ export type FormFieldSchema = {
105
106
  max?: number;
106
107
  min?: number;
107
108
  rows?: number;
109
+ allowedMinQueryLength?: number;
108
110
  defaultValue?: string | string[] | {} | boolean;
109
111
  options?: FieldOptionsSchema[];
110
112
  minDate?: Date | null | undefined;
@@ -127,9 +129,11 @@ export type FormFieldSchema = {
127
129
  decimalAllowed?: boolean;
128
130
  errorMessage?: string;
129
131
  submitOnChange?: boolean;
132
+ isMultiple?: boolean;
130
133
  formFieldPattern?: FormFieldPatternsImpl[];
131
134
  fetchUrl?: string;
132
135
  postUrl?: string;
136
+ fetchSavedDataUrl?: string;
133
137
  fileAccept?: string;
134
138
  icon?: ReactNode;
135
139
  outputFormat?: OutputFormatType;
@@ -11,6 +11,7 @@ import { handleChange } from ".";
11
11
  import { LoaderWithText } from "../../util/LoaderWithText";
12
12
  import SVGIcon from "../../util/svg/SVGIcon";
13
13
  import { FormContextType } from "../context/FormContext";
14
+ import Tippy from "@tippyjs/react";
14
15
  import {
15
16
  FieldOptionsSchema,
16
17
  FormFieldSchema,
@@ -132,7 +133,9 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
132
133
  }, [filteredList]);
133
134
 
134
135
  useEffect(() => {
135
- handleQueryCallback();
136
+ if (formField == FormFieldType.TYPEAHEAD_2)
137
+ queryCallback && queryCallback(query);
138
+ else handleQueryCallback();
136
139
  }, [query]);
137
140
 
138
141
  let enableCreateFlag =
@@ -142,6 +145,7 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
142
145
 
143
146
  let validTypeaheadFields = [
144
147
  FormFieldType.TYPEAHEAD,
148
+ FormFieldType.TYPEAHEAD_2,
145
149
  FormFieldType.TYPEAHEAD_MULTI_SELECT,
146
150
  ];
147
151
  let isTypeahead: boolean = validTypeaheadFields.indexOf(formField) > -1;
@@ -216,7 +220,9 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
216
220
  };
217
221
 
218
222
  const renderList = useMemo(() => {
219
- if (filteredList.length === 0 && (!enableCreateFlag || query === ""))
223
+ if (filteredList.length === 0 && (!enableCreateFlag || query === "")) {
224
+ if (formField == FormFieldType.TYPEAHEAD_2 && query === "")
225
+ return <></>;
220
226
  return (
221
227
  <div className="form-listbox-option text-center">
222
228
  <span className="empty-content text-gray-600 font-normal">
@@ -224,6 +230,7 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
224
230
  </span>
225
231
  </div>
226
232
  );
233
+ }
227
234
 
228
235
  return (
229
236
  <>