@engagebay/engagebay-form-module 1.0.2-beta.1 → 1.0.2-beta.10

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.2-beta.1",
3
+ "version": "1.0.2-beta.10",
4
4
  "description": "Provide base form components to reacho",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -16,7 +16,6 @@
16
16
  "@heroicons/react": ">=2.1.5",
17
17
  "@reduxjs/toolkit": ">=2.2.7",
18
18
  "@tippyjs/react": ">=4.2.6",
19
- "@types/lodash": ">=4.17.7",
20
19
  "@types/react-redux": ">=7.1.33",
21
20
  "axios": ">=1.7.2",
22
21
  "clsx": ">=2.1.1",
@@ -32,7 +31,6 @@
32
31
  "@heroicons/react": ">=2.1.5",
33
32
  "@reduxjs/toolkit": ">=2.2.7",
34
33
  "@tippyjs/react": ">=4.2.6",
35
- "@types/lodash": ">=4.17.7",
36
34
  "@types/react-redux": ">=7.1.33",
37
35
  "axios": ">=1.7.2",
38
36
  "clsx": ">=2.1.1",
package/src/api/index.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import axios, {AxiosInstance, AxiosRequestConfig} from "axios";
2
2
  import {FormFieldSchema} from "../form/schema/FormFieldSchema";
3
3
 
