@engagebay/engagebay-form-module 1.0.5 → 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.5",
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;
@@ -133,7 +133,9 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
133
133
  }, [filteredList]);
134
134
 
135
135
  useEffect(() => {
136
- handleQueryCallback();
136
+ if (formField == FormFieldType.TYPEAHEAD_2)
137
+ queryCallback && queryCallback(query);
138
+ else handleQueryCallback();
137
139
  }, [query]);
138
140
 
139
141
  let enableCreateFlag =
@@ -143,6 +145,7 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
143
145
 
144
146
  let validTypeaheadFields = [
145
147
  FormFieldType.TYPEAHEAD,
148
+ FormFieldType.TYPEAHEAD_2,
146
149
  FormFieldType.TYPEAHEAD_MULTI_SELECT,
147
150
  ];
148
151
  let isTypeahead: boolean = validTypeaheadFields.indexOf(formField) > -1;
@@ -217,7 +220,9 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
217
220
  };
218
221
 
219
222
  const renderList = useMemo(() => {
220
- if (filteredList.length === 0 && (!enableCreateFlag || query === ""))
223
+ if (filteredList.length === 0 && (!enableCreateFlag || query === "")) {
224
+ if (formField == FormFieldType.TYPEAHEAD_2 && query === "")
225
+ return <></>;
221
226
  return (
222
227
  <div className="form-listbox-option text-center">
223
228
  <span className="empty-content text-gray-600 font-normal">
@@ -225,6 +230,7 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
225
230
  </span>
226
231
  </div>
227
232
  );
233
+ }
228
234
 
229
235
  return (
230
236
  <>