@engagebay/engagebay-form-module 1.0.10 → 1.0.11

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.10",
3
+ "version": "1.0.11",
4
4
  "description": "Provide base form components to engagebay",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -1,32 +1,46 @@
1
1
  import { RegisterOptions } from "react-hook-form";
2
2
  import { useContext, useEffect, useState } from "react";
3
3
  import { FormContext } from "../context/FormContext";
4
- import { FormFieldComponentPropSchema } from "../schema/FormFieldSchema";
4
+ import {
5
+ FormFieldComponentPropSchema,
6
+ OutputFormatType,
7
+ } from "../schema/FormFieldSchema";
5
8
  import { Checkbox, Field, Input, Label } from "@headlessui/react";
6
9
  import React from "react";
7
10
  import RenderFormField from "../util/RenderFormField";
8
11
  import { handleChange, registerFormField } from "../util";
12
+ import { normalizeCheckboxBooleanValue, normalizeMultiSelectValue } from "../util/normalizeCustomFieldValues";
9
13
  const CheckboxField: React.FC<FormFieldComponentPropSchema> = (
10
14
  props: FormFieldComponentPropSchema
11
15
  ) => {
12
16
  const formContext = useContext(FormContext);
13
17
  const [selectedOption, setSelectedOption] = useState<string[]>(
14
- formContext.getValues(props.fieldConfig.name)
15
- ? formContext.getValues(props.fieldConfig.name)
16
- : []
18
+ normalizeMultiSelectValue(formContext.getValues(props.fieldConfig.name))
17
19
  );
18
20
  const [isChecked, setIsChecked] = useState<boolean>(
19
- formContext.getValues(props.fieldConfig.name)
20
- ? formContext.getValues(props.fieldConfig.name)
21
- : props.fieldConfig.defaultValue
21
+ normalizeCheckboxBooleanValue(
22
+ formContext.getValues(props.fieldConfig.name) ?? props.fieldConfig.defaultValue
23
+ )
22
24
  );
23
25
  useEffect(() => {
24
- if (formContext.getValues(props.fieldConfig.name)) {
25
- setSelectedOption(formContext.getValues(props.fieldConfig.name));
26
+ const current = formContext.getValues(props.fieldConfig.name);
27
+ if (props.fieldConfig.options) {
28
+ setSelectedOption(normalizeMultiSelectValue(current));
29
+ } else {
30
+ setIsChecked(
31
+ normalizeCheckboxBooleanValue(current ?? props.fieldConfig.defaultValue)
32
+ );
26
33
  }
27
34
  }, []);
28
35
  let registerOptions: RegisterOptions = registerFormField(props.fieldConfig);
29
36
  let hookProps = formContext.register(props.fieldConfig.name, registerOptions);
37
+ const getOutputValue = (value: any) => {
38
+ if (props.fieldConfig.outputFormat === OutputFormatType.STRING) {
39
+ if (Array.isArray(value)) return JSON.stringify(value);
40
+ if (typeof value === "boolean") return String(value);
41
+ }
42
+ return value;
43
+ };
30
44
  const handleInput = (option: string) => {
31
45
  let tempData: string[] = selectedOption;
32
46
  if (selectedOption.includes(option)) {
@@ -36,7 +50,12 @@ const CheckboxField: React.FC<FormFieldComponentPropSchema> = (
36
50
  tempData.push(option);
37
51
  setSelectedOption([...tempData]);
38
52
  }
39
- handleChange(tempData, formContext, props.fieldConfig, props.onChange);
53
+ handleChange(
54
+ getOutputValue(tempData),
55
+ formContext,
56
+ props.fieldConfig,
57
+ props.onChange
58
+ );
40
59
  };
41
60
  function getInput() {
42
61
  return props.fieldConfig.options ? (
@@ -87,7 +106,7 @@ const CheckboxField: React.FC<FormFieldComponentPropSchema> = (
87
106
  checked={isChecked}
88
107
  onChange={(checked) => {
89
108
  handleChange(
90
- checked,
109
+ getOutputValue(checked),
91
110
  formContext,
92
111
  props.fieldConfig,
93
112
  props.onChange
@@ -1,12 +1,15 @@
1
1
  import { RegisterOptions } from "react-hook-form";
2
- import React, { useContext, useEffect, useRef, useState } from "react";
2
+ import React, { useContext, useEffect } from "react";
3
3
  import Datepicker from "react-tailwindcss-datepicker";
4
4
  import { FormContext } from "../context/FormContext";
5
5
  import { FormFieldComponentPropSchema } from "../schema/FormFieldSchema";
6
6
  import RenderFormField from "../util/RenderFormField";
7
7
  import { handleChange, registerFormField } from "../util";
8
+ import { normalizeDateFormat, normalizeDateInputValue } from "../util/normalizeCustomFieldValues";
8
9
  import moment from "moment";
9
10
 
11
+
12
+
10
13
  const DatePicker: React.FC<FormFieldComponentPropSchema> = (
11
14
  props: FormFieldComponentPropSchema
12
15
  ) => {
@@ -35,6 +38,7 @@ const DatePicker: React.FC<FormFieldComponentPropSchema> = (
35
38
  let hookProps = formContext.register(props.fieldConfig.name, registerOptions);
36
39
 
37
40
  function getInput() {
41
+ const displayFormat = normalizeDateFormat(props.fieldConfig.dateDisplayFormat);
38
42
  const rawStart =
39
43
  initialDate?.startDate ||
40
44
  props.fieldConfig.defaultValue ||
@@ -43,13 +47,9 @@ const DatePicker: React.FC<FormFieldComponentPropSchema> = (
43
47
  initialDate?.endDate ||
44
48
  props.fieldConfig.defaultValue ||
45
49
  "";
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
50
  const value = {
51
- startDate: toDate(rawStart),
52
- endDate: toDate(rawEnd),
51
+ startDate: normalizeDateInputValue(rawStart, displayFormat),
52
+ endDate: normalizeDateInputValue(rawEnd, displayFormat),
53
53
  };
54
54
  // Calendar opens on selected date's month/year (must be a Date instance)
55
55
  const startFrom = value.startDate instanceof Date ? value.startDate : new Date();
@@ -58,10 +58,11 @@ const DatePicker: React.FC<FormFieldComponentPropSchema> = (
58
58
  value={value}
59
59
  startFrom={startFrom}
60
60
  {...hookProps}
61
+ displayFormat={displayFormat}
61
62
  placeholder={
62
63
  props.fieldConfig.placeholder
63
64
  ? props.fieldConfig.placeholder
64
- : "YYYY-MM-DD"
65
+ : displayFormat
65
66
  }
66
67
  asSingle={true}
67
68
  popoverDirection="down"
@@ -79,7 +80,7 @@ const DatePicker: React.FC<FormFieldComponentPropSchema> = (
79
80
  onChange={(dates) => {
80
81
  let date = null;
81
82
  if (dates?.startDate != null)
82
- date = moment(dates?.startDate).format("YYYY-MM-DD");
83
+ date = moment(dates?.startDate).format(displayFormat);
83
84
 
84
85
  handleChange(date, formContext, props.fieldConfig, props.onChange);
85
86
  }}
@@ -9,6 +9,8 @@ import { Description } from "@headlessui/react";
9
9
  import React from "react";
10
10
  import RenderFormField from "../util/RenderFormField";
11
11
  import clsx from "clsx";
12
+ import { XMarkIcon } from "@heroicons/react/20/solid";
13
+ import { normalizeFileFieldUrls } from "../util/normalizeCustomFieldValues";
12
14
  const FileUploadField: React.FC<FormFieldComponentPropSchema> = (
13
15
  props: FormFieldComponentPropSchema
14
16
  ) => {
@@ -103,7 +105,7 @@ const FileUploadField: React.FC<FormFieldComponentPropSchema> = (
103
105
  <Description>Loading...</Description>
104
106
  ) : (
105
107
  props.fieldConfig.fieldContainer ? <props.fieldConfig.fieldContainer /> :
106
- <div className={"flex flex-col items-center justify-center pt-5 pb-6"}>
108
+ <div className={"flex flex-col items-center justify-center pt-5 pb-6 px-2"}>
107
109
  {file ? (
108
110
  <>
109
111
  {isImageFile(file) ? (
@@ -125,19 +127,19 @@ const FileUploadField: React.FC<FormFieldComponentPropSchema> = (
125
127
  </div>
126
128
  ) : (
127
129
  <div className="flex">
128
- <span className="m-2">
129
- {file?.name}
130
+ <span className="m-2 max-w-xs truncate">
131
+ {normalizeFileFieldUrls(file?.name || file?.toString?.() || '')}
130
132
  </span>{" "}
131
133
  <button
132
134
  onClick={(e) => {
133
135
  e.preventDefault();
134
136
  formContext.setValue(
135
- props.fieldConfig.name,cnull
137
+ props.fieldConfig.name,null
136
138
  );
137
139
  setFile(null);
138
140
  }}
139
- className="btn btn-outline-dark btn-sm ">
140
- X
141
+ className="btn btn-sm ">
142
+ <XMarkIcon className="w-3.5 h-3.5 text-gray-800"/>
141
143
  </button>
142
144
  </div>
143
145
  )}
@@ -13,6 +13,7 @@ import {
13
13
  FieldOptionsSchema,
14
14
  FormFieldComponentPropSchema,
15
15
  FormFieldType,
16
+ OutputFormatType,
16
17
  } from "../schema/FormFieldSchema";
17
18
  import { FormContext } from "../context/FormContext";
18
19
  import RenderFormField from "../util/RenderFormField";
@@ -20,6 +21,7 @@ import RenderListOptions, {
20
21
  renderListBoxValue,
21
22
  } from "../util/RenderListOptions";
22
23
  import { handleChange, registerFormField } from "../util";
24
+ import { normalizeMultiSelectValue } from "../util/normalizeCustomFieldValues";
23
25
 
24
26
  const MultipleSelectField: React.FC<FormFieldComponentPropSchema> = ({
25
27
  fieldConfig,
@@ -34,9 +36,11 @@ const MultipleSelectField: React.FC<FormFieldComponentPropSchema> = ({
34
36
  fieldConfig.options ? fieldConfig.options : []
35
37
  );
36
38
 
37
- const validSelectedValues = (
38
- formContext.getValues(fieldConfig.name) || []
39
- ).filter(
39
+ const normalizedSelectedValues = normalizeMultiSelectValue(
40
+ formContext.getValues(fieldConfig.name)
41
+ );
42
+
43
+ const validSelectedValues = normalizedSelectedValues.filter(
40
44
  (selectedValue: any) =>
41
45
  fieldConfig.options &&
42
46
  fieldConfig.options.some((option) => option.value === selectedValue)
@@ -53,14 +57,22 @@ const MultipleSelectField: React.FC<FormFieldComponentPropSchema> = ({
53
57
  }, [fieldConfig.options]);
54
58
 
55
59
  function getInput() {
60
+ const getOutputValue = (val: any) => {
61
+ if (fieldConfig.outputFormat === OutputFormatType.STRING) {
62
+ const normalized = normalizeMultiSelectValue(val);
63
+ return JSON.stringify(normalized);
64
+ }
65
+ return val;
66
+ };
67
+
56
68
  return (
57
69
  <Listbox
58
70
  as={"div"}
59
71
  {...hookProps}
60
72
  value={validSelectedValues}
61
73
  defaultValue={fieldConfig.defaultValue}
62
- onChange={(val) =>
63
- handleChange(val, formContext, fieldConfig, onChange)
74
+ onChange={(val: any) =>
75
+ handleChange(getOutputValue(val), formContext, fieldConfig, onChange)
64
76
  }
65
77
  disabled={fieldConfig.disabled}
66
78
  multiple
@@ -111,6 +111,8 @@ export type FormFieldSchema = {
111
111
  options?: FieldOptionsSchema[];
112
112
  minDate?: Date | null | undefined;
113
113
  maxDate?: Date | null | undefined;
114
+ /** Input display format for date pickers (e.g. 'MMMM D, YYYY'). */
115
+ dateDisplayFormat?: string;
114
116
  // ref?:any
115
117
 
116
118
  /**
@@ -240,7 +242,7 @@ export class FormFieldPatternsImpl implements FormFieldPatterns {
240
242
  static readonly EMAIL = new FormFieldPatternsImpl(
241
243
  "EMAIL",
242
244
  "Please enter a valid email address",
243
- /^([A-Za-z0-9\._%+\-]+@[A-Za-z0-9\.\-]+\.[A-Za-z]{2,})$/
245
+ /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i
244
246
  );
245
247
  // static readonly PASSWORD = new FormFieldPatternsImpl(
246
248
  // "PASSWORD",
@@ -18,7 +18,7 @@ import {
18
18
  FormFieldType,
19
19
  OutputFormatType,
20
20
  } from "../schema/FormFieldSchema";
21
-
21
+ import { normalizeMultiSelectValue, normalizeMultiSelectValueForStringArrayValues } from "./normalizeCustomFieldValues";
22
22
  type RenderListOptionsProps = {
23
23
  formContext: FormContextType;
24
24
  fieldConfig: FormFieldSchema;
@@ -154,12 +154,17 @@ const RenderListOptions = forwardRef<HTMLUListElement, RenderListOptionsProps>(
154
154
 
155
155
  const renderOption = (option: FieldOptionsSchema) => {
156
156
  let selected = false;
157
- if (Array.isArray(formContext.getValues(fieldConfig.name))) {
158
- selected = formContext
159
- .getValues(fieldConfig.name)
160
- .includes(option.value);
157
+ const formValue = formContext.getValues(fieldConfig.name);
158
+ if (formField === FormFieldType.MULTI_SELECT) {
159
+ const normalizedValues = normalizeMultiSelectValue(formValue);
160
+ const normalizedOptionValue =
161
+ typeof option.value === "string"
162
+ ? option.value.trim()
163
+ : String(option.value).trim();
164
+ selected = normalizedValues.includes(normalizedOptionValue);
165
+ } else if (Array.isArray(formValue)) {
166
+ selected = formValue.includes(option.value);
161
167
  } else {
162
- const formValue = formContext.getValues(fieldConfig.name);
163
168
  // const defaultValue = fieldConfig.defaultValue;
164
169
  // selected = formValue ? option.value == formValue : defaultValue == option.value;
165
170
  selected = option.value == formValue;
@@ -335,7 +340,7 @@ export function renderListBoxValue(
335
340
  listOptions: FieldOptionsSchema[],
336
341
  onChange?: (value: any) => void,
337
342
  ): JSX.Element {
338
- let value = formContext.getValues(fieldConfig.name);
343
+ let value = normalizeMultiSelectValueForStringArrayValues(fieldConfig, formContext.getValues(fieldConfig.name));
339
344
  const renderAsString = () => {
340
345
  // if (!listOptions) {
341
346
  // return value;
@@ -437,7 +442,7 @@ export function renderListBoxValue(
437
442
  if (!value && !listOptions) {
438
443
  return getPlaceholder();
439
444
  }
440
- if (!outputFormat && Array.isArray(value)) {
445
+ if (Array.isArray(value)) {
441
446
  return renderAsArray();
442
447
  }
443
448
  return renderAsString();
@@ -0,0 +1,149 @@
1
+ import moment from "moment";
2
+ import { FormFieldSchema, OutputFormatType } from "../schema/FormFieldSchema";
3
+
4
+ const normalizeStringToken = (value: unknown): string => {
5
+ return typeof value === "string" ? value.trim() : String(value).trim();
6
+ };
7
+
8
+ const normalizeStringArray = (values: unknown[]): string[] => {
9
+ return values.map(normalizeStringToken).filter(Boolean);
10
+ };
11
+
12
+ const parseJsonArrayString = (value: string): string[] | null => {
13
+ if (!value.startsWith("[")) return null;
14
+ try {
15
+ const parsed = JSON.parse(value);
16
+ return Array.isArray(parsed) ? normalizeStringArray(parsed) : null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ };
21
+
22
+ export const normalizeCheckboxBooleanValue = (value: any): boolean => {
23
+ if (value === true || value === 1) return true;
24
+ if (value === false || value === 0 || value == null || value === "") {
25
+ return false;
26
+ }
27
+
28
+ if (typeof value === "string") {
29
+ const s = value.trim().toLowerCase();
30
+ if (s === "true" || s === "1" || s === "yes") return true;
31
+ if (s === "false" || s === "0" || s === "no" || s === "") return false;
32
+ }
33
+
34
+ return Boolean(value);
35
+ };
36
+
37
+ export const normalizeMultiSelectValue = (value: any): string[] => {
38
+ if (value == null) return [];
39
+
40
+ if (Array.isArray(value)) return normalizeStringArray(value);
41
+
42
+ if (typeof value === "string") {
43
+ const s = value.trim();
44
+ if (!s) return [];
45
+
46
+ const jsonArray = parseJsonArrayString(s);
47
+ if (jsonArray) return jsonArray;
48
+
49
+ return normalizeStringArray(s.split(","));
50
+ }
51
+
52
+ return normalizeStringArray([value]);
53
+ };
54
+
55
+ export const normalizeDateFormat = (format?: string): string => {
56
+ if (!format) return "YYYY-MM-DD";
57
+ return format
58
+ .replace(/yyyy/g, "YYYY")
59
+ .replace(/yy/g, "YY")
60
+ .replace(/dd/g, "DD")
61
+ .replace(/d/g, "D")
62
+ .replace(/mm/g, "MM");
63
+ };
64
+
65
+ export const normalizeDateInputValue = (
66
+ value: any,
67
+ preferredFormat?: string
68
+ ): Date | null => {
69
+ if (value == null || value === "") return null;
70
+
71
+ if (value instanceof Date) {
72
+ return Number.isNaN(value.getTime()) ? null : value;
73
+ }
74
+
75
+ if (typeof value === "number" && Number.isFinite(value)) {
76
+ const ms = value < 1_000_000_000_000 ? value * 1000 : value;
77
+ const date = new Date(ms);
78
+ return Number.isNaN(date.getTime()) ? null : date;
79
+ }
80
+
81
+ if (typeof value === "string") {
82
+ const s = value.trim();
83
+ if (!s) return null;
84
+
85
+ const numeric = Number(s);
86
+ if (Number.isFinite(numeric)) {
87
+ const ms = numeric < 1_000_000_000_000 ? numeric * 1000 : numeric;
88
+ const date = new Date(ms);
89
+ return Number.isNaN(date.getTime()) ? null : date;
90
+ }
91
+
92
+ const preferred = normalizeDateFormat(preferredFormat);
93
+ const formats = Array.from(
94
+ new Set([
95
+ preferred,
96
+ "YYYY-MM-DD",
97
+ "DD/MM/YYYY",
98
+ "MM/DD/YYYY",
99
+ "DD-MM-YYYY",
100
+ "MM-DD-YYYY",
101
+ ])
102
+ );
103
+ const parsedWithKnownFormats = moment(s, formats, true);
104
+ if (parsedWithKnownFormats.isValid()) {
105
+ return parsedWithKnownFormats.toDate();
106
+ }
107
+
108
+ const date = new Date(s);
109
+ return Number.isNaN(date.getTime()) ? null : date;
110
+ }
111
+
112
+ const date = new Date(value);
113
+ return Number.isNaN(date.getTime()) ? null : date;
114
+ };
115
+
116
+
117
+ export const normalizeMultiSelectValueForStringArrayValues = (fieldConfig: FormFieldSchema, value: any): string[] => {
118
+ if (fieldConfig.outputFormat === OutputFormatType.STRING) {
119
+ return normalizeMultiSelectValue(value);
120
+ }
121
+ return value;
122
+ };
123
+
124
+
125
+ export const normalizeFileFieldUrls = (value: any): string[] => {
126
+ if (value == null) return [];
127
+
128
+ if (Array.isArray(value)) {
129
+ return value.map((v) => String(v).trim()).filter(Boolean);
130
+ }
131
+
132
+ if (typeof value === 'string') {
133
+ const s = value.trim();
134
+ if (!s || s === '[]') return [];
135
+ if (s.startsWith('[')) {
136
+ try {
137
+ const parsed = JSON.parse(s);
138
+ if (Array.isArray(parsed)) {
139
+ return parsed.map((v) => String(v).trim()).filter(Boolean);
140
+ }
141
+ } catch {
142
+ // fall through — treat as single URL
143
+ }
144
+ }
145
+ return [s];
146
+ }
147
+
148
+ return [];
149
+ };