@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.
Files changed (50) hide show
  1. package/README.md +126 -0
  2. package/link.sh +2 -0
  3. package/package.json +30 -0
  4. package/src/api/index.ts +25 -0
  5. package/src/form/Form.tsx +157 -0
  6. package/src/form/FormField.tsx +80 -0
  7. package/src/form/FormFieldUtils.ts +241 -0
  8. package/src/form/FormFields.tsx +41 -0
  9. package/src/form/context/FormContext.tsx +66 -0
  10. package/src/form/formfields/ArrayField.tsx +169 -0
  11. package/src/form/formfields/BusinessHoursField.tsx +204 -0
  12. package/src/form/formfields/CheckboxButtonsField.tsx +97 -0
  13. package/src/form/formfields/CheckboxField.tsx +118 -0
  14. package/src/form/formfields/ColorPickerField.tsx +59 -0
  15. package/src/form/formfields/ComboMultiSelect.tsx +290 -0
  16. package/src/form/formfields/ComboSelect.tsx +278 -0
  17. package/src/form/formfields/DatePickerField.tsx +89 -0
  18. package/src/form/formfields/DateRangePickerField.tsx +104 -0
  19. package/src/form/formfields/DynamicMultiSelect.tsx +189 -0
  20. package/src/form/formfields/DynamicSelect.tsx +187 -0
  21. package/src/form/formfields/Error.tsx +15 -0
  22. package/src/form/formfields/ErrorContextHandler.tsx +77 -0
  23. package/src/form/formfields/FileUploadField.tsx +196 -0
  24. package/src/form/formfields/IframeField.tsx +65 -0
  25. package/src/form/formfields/InputField.tsx +67 -0
  26. package/src/form/formfields/InputGroupField.tsx +44 -0
  27. package/src/form/formfields/MultipleSelectField.tsx +98 -0
  28. package/src/form/formfields/NumberField.tsx +61 -0
  29. package/src/form/formfields/PasswordField.tsx +93 -0
  30. package/src/form/formfields/PhoneNumberField.tsx +163 -0
  31. package/src/form/formfields/RadioField.tsx +104 -0
  32. package/src/form/formfields/RadioGroupComponent.tsx +94 -0
  33. package/src/form/formfields/RangeField.tsx +53 -0
  34. package/src/form/formfields/SelectField.tsx +82 -0
  35. package/src/form/formfields/SwitchField.tsx +131 -0
  36. package/src/form/formfields/TextAreaField.tsx +48 -0
  37. package/src/form/formfields/TimeField.tsx +53 -0
  38. package/src/form/formfields/Typeahead.tsx +211 -0
  39. package/src/form/formfields/TypeaheadMultiSelect.tsx +203 -0
  40. package/src/form/formfields/UrlField.tsx +53 -0
  41. package/src/form/hooks/useDynamicReducer.tsx +42 -0
  42. package/src/form/schema/CustomValidators.ts +63 -0
  43. package/src/form/schema/FormFieldSchema.ts +342 -0
  44. package/src/form/util/RenderFormField.tsx +149 -0
  45. package/src/form/util/RenderListOptions.tsx +424 -0
  46. package/src/form/util/index.ts +185 -0
  47. package/src/util/LoaderWithText.tsx +28 -0
  48. package/src/util/svg/HELPER_ICONS.ts +16 -0
  49. package/src/util/svg/SVGIcon.tsx +23 -0
  50. package/tsconfig.json +25 -0