4
+ let baseURL;
5
+ try {
6
+ baseURL = (window as any).DEFAULT_HOST ||
7
+ (window as any).parent.DEFAULT_HOST
8
+ } catch (error) {
9
+
10
+ }
11
+
4
12
  const BASE_API: AxiosRequestConfig = {
5
- baseURL:
6
- (window as any).REACHO_BASE_URL ||
7
- (window as any).parent.REACHO_BASE_URL,
13
+ baseURL: baseURL ?? "",
8
14
  timeout: 30000,
9
15
  headers: {
10
16
  "Content-Type": "application/json",
@@ -34,6 +34,7 @@ import {
34
34
  FormFieldType,
35
35
  OptionMappingConfig,
36
36
  } from "./schema/FormFieldSchema";
37
+ import Typeahead2 from "./formfields/Typeahead2";
37
38
 
38
39
  /**
39
40
  * @property {React.FC<FormFieldComponentPropSchema>} component - React component for a form field.
@@ -139,6 +140,9 @@ const formFieldComponents: FormComponentSchema = {
139
140
  [FormFieldType[FormFieldType.TYPEAHEAD_MULTI_SELECT]]: {
140
141
  component: TypeaheadMultiSelect,
141
142
  },
143
+ [FormFieldType[FormFieldType.TYPEAHEAD_2]]: {
144
+ component: Typeahead2,
145
+ },
142
146
  [FormFieldType[FormFieldType.COMBO_SELECT]]: {
143
147
  component: ComboSelect,
144
148
  },
@@ -21,7 +21,6 @@ import FormField from "../FormField";
21
21
  import RenderFormField from "../util/RenderFormField";
22
22
  import { TrashIcon, XMarkIcon } from "@heroicons/react/24/outline";
23
23
  import Tippy from "@tippyjs/react";
24
- import { set } from "lodash";
25
24
 
26
25
  const defaultBusinessHours = {
27
26
  MONDAY: {
@@ -124,9 +123,8 @@ export const BusinessHoursField: React.FC<FormFieldComponentPropSchema> = (
124
123
  <div className="w-full sm:w-full text-end mr-[2.3em]">
125
124
  <button
126
125
  type="button"
127
- className={`text-end text-primary cursor-pointer font-[13px] font-medium ${
128
- getValues(`${mappedName}.sessions`)?.length > 1 ? "mr-9" : ""
129
- }`}
126
+ className={`text-end text-primary cursor-pointer font-[13px] font-medium ${getValues(`${mappedName}.sessions`)?.length > 1 ? "mr-9" : ""
127
+ }`}
130
128
  onClick={() => {
131
129
  const lastEndTime =
132
130
  getValues(`${mappedName}.sessions`)?.[
@@ -8,7 +8,7 @@ import { handleChange, registerFormField } from "../util";
8
8
  import moment from "moment";
9
9
 
10
10
  const DatePicker: React.FC<FormFieldComponentPropSchema> = (
11
- props: FormFieldComponentPropSchema
11
+ props: FormFieldComponentPropSchema,
12
12
  ) => {
13
13
  const formContext = useContext(FormContext);
14
14
  const initialDate = formContext.getValues(props.fieldConfig.name)
@@ -25,24 +25,38 @@ const DatePicker: React.FC<FormFieldComponentPropSchema> = (
25
25
  ) {
26
26
  formContext.setValue(
27
27
  props.fieldConfig.name,
28
- props.fieldConfig.defaultValue
28
+ props.fieldConfig.defaultValue,
29
29
  );
30
30
  }
31
31
  }, [props.fieldConfig.forceUpdate]);
32
32
 
33
33
  let registerOptions: RegisterOptions = registerFormField(props.fieldConfig);
34
-
34
+ //registerOptions.valueAsDate = true;
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,62 @@
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);
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
- }
13
+ let registerOptions: RegisterOptions = registerFormField(props.fieldConfig);
14
+ registerOptions.valueAsNumber = true;
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"
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
+ value = numericValue.toString();
52
+ handleChange(value, formContext, props.fieldConfig, props.onChange);
53
+ }}
54
+ />
59
55
  );
56
+ }
57
+
58
+ return (
59
+ <RenderFormField fieldConfig={props.fieldConfig} getInput={getInput} />
60
+ );
60
61
  };
61
62
  export default NumberField;
@@ -1,9 +1,9 @@
1
1
  import React, {
2
- useCallback,
3
- useContext,
4
- useEffect,
5
- useRef,
6
- useState,
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
7
  } from "react";
8
8
 
9
9
  import { Listbox, ListboxButton } from "@headlessui/react";
@@ -12,199 +12,185 @@ import { RegisterOptions } from "react-hook-form";
12
12
  import { FormContext } from "../context/FormContext";
13
13
  import { getListOption, getListOptions } from "../FormFieldUtils";
14
14
  import {
15
- FieldOptionsSchema,
16
- FormFieldComponentPropSchema,
17
- FormFieldType,
18
- OutputFormatType,
15
+ FieldOptionsSchema,
16
+ FormFieldComponentPropSchema,
17
+ FormFieldType,
18
+ OutputFormatType,
19
19
  } from "../schema/FormFieldSchema";
20
20
  import { handleChange, registerFormField } from "../util";
21
21
  import RenderFormField from "../util/RenderFormField";
22
22
  import RenderListOptions, {
23
- renderListBoxValue,
23
+ renderListBoxValue,
24
24
  } from "../util/RenderListOptions";
25
25
 
26
26
  const Typeahead: React.FC<FormFieldComponentPropSchema> = (
27
- props: FormFieldComponentPropSchema
27
+ props: FormFieldComponentPropSchema,
28
28
  ) => {
29
- const dynamicSelectRef = useRef<HTMLUListElement>(null);
30
- const formContext = useContext(FormContext);
31
- let registerOptions: RegisterOptions = registerFormField(props.fieldConfig);
32
- let hookProps = formContext.register(
33
- props.fieldConfig.name,
34
- registerOptions
35
- );
36
- const [listOptions, setListOptions] = useState<FieldOptionsSchema[]>([]);
37
- const [selectedValues, setSelectedValues] = useState<FieldOptionsSchema[]>(
38
- []
39
- );
40
- const [loading, setLoading] = useState<boolean>(true);
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
- fetchData(undefined);
53
- }, []);
29
+ const dynamicSelectRef = useRef<HTMLUListElement>(null);
30
+ const formContext = useContext(FormContext);
31
+ let registerOptions: RegisterOptions = registerFormField(props.fieldConfig);
32
+ let hookProps = formContext.register(props.fieldConfig.name, registerOptions);
33
+ const [listOptions, setListOptions] = useState<FieldOptionsSchema[]>([]);
34
+ const [selectedValues, setSelectedValues] = useState<FieldOptionsSchema[]>(
35
+ [],
36
+ );
37
+ const [loading, setLoading] = useState<boolean>(true);
54
38
 
55
- const fetchData = useCallback(
56
- async (_query: string | undefined) => {
57
- setLoading(true);
58
- try {
59
- if (!props.fieldConfig.fetchUrl) return;
39
+ useEffect(() => {
40
+ if (
41
+ !formContext.getValues(props.fieldConfig.name) &&
42
+ props.fieldConfig.defaultValue
43
+ ) {
44
+ formContext.setValue(
45
+ props.fieldConfig.name,
46
+ props.fieldConfig.defaultValue,
47
+ );
48
+ }
49
+ fetchData(undefined);
50
+ }, []);
60
51
 
61
- let url = props.fieldConfig.fetchUrl;
62
- if (_query) {
63
- url = url.includes("?")
64
- ? url + "&q=" + _query
65
- : url + "?q=" + _query;
66
- }
52
+ const fetchData = useCallback(
53
+ async (_query: string | undefined) => {
54
+ setLoading(true);
55
+ try {
56
+ if (!props.fieldConfig.fetchUrl) return;
67
57
 
68
- let response = await (props.fieldConfig.disableHeaderInFetch
69
- ? axios.get(url)
70
- : formContext.axiosInstance?.get(url));
71
- if (response?.data) {
72
- const data: FieldOptionsSchema[] = getListOptions(
73
- response.data,
74
- props.fieldConfig.optionsConfig
75
- );
76
- setListOptions([...data]);
77
- let value = formContext.getValues(props.fieldConfig.name);
78
- if (
79
- value &&
80
- data.find((i) => i.value !== value)
81
- ) {
82
- if (selectedValues.find((i) => i.value !== value))
83
- fetchValue(value);
84
- }
85
- if (props.fieldConfig.fetchCallback) {
86
- props.fieldConfig.fetchCallback(response);
87
- }
88
- } else {
89
- console.error(response?.statusText);
90
- }
91
- } catch (err) {
92
- } finally {
93
- setLoading(false);
94
- }
95
- },
96
- [props.fieldConfig.fetchUrl]
97
- );
58
+ let url = props.fieldConfig.fetchUrl;
59
+ if (_query) {
60
+ url = url.includes("?") ? url + "&q=" + _query : url + "?q=" + _query;
61
+ }
98
62
 
99
- const fetchValue = async (value: string) => {
100
- try {
101
- if (
102
- props.fieldConfig.ignoreFetchValue ||
103
- !props.fieldConfig.fetchUrl
104
- ) {
105
- return;
106
- }
63
+ let response = await (props.fieldConfig.disableHeaderInFetch
64
+ ? axios.get(url)
65
+ : formContext.axiosInstance?.get(url));
66
+ if (response?.data) {
67
+ const data: FieldOptionsSchema[] = getListOptions(
68
+ response.data,
69
+ props.fieldConfig.optionsConfig,
70
+ );
71
+ setListOptions([...data]);
72
+ let value = formContext.getValues(props.fieldConfig.name);
73
+ if (value && data.find((i) => i.value !== value)) {
74
+ if (selectedValues.find((i) => i.value !== value))
75
+ fetchValue(value);
76
+ }
77
+ if (props.fieldConfig.fetchCallback) {
78
+ props.fieldConfig.fetchCallback(response);
79
+ }
80
+ } else {
81
+ console.error(response?.statusText);
82
+ }
83
+ } catch (err) {
84
+ } finally {
85
+ setLoading(false);
86
+ }
87
+ },
88
+ [props.fieldConfig.fetchUrl],
89
+ );
107
90
 
108
- let url = props.fieldConfig.fetchUrl;
91
+ const fetchValue = async (value: string) => {
92
+ try {
93
+ if (props.fieldConfig.ignoreFetchValue || !props.fieldConfig.fetchUrl) {
94
+ return;
95
+ }
109
96
 
110
- if (value)
111
- url = url.includes("?")
112
- ? url + "&values=" + value
113
- : url + "?values=" + value;
97
+ let url = props.fieldConfig.fetchUrl;
114
98
 
115
- let response = await (
116
- props.fieldConfig.disableHeaderInFetch
117
- ? axios.get(url)
118
- : formContext.axiosInstance?.get(url)
119
- );
120
- if (response?.data) {
121
- const data: FieldOptionsSchema[] = getListOptions(
122
- response?.data,
123
- props.fieldConfig.optionsConfig
124
- );
125
- setSelectedValues([...data]);
126
- }
127
- } catch (err) { }
128
- };
99
+ if (value)
100
+ url = url.includes("?")
101
+ ? url + "&values=" + value
102
+ : url + "?values=" + value;
129
103
 
130
- const updateListOptions = (data: any) => {
131
- const resData: FieldOptionsSchema = getListOption(
132
- data,
133
- props.fieldConfig.optionsConfig
104
+ let response = await (props.fieldConfig.disableHeaderInFetch
105
+ ? axios.get(url)
106
+ : formContext.axiosInstance?.get(url));
107
+ if (response?.data) {
108
+ const data: FieldOptionsSchema[] = getListOptions(
109
+ response?.data,
110
+ props.fieldConfig.optionsConfig,
134
111
  );
135
- setSelectedValues((perv) => [resData]);
136
- formContext.setValue(props.fieldConfig.name, resData.value);
137
- };
112
+ setSelectedValues([...data]);
113
+ }
114
+ } catch (err) {}
115
+ };
138
116
 
139
- function getInput() {
140
- return (
141
- <Listbox
142
- as={"div"}
143
- {...hookProps}
144
- className={`relative form-listbox flex-1`}
145
- value={
146
- formContext.getValues(props.fieldConfig.name)
147
- ? props.fieldConfig.outputFormat ===
148
- OutputFormatType.ARRAY
149
- ? formContext.getValues(props.fieldConfig.name)[0]
150
- : formContext.getValues(props.fieldConfig.name)
151
- : undefined
152
- }
153
- onChange={(val) => {
154
- const currentValue = formContext.getValues(
155
- props.fieldConfig.name
156
- );
117
+ const updateListOptions = (data: any) => {
118
+ const resData: FieldOptionsSchema = getListOption(
119
+ data,
120
+ props.fieldConfig.optionsConfig,
121
+ );
122
+ setSelectedValues((perv) => [resData]);
123
+ formContext.setValue(props.fieldConfig.name, resData.value);
124
+ };
157
125
 
158
- // If the value matches, set it to null, otherwise set it to val
159
- const newValue = currentValue === val ? null : val;
126
+ function getInput() {
127
+ return (
128
+ <Listbox
129
+ as={"div"}
130
+ {...hookProps}
131
+ className={`relative form-listbox flex-1`}
132
+ value={
133
+ formContext.getValues(props.fieldConfig.name)
134
+ ? props.fieldConfig.outputFormat === OutputFormatType.ARRAY
135
+ ? formContext.getValues(props.fieldConfig.name)[0]
136
+ : formContext.getValues(props.fieldConfig.name)
137
+ : undefined
138
+ }
139
+ onChange={(val) => {
140
+ const currentValue = formContext.getValues(props.fieldConfig.name);
160
141
 
161
- const selected = listOptions.find(o => o.value == newValue);
162
- selected && setSelectedValues([selected]);
163
- handleChange(
164
- newValue,
165
- formContext,
166
- props.fieldConfig,
167
- props.onChange
168
- );
169
- }}
170
- name={props.fieldConfig.name}
171
- disabled={props.fieldConfig.disabled}>
172
- <ListboxButton
173
- className={
174
- props.fieldConfig.customClassNames?.fieldClassName
175
- ? "form-listbox-select " +
176
- props.fieldConfig.customClassNames?.fieldClassName
177
- : "form-listbox-select"
178
- }>
179
- {renderListBoxValue(
180
- formContext,
181
- props.fieldConfig,
182
- [...selectedValues, ...listOptions],
183
- props.onChange
184
- )}
185
- </ListboxButton>
186
- <RenderListOptions
187
- formContext={formContext}
188
- onChange={props.onChange}
189
- formField={FormFieldType.TYPEAHEAD}
190
- ref={dynamicSelectRef}
191
- fieldConfig={props.fieldConfig}
192
- listOptions={[...selectedValues, ...listOptions]}
193
- setListOptions={setListOptions}
194
- loading={loading}
195
- setLoading={setLoading}
196
- createCallback={(data) => updateListOptions(data)}
197
- queryCallback={(query) => fetchData(query)}
198
- />
199
- </Listbox>
200
- );
201
- }
202
- if (props.fieldConfig.hideWhenNoResults && listOptions.length == 0) {
203
- return <></>;
204
- }
142
+ // If the value matches, set it to null, otherwise set it to val
143
+ const newValue = currentValue === val ? null : val;
205
144
 
206
- return (
207
- <RenderFormField fieldConfig={props.fieldConfig} getInput={getInput} />
145
+ const selected = listOptions.find((o) => o.value == newValue);
146
+ selected && setSelectedValues([selected]);
147
+ handleChange(
148
+ newValue,
149
+ formContext,
150
+ props.fieldConfig,
151
+ props.onChange,
152
+ );
153
+ }}
154
+ name={props.fieldConfig.name}
155
+ disabled={props.fieldConfig.disabled}
156
+ >
157
+ <ListboxButton
158
+ className={
159
+ props.fieldConfig.customClassNames?.fieldClassName
160
+ ? "form-listbox-select " +
161
+ props.fieldConfig.customClassNames?.fieldClassName
162
+ : "form-listbox-select"
163
+ }
164
+ >
165
+ {renderListBoxValue(
166
+ formContext,
167
+ props.fieldConfig,
168
+ [...selectedValues, ...listOptions],
169
+ props.onChange,
170
+ )}
171
+ </ListboxButton>
172
+ <RenderListOptions
173
+ formContext={formContext}
174
+ onChange={props.onChange}
175
+ formField={FormFieldType.TYPEAHEAD}
176
+ ref={dynamicSelectRef}
177
+ fieldConfig={props.fieldConfig}
178
+ listOptions={[...selectedValues, ...listOptions]}
179
+ setListOptions={setListOptions}
180
+ loading={loading}
181
+ setLoading={setLoading}
182
+ createCallback={(data) => updateListOptions(data)}
183
+ queryCallback={(query) => fetchData(query)}
184
+ />
185
+ </Listbox>
208
186
  );
187
+ }
188
+ if (props.fieldConfig.hideWhenNoResults && listOptions.length == 0) {
189
+ return <></>;
190
+ }
191
+
192
+ return (
193
+ <RenderFormField fieldConfig={props.fieldConfig} getInput={getInput} />
194
+ );
209
195
  };
210
196
  export default Typeahead;
@@ -0,0 +1,283 @@
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
+
89
+ if (!_query || (props.fieldConfig.allowedMinQueryLength && _query.length < props.fieldConfig.allowedMinQueryLength) || _query.length < 3) {
90
+ setListOptions([]);
91
+ setLoading(false);
92
+ return;
93
+ }
94
+ setLoading(true);
95
+ try {
96
+ if (!props.fieldConfig.fetchUrl) return;
97
+
98
+ let url = props.fieldConfig.fetchUrl;
99
+
100
+ url = url.includes("?") ? url + "&q=" + _query : url + "?q=" + _query;
101
+
102
+ const axiosInstance = getAxiosInstance(
103
+ formContext.axiosInstance,
104
+ props.fieldConfig,
105
+ );
106
+ const response = await axiosInstance.get(url);
107
+
108
+ if (controller === abortControllerRef.current) {
109
+ if (response?.data) {
110
+ const data: FieldOptionsSchema[] = getListOptions(
111
+ response?.data,
112
+ props.fieldConfig.optionsConfig,
113
+ );
114
+ setListOptions([...data]);
115
+
116
+ if (props.fieldConfig.fetchCallback) {
117
+ props.fieldConfig.fetchCallback(response);
118
+ }
119
+ }
120
+ }
121
+ } catch (err) {
122
+ console.error("Fetch error:", err);
123
+ } finally {
124
+ // Only stop loading if this was the "active" request
125
+ if (controller === abortControllerRef.current) {
126
+ setLoading(false);
127
+ }
128
+ }
129
+ }, 250);
130
+ };
131
+ }, []);
132
+
133
+ // Cleanup on unmount
134
+ useEffect(() => {
135
+ return () => {
136
+ abortControllerRef.current?.abort();
137
+ };
138
+ }, []);
139
+
140
+ const fetchValue = async (value: string | any | any[]) => {
141
+ try {
142
+ if (
143
+ props.fieldConfig.ignoreFetchValue ||
144
+ !props.fieldConfig.fetchSavedDataUrl
145
+ ) {
146
+ return;
147
+ }
148
+
149
+ let url = props.fieldConfig.fetchSavedDataUrl;
150
+
151
+ // url = url.includes("?")
152
+ // ? url + "&values=" + value
153
+ // : url + "?values=" + value;
154
+
155
+ let response = await (props.fieldConfig.disableHeaderInFetch
156
+ ? axios.post(url, Array.isArray(value) ? value : [value])
157
+ : formContext.axiosInstance?.post(
158
+ url,
159
+ Array.isArray(value) ? value : [value],
160
+ ));
161
+ if (response?.data) {
162
+ const data: FieldOptionsSchema[] = getListOptions(
163
+ response?.data,
164
+ props.fieldConfig.optionsConfig,
165
+ );
166
+ let values: any[] = formContext.getValues(props.fieldConfig.name) || [];
167
+ setSelectedValues(
168
+ [
169
+ ...data,
170
+ props.fieldConfig.dropdownFieldConfig?.isSuggestionBox &&
171
+ values &&
172
+ Array.isArray(values)
173
+ ? values
174
+ .filter(
175
+ (value) =>
176
+ data.some((d) => d.value !== value) || data.length == 0,
177
+ )
178
+ .map((val) => ({ label: val, value: val }))
179
+ : [],
180
+ ].flat(),
181
+ );
182
+ }
183
+ } catch (err) { }
184
+ };
185
+
186
+ const updateListOptions = (data: any) => {
187
+ const resData: FieldOptionsSchema = getListOption(
188
+ data,
189
+ props.fieldConfig.optionsConfig,
190
+ );
191
+ setListOptions([]);
192
+ setSelectedValues((prev) =>
193
+ props.fieldConfig.isMultiple ? [...prev, resData] : [resData],
194
+ );
195
+ let result = formContext.getValues(props.fieldConfig.name) || [];
196
+ result = !props.fieldConfig.isMultiple
197
+ ? resData.value
198
+ : result.includes(resData.value)
199
+ ? [...result.filter((v: string) => v != resData.value), resData.value]
200
+ : [...result, resData.value];
201
+ formContext.setValue(props.fieldConfig.name, result);
202
+ };
203
+
204
+ const getInput = () => {
205
+ return (
206
+ <Listbox
207
+ as={"div"}
208
+ {...hookProps}
209
+ value={
210
+ props.fieldConfig.isMultiple
211
+ ? formContext.getValues(props.fieldConfig.name)
212
+ ? formContext.getValues(props.fieldConfig.name)
213
+ : []
214
+ : formContext.getValues(props.fieldConfig.name)
215
+ }
216
+ name={props.fieldConfig.name}
217
+ defaultValue={props.fieldConfig.defaultValue}
218
+ key={props.fieldConfig.name}
219
+ className={"relative form-listbox flex-1 overflow-hidden"}
220
+ onChange={(selectedOptions) => {
221
+ let currentValue = formContext.getValues(props.fieldConfig.name);
222
+ const newValue =
223
+ currentValue === selectedOptions ? null : selectedOptions;
224
+
225
+ if (props.fieldConfig.isMultiple) {
226
+ currentValue = listOptions.filter((op) =>
227
+ selectedOptions.includes(op.value),
228
+ );
229
+ setSelectedValues((prev) => [...prev, ...currentValue]);
230
+ } else {
231
+ const selected = listOptions.find((o) => o.value == newValue);
232
+ selected && setSelectedValues([selected]);
233
+ }
234
+ setListOptions([]);
235
+ handleChange(
236
+ newValue,
237
+ formContext,
238
+ props.fieldConfig,
239
+ props.onChange,
240
+ );
241
+ }}
242
+ multiple={props.fieldConfig.isMultiple}
243
+ >
244
+ <ListboxButton
245
+ className={
246
+ props.fieldConfig.customClassNames?.fieldClassName
247
+ ? "form-listbox-select " +
248
+ props.fieldConfig.customClassNames?.fieldClassName
249
+ : "form-listbox-select"
250
+ }
251
+ >
252
+ {renderListBoxValue(
253
+ formContext,
254
+ props.fieldConfig,
255
+ [...selectedValues, ...listOptions],
256
+ props.onChange,
257
+ )}
258
+ </ListboxButton>
259
+ <RenderListOptions
260
+ formContext={formContext}
261
+ onChange={props.onChange}
262
+ formField={FormFieldType.TYPEAHEAD_2}
263
+ ref={dynamicSelectRef}
264
+ fieldConfig={props.fieldConfig}
265
+ listOptions={listOptions}
266
+ setListOptions={setListOptions}
267
+ loading={loading}
268
+ setLoading={setLoading}
269
+ createCallback={(data) => updateListOptions(data)}
270
+ queryCallback={(query) => fetchData(query)}
271
+ />
272
+ </Listbox>
273
+ );
274
+ };
275
+ if (props.fieldConfig.hideWhenNoResults && listOptions.length == 0) {
276
+ return <></>;
277
+ }
278
+
279
+ return (
280
+ <RenderFormField fieldConfig={props.fieldConfig} getInput={getInput} />
281
+ );
282
+ };
283
+ export default Typeahead2;
@@ -23,7 +23,7 @@ import RenderListOptions, {
23
23
  } from "../util/RenderListOptions";
24
24
  // import _ from "lodash";
25
25
  import axios from "axios";
26
- import { getAxiosInstance } from "src/api";
26
+ import { getAxiosInstance } from "../../api";
27
27
 
28
28
  const TypeaheadMultiSelect: React.FC<FormFieldComponentPropSchema> = (
29
29
  props: FormFieldComponentPropSchema,
@@ -108,7 +108,7 @@ const TypeaheadMultiSelect: React.FC<FormFieldComponentPropSchema> = (
108
108
 
109
109
  let url = props.fieldConfig.fetchUrl;
110
110
 
111
- url = url = url.includes("?")
111
+ url = url.includes("?")
112
112
  ? url + "&values=" + value
113
113
  : url + "?values=" + value;
114
114
 
@@ -126,16 +126,16 @@ const TypeaheadMultiSelect: React.FC<FormFieldComponentPropSchema> = (
126
126
  ...data,
127
127
  props.fieldConfig.dropdownFieldConfig?.isSuggestionBox && values
128
128
  ? values
129
- .filter(
130
- (value) =>
131
- data.some((d) => d.value !== value) || data.length == 0,
132
- )
133
- .map((val) => ({ label: val, value: val }))
129
+ .filter(
130
+ (value) =>
131
+ data.some((d) => d.value !== value) || data.length == 0,
132
+ )
133
+ .map((val) => ({ label: val, value: val }))
134
134
  : [],
135
135
  ].flat(),
136
136
  );
137
137
  }
138
- } catch (err) {}
138
+ } catch (err) { }
139
139
  };
140
140
 
141
141
  const updateListOptions = (data: any) => {
@@ -179,7 +179,7 @@ const TypeaheadMultiSelect: React.FC<FormFieldComponentPropSchema> = (
179
179
  className={
180
180
  props.fieldConfig.customClassNames?.fieldClassName
181
181
  ? "form-listbox-select " +
182
- props.fieldConfig.customClassNames?.fieldClassName
182
+ props.fieldConfig.customClassNames?.fieldClassName
183
183
  : "form-listbox-select"
184
184
  }
185
185
  >
@@ -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",
@@ -129,10 +130,13 @@ export type FormFieldSchema = {
129
130
  submitOnChange?: boolean;
130
131
  formFieldPattern?: FormFieldPatternsImpl[];
131
132
  fetchUrl?: string;
133
+ fetchSavedDataUrl?: string;
132
134
  postUrl?: string;
133
135
  fileAccept?: string;
134
136
  icon?: ReactNode;
135
137
  outputFormat?: OutputFormatType;
138
+ isMultiple?: boolean;
139
+ allowedMinQueryLength?: number;
136
140
  children?: FormFieldSchema[];
137
141
  defaultOptions?: FieldOptionsSchema[];
138
142
  optionsConfig?: OptionMappingConfig;
@@ -201,6 +205,7 @@ export type FieldOptionsSchema = {
201
205
  isDisabled?: boolean;
202
206
  helpText?: string;
203
207
  groupName?: string;
208
+ tooltip?: string;
204
209
  icon?: React.ReactNode;
205
210
  };
206
211
 
@@ -17,6 +17,7 @@ import {
17
17
  FormFieldType,
18
18
  OutputFormatType,
19
19
  } from "../schema/FormFieldSchema";
20
+ import Tippy from "@tippyjs/react";
20
21
 
21
22
  type RenderListOptionsProps = {
22
23
  formContext: FormContextType;
@@ -45,7 +46,7 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
45
46
  queryCallback,
46
47
  createCallback,
47
48
  },
48
- ref
49
+ ref,
49
50
  ) => {
50
51
  const [query, setQuery] = useState<string>("");
51
52
  const [createdListItems, setCreatedListItems] = useState<
@@ -90,7 +91,7 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
90
91
  .filter((result) => result.label)
91
92
  .filter(
92
93
  (option, index, self) =>
93
- index === self.findIndex((obj) => obj.value === option.value)
94
+ index === self.findIndex((obj) => obj.value === option.value),
94
95
  ),
95
96
  ]; // removing duplicate values
96
97
 
@@ -101,15 +102,15 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
101
102
 
102
103
  const filteredList = query
103
104
  ? resultList.filter((item) => {
104
- const normalizedLabel = fieldConfig.dropdownFieldConfig
105
- ?.isCaseSensitive
106
- ? item.label
107
- : item.label.toLowerCase();
105
+ const normalizedLabel = fieldConfig.dropdownFieldConfig
106
+ ?.isCaseSensitive
107
+ ? item.label
108
+ : item.label.toLowerCase();
108
109
 
109
- return normalizedLabel
110
- .replace(/\s+/g, "")
111
- .includes(caseSensitive.replace(/\s+/g, ""));
112
- })
110
+ return normalizedLabel
111
+ .replace(/\s+/g, "")
112
+ .includes(caseSensitive.replace(/\s+/g, ""));
113
+ })
113
114
  : resultList;
114
115
 
115
116
  let nullGroupOptions: any[] = [];
@@ -126,13 +127,15 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
126
127
  }, {});
127
128
 
128
129
  const handleQueryCallback = useCallback(() => {
129
- if (filteredList.length == 0 && isTypeahead) {
130
+ if (filteredList.length < 5 && isTypeahead) {
130
131
  queryCallback && queryCallback(query);
131
132
  }
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;
@@ -165,11 +169,10 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
165
169
  key={option.value}
166
170
  disabled={option.isDisabled}
167
171
  onClick={() => setTimeout(resetToDefault, 300)}
168
- className={`form-listbox-option ${
169
- selected
170
- ? " bg-gray-100 text-gray-900"
171
- : "hover:bg-gray-100 text-gray-700"
172
- }`}
172
+ className={`form-listbox-option ${selected
173
+ ? " bg-gray-100 text-gray-900"
174
+ : "hover:bg-gray-100 text-gray-700"
175
+ }`}
173
176
  value={option.value}
174
177
  >
175
178
  {fieldConfig.fieldOptionWrapper ? (
@@ -178,17 +181,30 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
178
181
  <>
179
182
  <div className="flex items-center justify-between gap-x-1.5">
180
183
  <span
181
- className={`block truncate w-full !max-w-[150px] space-x-2 ${
182
- selected ? "font-medium" : "font-normal"
183
- }`}
184
+ className={`block truncate w-full ${fieldConfig.customClassNames?.optionClassName} space-x-2 ${selected ? "font-medium" : "font-normal"
185
+ }`}
184
186
  >
185
187
  {option.icon && (
186
188
  <span className="listbox-svg">{option.icon}</span>
187
189
  )}
188
- {option.label}
190
+ {option.tooltip ? (
191
+ <Tippy
192
+ content={
193
+ <>
194
+ {option.label}
195
+ <br />
196
+ {option.helpText}
197
+ </>
198
+ }
199
+ >
200
+ <div className="truncate">{option.label}</div>
201
+ </Tippy>
202
+ ) : (
203
+ option.label
204
+ )}
189
205
  </span>
190
206
  {isTypeahead &&
191
- !fieldConfig.dropdownFieldConfig?.isSuggestionBox ? (
207
+ !fieldConfig.dropdownFieldConfig?.isSuggestionBox ? (
192
208
  <input
193
209
  type="checkbox"
194
210
  className="form-checkbox"
@@ -204,11 +220,30 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
204
220
  )
205
221
  )}
206
222
  </div>
207
- {option.helpText && (
208
- <div className="mt-0 text-xs text-gray-500 font-normal truncate w-full !max-w-[150px]">
209
- {option.helpText}
210
- </div>
211
- )}
223
+ {option.helpText &&
224
+ (option.tooltip ? (
225
+ <Tippy
226
+ content={
227
+ <>
228
+ {option.label}
229
+ <br />
230
+ {option.helpText}
231
+ </>
232
+ }
233
+ >
234
+ <div
235
+ className={`mt-0 text-xs text-gray-500 font-normal truncate w-full ${fieldConfig.customClassNames?.optionClassName}`}
236
+ >
237
+ {option.helpText}
238
+ </div>
239
+ </Tippy>
240
+ ) : (
241
+ <div
242
+ className={`mt-0 text-xs text-gray-500 font-normal truncate w-full ${fieldConfig.customClassNames?.optionClassName}`}
243
+ >
244
+ {option.helpText}
245
+ </div>
246
+ ))}
212
247
  </>
213
248
  )}
214
249
  </ListboxOption>
@@ -318,7 +353,7 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
318
353
  </div>
319
354
  </ListboxOptions>
320
355
  );
321
- }
356
+ },
322
357
  );
323
358
 
324
359
  export default RenderListOptions;
@@ -326,7 +361,7 @@ export function renderListBoxValue(
326
361
  formContext: FormContextType,
327
362
  fieldConfig: FormFieldSchema,
328
363
  listOptions: FieldOptionsSchema[],
329
- onChange?: (value: any) => void
364
+ onChange?: (value: any) => void,
330
365
  ): JSX.Element {
331
366
  let value = formContext.getValues(fieldConfig.name);
332
367
  const renderAsString = () => {
@@ -338,10 +373,17 @@ export function renderListBoxValue(
338
373
  return icon ? (
339
374
  <span className="flex items-center fs-8 whitespace-nowrap text-gray-500">
340
375
  <span>{icon}</span>
341
- <span>{label} </span>
376
+
377
+ <Tippy content={label} delay={500}>
378
+ <span className="block truncate">{label}</span>
379
+ </Tippy>
342
380
  </span>
381
+ ) : label ? (
382
+ <Tippy content={<>{label}</>} delay={500}>
383
+ <span className="block truncate">{label}</span>
384
+ </Tippy>
343
385
  ) : (
344
- label || getPlaceholder()
386
+ getPlaceholder()
345
387
  );
346
388
  };
347
389
  const renderAsArray = () => {
@@ -358,23 +400,28 @@ export function renderListBoxValue(
358
400
  {values.length > 1
359
401
  ? `${values.length} selected`
360
402
  : listOptions.find((option) => option.value == value[0])?.label ||
361
- values[0]}
403
+ values[0]}
362
404
  </span>
363
405
  {getDeleteButton()}
364
406
  </span>
365
407
  ) : (
366
408
  Array.isArray(values) &&
367
- values.map((opt: any) => {
368
- const option = listOptions.find((option) => option.value == opt);
369
- if (!option && values.length == 0) return getPlaceholder();
409
+ values.map((opt: any) => {
410
+ const option = listOptions.find((option) => option.value == opt);
411
+ if (!option && values.length == 0) return getPlaceholder();
370
412
 
371
- return (
372
- <span key={option?.value} className="form-selected-badge">
373
- <span className="form-selected-badge-name">{option?.label}</span>
374
- {getDeleteButton(opt)}
375
- </span>
376
- );
377
- })
413
+ return (
414
+ <span key={option?.value} className="form-selected-badge">
415
+ <Tippy content={option?.label} delay={500}>
416
+ <span className="form-selected-badge-name">
417
+ {option?.label}
418
+ </span>
419
+ </Tippy>
420
+
421
+ {getDeleteButton(opt)}
422
+ </span>
423
+ );
424
+ })
378
425
  );
379
426
  };
380
427
  const getDeleteButton = (option?: string) => {
@@ -406,9 +453,10 @@ export function renderListBoxValue(
406
453
  )
407
454
  );
408
455
  };
409
- let outputFormat = fieldConfig.outputFormat
410
- ? fieldConfig.outputFormat === OutputFormatType.ARRAY
411
- : false;
456
+ let outputFormat =
457
+ fieldConfig.outputFormat != undefined
458
+ ? fieldConfig.outputFormat === OutputFormatType.STRING
459
+ : false;
412
460
  const getPlaceholder = () => (
413
461
  <span className="form-placeholder">
414
462
  {fieldConfig.placeholder || "Select any option"}