@@ -0,0 +1,41 @@
1
+ import React, { useContext } from "react";
2
+ import { FormContext } from "./context/FormContext";
3
+ import FormField from "./FormField";
4
+
5
+ /**
6
+ * FormFields Component
7
+ *
8
+ * The FormFields component maps over a list of field names and renders
9
+ * corresponding FormField components based on the provided form context.
10
+ *
11
+ * @param {Object} props - The component props.
12
+ * @param {string[]} props.fields - An array of field names to render.
13
+ * @returns {JSX.Element} The rendered FormFields component.
14
+ */
15
+ export default function FormFields({
16
+ fields,
17
+ }: {
18
+ fields: string[];
19
+ }): JSX.Element {
20
+ // Get context from FormContext
21
+ let formContext = useContext(FormContext);
22
+
23
+ return (
24
+ <>
25
+ {/* Map over the field names and render FormField components */}
26
+ {fields.map((fieldName, index) => {
27
+ // Find the field configuration from the form context
28
+ let fieldConfig = formContext.formFields.find((fieldInfo) => {
29
+ return fieldInfo.name == fieldName;
30
+ });
31
+
32
+ // Render the FormField component if fieldConfig is found, else render an empty fragment
33
+ return fieldConfig ? (
34
+ <FormField key={index} fieldConfig={fieldConfig} />
35
+ ) : (
36
+ <></>
37
+ );
38
+ })}
39
+ </>
40
+ );
41
+ }
@@ -0,0 +1,66 @@
1
+ import { createContext } from "react";
2
+ import {
3
+ Control,
4
+ FieldErrors,
5
+ UseFormClearErrors,
6
+ UseFormGetFieldState,
7
+ UseFormGetValues,
8
+ UseFormHandleSubmit,
9
+ UseFormRegister,
10
+ UseFormReset,
11
+ UseFormResetField,
12
+ UseFormSetError,
13
+ UseFormSetFocus,
14
+ UseFormSetValue,
15
+ UseFormTrigger,
16
+ UseFormUnregister,
17
+ UseFormWatch,
18
+ } from "react-hook-form";
19
+ import { FormFieldSchema } from "../schema/FormFieldSchema";
20
+
21
+ export interface FormContextType {
22
+ formFields: FormFieldSchema[];
23
+ onSubmit?: (e?: any) => void;
24
+ register: UseFormRegister<any>;
25
+ control?: Control<any, any>;
26
+ setValue: UseFormSetValue<any>;
27
+ errors?: FieldErrors<any>;
28
+ getValues: UseFormGetValues<any>;
29
+ reset: UseFormReset<any>;
30
+ watch: UseFormWatch<any>;
31
+ // updateField: (context: any) => void
32
+ // getValues?: UseFormGetValues<TFieldValues>;
33
+ getFieldState: UseFormGetFieldState<any>;
34
+ setError: UseFormSetError<any>;
35
+ clearErrors?: UseFormClearErrors<any>;
36
+ // setValue: UseFormSetValue<TFieldValues>;
37
+ trigger: UseFormTrigger<any>;
38
+ // formState: FormState<TFieldValues>;
39
+ resetField: UseFormResetField<any>;
40
+ // reset: UseFormReset<TFieldValues>;
41
+ handleSubmit?: UseFormHandleSubmit<any, any>;
42
+ unregister: UseFormUnregister<any>;
43
+ // control: Control<TFieldValues, TContext>;
44
+ // register: UseFormRegister<TFieldValues>;
45
+ setFocus: UseFormSetFocus<any>;
46
+ }
47
+
48
+ export const FormContext = createContext({
49
+ formFields: [],
50
+ // onSubmit: (undefined) => void,
51
+ register: {} as UseFormRegister<{}>,
52
+ getValues: {} as UseFormGetValues<{}>,
53
+ setValue: {} as UseFormSetValue<any>,
54
+ watch: {} as UseFormWatch<any>,
55
+ reset: {} as UseFormReset<any>,
56
+ resetField: {} as UseFormResetField<any>,
57
+ getFieldState: {} as UseFormGetFieldState<any>,
58
+ setError: {} as UseFormSetError<any>,
59
+ unregister: {} as UseFormUnregister<any>,
60
+ // updateField: (context: any) => { },
61
+ // control: undefined,
62
+ // setValue: [],
63
+ setFocus: {} as UseFormSetFocus<any>,
64
+ trigger: {} as UseFormTrigger<any>,
65
+ // errors: []
66
+ } as FormContextType);
@@ -0,0 +1,169 @@
1
+ import { useContext, useEffect } from "react";
2
+ import { FieldArrayMethodProps, FieldValues, useFieldArray, UseFieldArrayAppend, UseFieldArrayPrepend, UseFieldArrayRemove } from "react-hook-form";
3
+ import FormField from "../FormField";
4
+ import { FormContext } from "../context/FormContext";
5
+ import { FormFieldComponentPropSchema } from "../schema/FormFieldSchema";
6
+ import React from "react";
7
+
8
+ const ArrayField: React.FC<FormFieldComponentPropSchema> = ({
9
+ fieldConfig,
10
+ }: FormFieldComponentPropSchema) => {
11
+ let {
12
+ formFields,
13
+ getFieldState,
14
+ getValues,
15
+ register,
16
+ reset,
17
+ resetField,
18
+ setError,
19
+ setFocus,
20
+ setValue,
21
+ trigger,
22
+ unregister,
23
+ watch,
24
+ clearErrors,
25
+ control,
26
+ errors,
27
+ onSubmit,
28
+ } = useContext(FormContext);
29
+ useEffect(() => {
30
+ if(fieldConfig.defaultValue && !getValues(fieldConfig.name)){
31
+ append(fieldConfig.defaultValue);
32
+ }
33
+ }, []);
34
+
35
+ const { fields, append, prepend, remove, swap, move, insert, replace } =
36
+ useFieldArray({
37
+ control: control,
38
+ name: fieldConfig.name,
39
+ rules: {
40
+ minLength: fieldConfig.minLength,
41
+ maxLength: fieldConfig.maxLength,
42
+ required: fieldConfig.required,
43
+ validate: fieldConfig.formFieldPattern
44
+ ? fieldConfig?.formFieldPattern[0].getValidate()
45
+ : null,
46
+ },
47
+ });
48
+
49
+ const removeField: UseFieldArrayRemove = (
50
+ index: number | number[] | undefined
51
+ ) => {
52
+ remove(index);
53
+ reset(getValues());
54
+
55
+ if (fieldConfig.submitOnChange) {
56
+ onSubmit && onSubmit();
57
+ }
58
+ };
59
+
60
+ const appendField: UseFieldArrayAppend<any> = (
61
+ value,
62
+ options?: FieldArrayMethodProps
63
+ ) => {
64
+ append(value, options);
65
+ reset(getValues());
66
+
67
+ if (fieldConfig.submitOnChange) {
68
+ onSubmit && onSubmit();
69
+ }
70
+ };
71
+
72
+ const prependField: UseFieldArrayPrepend<any> = (
73
+ value,
74
+ options?: FieldArrayMethodProps
75
+ ) => {
76
+ prepend(value, options);
77
+ reset(getValues());
78
+
79
+ if (fieldConfig.submitOnChange) {
80
+ onSubmit && onSubmit();
81
+ }
82
+ };
83
+
84
+ const getObjectField = (objectIndex: number) => {
85
+ return fieldConfig.children?.map((child) => {
86
+ let childConfig = { ...child };
87
+ childConfig.name =
88
+ fieldConfig.name + "." + objectIndex + "." + child.name;
89
+
90
+ return <FormField fieldConfig={childConfig}></FormField>;
91
+ });
92
+ };
93
+
94
+ const childByFieldName = (
95
+ objectIndex: number,
96
+ fieldName: string,
97
+ mapperType?: string
98
+ ) => {
99
+ if (!fieldConfig.children || fieldConfig.children.length == 0)
100
+ return null;
101
+
102
+ for (const child of fieldConfig.children) {
103
+ if (
104
+ (mapperType &&
105
+ child.name == fieldName &&
106
+ child.mapperType == mapperType) ||
107
+ (!mapperType && child.name == fieldName)
108
+ ) {
109
+ let childConfig = { ...child };
110
+ childConfig.name =
111
+ fieldConfig.name + "." + objectIndex + "." + child.name;
112
+ return <FormField fieldConfig={childConfig}></FormField>;
113
+ }
114
+ }
115
+ return null;
116
+ };
117
+
118
+ const getArrayfields = () => {
119
+ return (
120
+ fields &&
121
+ fields.map((field, index) => {
122
+ return fieldConfig.arrayIndexWrapper ? (
123
+ <fieldConfig.arrayIndexWrapper
124
+ key={field.id}
125
+ {...{
126
+ append: appendField,
127
+ prepend: prependField,
128
+ remove: removeField,
129
+ index,
130
+ getValues,
131
+ setValue,
132
+ length: fields.length,
133
+ mappedName: fieldConfig.name + "." + index,
134
+ childByFieldName: (
135
+ fieldName: string,
136
+ mapperType?: string
137
+ ) => {
138
+ let field = childByFieldName(
139
+ index,
140
+ fieldName,
141
+ mapperType
142
+ );
143
+ return field ? field : <span></span>;
144
+ },
145
+ }}>
146
+ {getObjectField(index)}
147
+ </fieldConfig.arrayIndexWrapper>
148
+ ) : (
149
+ <div key={field.id}>{getObjectField(index)}</div>
150
+ );
151
+ })
152
+ );
153
+ };
154
+
155
+ return fieldConfig.arrayWrapper ? (
156
+ <fieldConfig.arrayWrapper
157
+ {...{
158
+ append,
159
+ prepend,
160
+ remove,
161
+ getValues: getValues,
162
+ }}>
163
+ {getArrayfields()}
164
+ </fieldConfig.arrayWrapper>
165
+ ) : (
166
+ <>{getArrayfields()}</>
167
+ );
168
+ };
169
+ export default ArrayField;
@@ -0,0 +1,204 @@
1
+ import {
2
+ FieldAlignType,
3
+ FieldOptionsSchema,
4
+ FormFieldComponentPropSchema,
5
+ FormFieldSchema,
6
+ FormFieldType
7
+ } from "../schema/FormFieldSchema";
8
+ import React, {useCallback, useContext, useEffect, useMemo, useState} from "react";
9
+ import {FormContext} from "../context/FormContext";
10
+ import {RegisterOptions} from "react-hook-form";
11
+ import {convertToTitleCase, registerFormField} from "../util";
12
+ import axios from "axios";
13
+ import {reachoAPI} from "../../api";
14
+ import SwitchField from "./SwitchField";
15
+ import FormField from "../FormField";
16
+ import RenderFormField from "../util/RenderFormField";
17
+ import {TrashIcon, XMarkIcon} from "@heroicons/react/24/outline";
18
+ import Tippy from "@tippyjs/react";
19
+
20
+
21
+ export const BusinessHoursField: React.FC<FormFieldComponentPropSchema> = (props: FormFieldComponentPropSchema) => {
22
+ const formContext = useContext(FormContext);
23
+ const [listOptions, setListOptions] = useState<any[]>([]);
24
+ const [loading, setLoading] = useState<boolean>(false);
25
+
26
+ let registerOptions: RegisterOptions = registerFormField(props.fieldConfig);
27
+ const hookProps = useMemo(() => formContext.register(props.fieldConfig.name, registerOptions), [formContext, props.fieldConfig.name, registerOptions]);
28
+
29
+ useEffect(() => {
30
+ fetchData();
31
+ }, []);
32
+
33
+ const getTimeConfig = (mappedName: string): FormFieldSchema => {
34
+ return {
35
+ required: true,
36
+ name: mappedName + ".sessions",
37
+ wrapper: ({children}) => {
38
+ return <div style={{border: '1px sold #ddd', padding: '0px'}}>{children}</div>;
39
+ },
40
+ arrayWrapper: ({children, append,getValues}) => {
41
+ return (
42
+ <>
43
+ <div style={{border: '1px sold #ddd',}}>
44
+ {children}
45
+ </div>
46
+ <div className='w-full sm:w-full text-end mr-[2.3em]'>
47
+ <button type='button' className={`text-end text-primary cursor-pointer font-[13px] font-medium ${getValues(`${mappedName}.sessions`)?.length > 1 ? "mr-9" : ''}`}
48
+ onClick={() => {
49
+ const lastEndTime = getValues(`${mappedName}.sessions`)?.[getValues(`${mappedName}.sessions`).length - 1]?.endTime || '08:30';
50
+
51
+ const newStartTime = lastEndTime; // Start new session at last session's end time
52
+ const newEndTime = "23:00"; // Calculate end time, but cap at 23:00
53
+
54
+ append({
55
+ startTime: newStartTime,
56
+ endTime: newEndTime,
57
+ });
58
+ }}
59
+ >
60
+ + Add
61
+ </button>
62
+ </div>
63
+ </>
64
+ );
65
+ },
66
+ arrayIndexWrapper: ({
67
+ mappedName,
68
+ getValues,
69
+ children,
70
+ childByFieldName,
71
+ append,
72
+ prepend,
73
+ remove,
74
+ index
75
+ }) => {
76
+ return (
77
+ <div className="flex items-center gap-4 form-group !mb-2">
78
+ <div>{childByFieldName('startTime')}</div>
79
+ <p>To</p>
80
+ <div>{childByFieldName('endTime')}</div>
81
+ {index > 0 ?
82
+ <button
83
+ className="Button__StyledButton-sc-1l1v2a6-0 gVdTUT"
84
+ type="button"
85
+ onClick={() => {
86
+ remove(index);
87
+ }}
88
+ >
89
+ {/* <TrashIcon height={18} width={18} className=""/> */}
90
+ <Tippy content="Delete">
91
+ <XMarkIcon aria-hidden="true" height={18} width={18} className="" />
92
+ </Tippy>
93
+
94
+ </button> : <></>}
95
+ </div>
96
+
97
+ );
98
+ },
99
+ formFieldType: FormFieldType.ARRAY,
100
+ align: FieldAlignType.HORIZONTAL,
101
+ disableDefaultWrapper: true,
102
+ defaultValue: {
103
+ startTime: "08:30",
104
+ endTime: "18:00"
105
+ },
106
+ children: [
107
+ {
108
+ required: false,
109
+ formFieldType: FormFieldType.TIME,
110
+ name: "startTime",
111
+ disableDefaultWrapper:true,
112
+ defaultValue: "08:30",
113
+ customClassNames: {
114
+ fieldClassName: "border-none bg-blue-100",
115
+ },
116
+ }, {
117
+ required: false,
118
+ formFieldType: FormFieldType.TIME,
119
+ name: "endTime",
120
+ disableDefaultWrapper: true,
121
+ defaultValue: "18:00",
122
+ customClassNames: {
123
+ fieldClassName: "border-none bg-blue-100",
124
+ },
125
+ },
126
+ ],
127
+ }
128
+ }
129
+ ;
130
+
131
+ const fetchData = useCallback(async () => {
132
+ setLoading(true);
133
+ // if (!props.fieldConfig.fetchUrl) return;
134
+ let url = "/api/core/user-prefs/get-default-business-days";
135
+ if (props.fieldConfig.fetchUrl) {
136
+ url = props.fieldConfig.fetchUrl;
137
+ }
138
+ try {
139
+ let response = await (props.fieldConfig.disableHeaderInFetch
140
+ ? axios.get(url)
141
+ : reachoAPI.get(url));
142
+
143
+ if (response.data) {
144
+ const data: FieldOptionsSchema[] = response.data;
145
+ setListOptions(data);
146
+ setLoading(false);
147
+
148
+ if (props.fieldConfig.fetchCallback) {
149
+ props.fieldConfig.fetchCallback(response);
150
+ }
151
+ } else {
152
+ console.error(response.statusText);
153
+ setLoading(false);
154
+ }
155
+ } catch (error) {
156
+ console.error('Fetch error:', error);
157
+ setLoading(false);
158
+ }
159
+ },
160
+ [props.fieldConfig.fetchUrl, props.fieldConfig.optionsConfig, props.fieldConfig.fetchCallback, props.fieldConfig.disableHeaderInFetch]
161
+ );
162
+
163
+ function getInput() {
164
+ return <>
165
+ {loading?<></>:
166
+ listOptions && Object.keys(listOptions).map((day) => {
167
+ const dayInfo = listOptions[day as any];
168
+ return (
169
+ <>
170
+ <div className="flex baseline mb-6" data-testid="" key={day}>
171
+ <div className="max-w-sm space-y-2">
172
+ <div
173
+ className="group flex items-center w-full gap-x-2 text-sm font-normal leading-none text-gray-700 mt-3">
174
+ <SwitchField fieldConfig={{
175
+ name: props.fieldConfig.name + "." + day + ".enabledDay",
176
+ defaultValue: dayInfo.enabledDay,
177
+ required: false,
178
+ formFieldType: FormFieldType.SWITCH,
179
+ disableDefaultWrapper: true,
180
+ customClassNames: {
181
+ fieldClassName: "!mb-0",
182
+ },
183
+ }}/>
184
+ <p className="flex justify-between border-gray-900 text-sm text-gray-900 font-medium ml-2 w-24 leading-none">
185
+ <span className="w-full">{convertToTitleCase(day)}</span>
186
+ </p>
187
+ </div>
188
+ </div>
189
+
190
+ <div className=" flex items-center text-gray-500 focus-within:text-blue-500">
191
+ <FormField fieldConfig={getTimeConfig(props.fieldConfig.name + "." + day)}/>
192
+ </div>
193
+ </div>
194
+ </>
195
+ );
196
+ })
197
+ }
198
+ </>
199
+ }
200
+
201
+ return (
202
+ <RenderFormField fieldConfig={props.fieldConfig} getInput={getInput}/>
203
+ );
204
+ };
@@ -0,0 +1,97 @@
1
+ import { RegisterOptions } from "react-hook-form";
2
+ import { useContext } from "react";
3
+ import React from "react";
4
+ import { FormContext } from "../context/FormContext";
5
+ import {
6
+ FormFieldComponentPropSchema,
7
+ FormFieldPatternsImpl,
8
+ } from "../schema/FormFieldSchema";
9
+ import { Field, Label } from "@headlessui/react";
10
+ import RenderFormField from "../util/RenderFormField";
11
+
12
+ const CheckboxButtonField: React.FC<FormFieldComponentPropSchema> = (
13
+ props: FormFieldComponentPropSchema
14
+ ) => {
15
+ const formContext = useContext(FormContext);
16
+
17
+ let registerOptions: RegisterOptions = {
18
+ required: props.fieldConfig.required
19
+ ? FormFieldPatternsImpl.REQUIRED.getMessage()
20
+ : false,
21
+ };
22
+ if (props.fieldConfig?.formFieldPattern) {
23
+ registerOptions.pattern =
24
+ props.fieldConfig?.formFieldPattern[0].getPattern();
25
+ }
26
+
27
+ let hookProps = formContext.register(
28
+ props.fieldConfig.name,
29
+ registerOptions
30
+ );
31
+
32
+ const handleChange = (value: boolean) => {
33
+ formContext.setValue(props.fieldConfig?.name, value, {
34
+ shouldValidate: true,
35
+ shouldDirty: formContext.getFieldState(props.fieldConfig.name)
36
+ .isDirty,
37
+ shouldTouch: formContext.getFieldState(props.fieldConfig.name)
38
+ .isTouched,
39
+ });
40
+ };
41
+
42
+ function getInput() {
43
+ return props.fieldConfig.options ? (
44
+ props.fieldConfig.options.map((option) => {
45
+ return (
46
+ <Field
47
+ className={
48
+ props.fieldConfig.customClassNames?.fieldClassName
49
+ ? "checkbox-inline" +
50
+ props.fieldConfig.customClassNames
51
+ ?.fieldClassName
52
+ : "checkbox-inline"
53
+ }>
54
+ <Label className="text-capitalize checkbox checkbox-primary">
55
+ <input
56
+ {...hookProps}
57
+ name={props.fieldConfig.name}
58
+ type="checkbox"
59
+ value={option.value}
60
+ data-do-not-deserialize="true"
61
+ />
62
+ {/* <span className="text-capitalize"> </span>{" "} */}
63
+ {option.label}
64
+ </Label>
65
+ </Field>
66
+ );
67
+ })
68
+ ) : (
69
+ <>
70
+ <Field className="be-checkbox">
71
+ <Label
72
+ className="checkbox checkbox-primary flex"
73
+ style={{ width: "fit-content" }}>
74
+ <input
75
+ {...hookProps}
76
+ className="form-checkbox"
77
+ type="checkbox"
78
+ // defaultValue={props.fieldConfig.defaultValue}
79
+ value={formContext.getValues(
80
+ props.fieldConfig.name
81
+ )}
82
+ onChange={(e) => handleChange(e.target.checked)}
83
+ />
84
+ <Label className="form-check-label ms-1">
85
+ {props.fieldConfig.label}
86
+ </Label>
87
+ </Label>
88
+ </Field>
89
+ </>
90
+ );
91
+ }
92
+ return (
93
+ <RenderFormField fieldConfig={props.fieldConfig} getInput={getInput} />
94
+ );
95
+ };
96
+
97
+ export default CheckboxButtonField;