From e869c1f42110db0b3e8ce013073a6a2cdebe877a Mon Sep 17 00:00:00 2001 From: mukund-egov Date: Wed, 18 Sep 2024 14:26:33 +0530 Subject: [PATCH] Corrected FormComposerV2 Error --- .../employee/FSMRegistry/Worker/AddWorker.js | 23 +- .../employee/FSMRegistry/Worker/EditWorker.js | 36 +- .../react-components/src/atoms/Button.js | 76 ++ .../src/atoms/HorizontalNav.js | 52 + .../src/atoms/InputTextAmount.js | 417 ++++++++ .../react-components/src/atoms/Paragraph.js | 13 + .../src/atoms/amtUtils/index.js | 414 ++++++++ .../src/hoc/FormComposerV2.js | 906 ++++++++++++++++++ .../src/hoc/UploadFileComposer.js | 154 +++ .../react-components/src/hoc/formUtils.js | 21 + .../packages/react-components/src/index.js | 4 +- .../src/molecules/ApiDropdown.js | 73 ++ .../src/molecules/LocationDropdownWrapper.js | 105 ++ 13 files changed, 2276 insertions(+), 18 deletions(-) create mode 100644 frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/Button.js create mode 100644 frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/HorizontalNav.js create mode 100644 frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/InputTextAmount.js create mode 100644 frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/Paragraph.js create mode 100644 frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/amtUtils/index.js create mode 100644 frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/hoc/FormComposerV2.js create mode 100644 frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/hoc/UploadFileComposer.js create mode 100644 frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/hoc/formUtils.js create mode 100644 frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/molecules/ApiDropdown.js create mode 100644 frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/molecules/LocationDropdownWrapper.js diff --git a/frontend/micro-ui/web/micro-ui-internals/packages/modules/fsm/src/pages/employee/FSMRegistry/Worker/AddWorker.js b/frontend/micro-ui/web/micro-ui-internals/packages/modules/fsm/src/pages/employee/FSMRegistry/Worker/AddWorker.js index 3e0e77da015..f5a9151be7d 100644 --- a/frontend/micro-ui/web/micro-ui-internals/packages/modules/fsm/src/pages/employee/FSMRegistry/Worker/AddWorker.js +++ b/frontend/micro-ui/web/micro-ui-internals/packages/modules/fsm/src/pages/employee/FSMRegistry/Worker/AddWorker.js @@ -1,6 +1,7 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { FormComposer, Toast, Header, FormComposerV2 } from "@upyog/digit-ui-react-components"; +import { FormComposer, Toast, Header } from "@upyog/digit-ui-react-components"; +import { FormComposerV2 } from "../../../../../../../react-components/src"; import { useHistory } from "react-router-dom"; import WorkerConfig from "../../configs/WorkerConfig"; import { useQueryClient } from "react-query"; @@ -17,7 +18,9 @@ const AddWorker = ({ parentUrl, heading }) => { const [Config, setConfig] = useState(WorkerConfig({ t })); const [skillsOption, setSkillsOption] = useState([]); const [employer, setEmployer] = useState([]); - const { isLoading: isLoading, isError: vendorCreateError, data: updateResponse, error: updateError, mutate } = Digit.Hooks.fsm.useWorkerCreate(tenantId); + const { isLoading: isLoading, isError: vendorCreateError, data: updateResponse, error: updateError, mutate } = Digit.Hooks.fsm.useWorkerCreate( + tenantId + ); const { isLoading: isVendorUpdateLoading, isError: isvendorUpdateError, @@ -26,9 +29,13 @@ const AddWorker = ({ parentUrl, heading }) => { mutate: vendorMutate, } = Digit.Hooks.fsm.useVendorUpdate(tenantId); - const { isLoading: isPlantUserLoading, isError: isPlantUserError, data: plantUserResponse, error: PlantUserError, mutate: PlantUserMutate } = Digit.Hooks.fsm.usePlantUserCreate( - tenantId - ); + const { + isLoading: isPlantUserLoading, + isError: isPlantUserError, + data: plantUserResponse, + error: PlantUserError, + mutate: PlantUserMutate, + } = Digit.Hooks.fsm.usePlantUserCreate(tenantId); const { isLoading: ismdms, data: mdmsOptions } = Digit.Hooks.useCustomMDMS( stateId, @@ -130,7 +137,9 @@ const AddWorker = ({ parentUrl, heading }) => { restructuredData.push(restructuredItem); }); - const driverLicenses = roleDetails?.filter((entry) => entry.fn_role && entry.fn_role.code === "DRIVER" && entry.licenseNo).map((entry) => entry.licenseNo); + const driverLicenses = roleDetails + ?.filter((entry) => entry.fn_role && entry.fn_role.code === "DRIVER" && entry.licenseNo) + .map((entry) => entry.licenseNo); const roleDetailsArray = []; roleDetails?.forEach((item, index) => { @@ -320,4 +329,4 @@ const AddWorker = ({ parentUrl, heading }) => { ); }; -export default AddWorker; \ No newline at end of file +export default AddWorker; diff --git a/frontend/micro-ui/web/micro-ui-internals/packages/modules/fsm/src/pages/employee/FSMRegistry/Worker/EditWorker.js b/frontend/micro-ui/web/micro-ui-internals/packages/modules/fsm/src/pages/employee/FSMRegistry/Worker/EditWorker.js index 2303bc8fe86..365cec67b92 100644 --- a/frontend/micro-ui/web/micro-ui-internals/packages/modules/fsm/src/pages/employee/FSMRegistry/Worker/EditWorker.js +++ b/frontend/micro-ui/web/micro-ui-internals/packages/modules/fsm/src/pages/employee/FSMRegistry/Worker/EditWorker.js @@ -1,8 +1,10 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Toast, Header, FormComposerV2, Loader } from "@upyog/digit-ui-react-components"; +import { Toast, Header, Loader } from "@upyog/digit-ui-react-components"; +import { FormComposerV2 } from "../../../../../../../react-components/src"; + import { useHistory } from "react-router-dom"; -import WorkerConfig from "../../configs/WorkerConfig"; +import WorkerConfig from "../../../employee/configs/WorkerConfig"; import { useQueryClient } from "react-query"; // IND-2023-11-24-010875 @@ -68,8 +70,9 @@ const EditWorker = ({ parentUrl, heading }) => { setEmployer(tempEmp); }, [mdmsOptions, ismdms]); - - const { isLoading: isLoading, isError: vendorCreateError, data: updateResponse, error: updateError, mutate } = Digit.Hooks.fsm.useWorkerUpdate(tenantId); + const { isLoading: isLoading, isError: vendorCreateError, data: updateResponse, error: updateError, mutate } = Digit.Hooks.fsm.useWorkerUpdate( + tenantId + ); const { data: workerData, isLoading: WorkerLoading } = Digit.Hooks.fsm.useWorkerSearch({ tenantId, @@ -89,7 +92,13 @@ const EditWorker = ({ parentUrl, heading }) => { setConfig(WorkerConfig({ t, disabled: true, skillsOption, defaultSkill: workerDetails?.skills?.filter((i) => i.isDeleted === false), employer })); }, [skillsOption, employer, workerData]); - const { data: vendorData, isLoading: isVendorLoading, isSuccess: isVendorSuccess, error: vendorError, refetch: refetchVendor } = Digit.Hooks.fsm.useDsoSearch( + const { + data: vendorData, + isLoading: isVendorLoading, + isSuccess: isVendorSuccess, + error: vendorError, + refetch: refetchVendor, + } = Digit.Hooks.fsm.useDsoSearch( tenantId, { sortBy: "name", sortOrder: "ASC", status: "ACTIVE", individualIds: workerData?.Individual?.[0]?.id }, { enabled: workerData && !WorkerLoading, cacheTime: 0 }, @@ -162,7 +171,7 @@ const EditWorker = ({ parentUrl, heading }) => { } useEffect(() => { - if (workerData && workerData?.Individual && !isVendorLoading && vendorData ) { + if (workerData && workerData?.Individual && !isVendorLoading && vendorData) { const workerDetails = workerData?.Individual?.[0]; const respSkills = workerDetails?.skills?.filter((i) => i.isDeleted === false); setWorkerinfo(workerDetails); @@ -242,7 +251,10 @@ const EditWorker = ({ parentUrl, heading }) => { formData?.employementDetails?.vendor && (!formData?.AddWorkerRoles || formData?.AddWorkerRoles?.length === 0 || (formData?.AddWorkerRoles?.length > 0 && checkRoleField)) ) { - if (errors?.SelectEmployeePhoneNumber?.isMobilePresent && formData?.SelectEmployeePhoneNumber?.mobileNumber !== workerData?.Individual?.[0]?.mobileNumber) { + if ( + errors?.SelectEmployeePhoneNumber?.isMobilePresent && + formData?.SelectEmployeePhoneNumber?.mobileNumber !== workerData?.Individual?.[0]?.mobileNumber + ) { setSubmitValve(false); } else { setSubmitValve(true); @@ -275,7 +287,9 @@ const EditWorker = ({ parentUrl, heading }) => { let respSkills = workerinfo?.skills; // if skills are not there but present in resp then remove - respSkills = respSkills.filter((resp_item) => skills.some((mydata_item) => resp_item.type === mydata_item.type && resp_item.level === mydata_item.level)); + respSkills = respSkills.filter((resp_item) => + skills.some((mydata_item) => resp_item.type === mydata_item.type && resp_item.level === mydata_item.level) + ); // respSkills.forEach((resp_item) => { // const existsInSelected = skills.some((selected_item) => resp_item.type === selected_item.type && resp_item.level === selected_item.level); // resp_item.isDeleted = !existsInSelected; @@ -306,7 +320,9 @@ const EditWorker = ({ parentUrl, heading }) => { restructuredData.push(restructuredItem); }); - const driverLicenses = roleDetails?.filter((entry) => entry.fn_role && entry.fn_role.code === "DRIVER" && entry.licenseNo).map((entry) => entry.licenseNo); + const driverLicenses = roleDetails + ?.filter((entry) => entry.fn_role && entry.fn_role.code === "DRIVER" && entry.licenseNo) + .map((entry) => entry.licenseNo); const roleDetailsArray = []; if (roleDetails) { @@ -506,4 +522,4 @@ const EditWorker = ({ parentUrl, heading }) => { ); }; -export default EditWorker; \ No newline at end of file +export default EditWorker; diff --git a/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/Button.js b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/Button.js new file mode 100644 index 00000000000..22e4703d333 --- /dev/null +++ b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/Button.js @@ -0,0 +1,76 @@ +import React from "react"; +import PropTypes from "prop-types"; + +/** + * Button Component + * + * @author jagankumar-egov + * + * Feature :: CORE + * + * @example + * // Primary button + + ); +}; + +Button.propTypes = { + /** + * ButtonSelector content + */ + label: PropTypes.string.isRequired, + /** + * button border theme + */ + variation: PropTypes.string, + /** + * button icon if any + */ + icon: PropTypes.element, + /** + * click handler + */ + onButtonClick: PropTypes.func.isRequired, + /** + * Custom classname + */ + className: PropTypes.string, + /** + * Custom styles + */ + style: PropTypes.object, + /** + * Custom label style or h2 style + */ + textStyles: PropTypes.object, +}; + +Button.defaultProps = { + label: "TEST", + variation: "primary", + onButtonClick: () => {}, +}; + +export default Button; diff --git a/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/HorizontalNav.js b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/HorizontalNav.js new file mode 100644 index 00000000000..a88e2a369fd --- /dev/null +++ b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/HorizontalNav.js @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; + +const HorizontalNav = ({ + configNavItems, + activeLink, + setActiveLink, + showNav = false, + children, + customStyle = {}, + customClassName = "", + inFormComposer = true, + navClassName = "", + navStyles = {}, +}) => { + const { t } = useTranslation(); + + const setActive = (item) => { + setActiveLink(item.name); + }; + + const MenuItem = ({ item }) => { + let itemComponent = item.code; + + const Item = () => ( + +
{t(itemComponent)}
+
+ ); + + return ; + }; + return ( +
+ {showNav && ( +
+ {configNavItems?.map((item, index) => ( +
setActive(item)}> + +
+ ))} +
+ )} + {children} +
+ ); +}; + +export default HorizontalNav; diff --git a/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/InputTextAmount.js b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/InputTextAmount.js new file mode 100644 index 00000000000..53b29afe8e6 --- /dev/null +++ b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/InputTextAmount.js @@ -0,0 +1,417 @@ +import TextInput from "./TextInput"; + +import React, { forwardRef, useImperativeHandle, useMemo, useRef, useEffect, useState } from "react"; +import { + cleanValue, + fixedDecimalValue, + formatValue, + getLocaleConfig, + getIntlConfig, + getSuffix, + isNumber, + padTrimValue, + repositionCursor, +} from "./amtUtils"; + +/* Amount component by default round offs and formats for amount */ + +const InputTextAmount = ({ value, prefix = "₹ ", intlConfig = getIntlConfig(prefix), onChange, inputRef, ...otherProps }) => { + return ( + + ); +}; + +export default InputTextAmount; + +export const CurrencyInput = forwardRef( + ( + { + allowDecimals = true, + allowNegativeValue = true, + id, + name, + className, + customInput, + decimalsLimit, + defaultValue, + disabled = false, + maxLength: userMaxLength, + value: userValue, + onValueChange, + fixedDecimalLength, + placeholder, + decimalScale, + prefix, + suffix, + intlConfig, + step, + min, + max, + disableGroupSeparators = false, + disableAbbreviations = false, + decimalSeparator: _decimalSeparator, + groupSeparator: _groupSeparator, + onChange, + onFocus, + onBlur, + onKeyDown, + onKeyUp, + transformRawValue, + otherProps, + inputRef: inputRefFrom, + ...props + }, + ref + ) => { + if (_decimalSeparator && isNumber(_decimalSeparator)) { + throw new Error("decimalSeparator cannot be a number"); + } + + if (_groupSeparator && isNumber(_groupSeparator)) { + throw new Error("groupSeparator cannot be a number"); + } + + const localeConfig = useMemo(() => getLocaleConfig(intlConfig), [intlConfig]); + const decimalSeparator = _decimalSeparator || localeConfig.decimalSeparator || ""; + const groupSeparator = _groupSeparator || localeConfig.groupSeparator || ""; + + if (decimalSeparator && groupSeparator && decimalSeparator === groupSeparator && disableGroupSeparators === false) { + throw new Error("decimalSeparator cannot be the same as groupSeparator"); + } + + const formatValueOptions = { + decimalSeparator, + groupSeparator, + disableGroupSeparators, + intlConfig, + prefix: prefix || localeConfig.prefix, + suffix: suffix, + }; + + const cleanValueOptions = { + decimalSeparator, + groupSeparator, + allowDecimals, + decimalsLimit: decimalsLimit || fixedDecimalLength || 2, + allowNegativeValue, + disableAbbreviations, + prefix: prefix || localeConfig.prefix, + transformRawValue, + }; + + const formattedStateValue = + defaultValue !== undefined && defaultValue !== null + ? formatValue({ ...formatValueOptions, decimalScale, value: String(defaultValue) }) + : userValue !== undefined && userValue !== null + ? formatValue({ ...formatValueOptions, decimalScale, value: String(userValue) }) + : ""; + + const [stateValue, setStateValue] = useState(formattedStateValue); + const [dirty, setDirty] = useState(false); + const [cursor, setCursor] = useState(0); + const [changeCount, setChangeCount] = useState(0); + const [lastKeyStroke, setLastKeyStroke] = useState(null); + const inputRef = useRef(inputRefFrom); + useImperativeHandle(ref, () => inputRef.current); + + /** + * Process change in value + */ + const processChange = (value, selectionStart) => { + setDirty(true); + + const { modifiedValue, cursorPosition } = repositionCursor({ + selectionStart, + value, + lastKeyStroke, + stateValue, + groupSeparator, + }); + + const stringValue = cleanValue({ value: modifiedValue, ...cleanValueOptions }); + + if (userMaxLength && stringValue.replace(/-/g, "").length > userMaxLength) { + return; + } + + if (stringValue === "" || stringValue === "-" || stringValue === decimalSeparator) { + onValueChange && onValueChange(undefined, name, { float: null, formatted: "", value: "" }); + setStateValue(stringValue); + return; + } + + const stringValueWithoutSeparator = decimalSeparator ? stringValue.replace(decimalSeparator, ".") : stringValue; + + const numberValue = parseFloat(stringValueWithoutSeparator); + + const formattedValue = formatValue({ + value: stringValue, + ...formatValueOptions, + }); + + if (cursorPosition !== undefined && cursorPosition !== null) { + // Prevent cursor jumping + let newCursor = cursorPosition + (formattedValue.length - value.length); + newCursor = newCursor <= 0 ? (prefix ? prefix.length : 0) : newCursor; + + setCursor(newCursor); + setChangeCount(changeCount + 1); + } + + setStateValue(formattedValue); + + if (onValueChange) { + const values = { + float: numberValue, + formatted: formattedValue, + value: stringValue, + }; + onValueChange(stringValue, name, values); + } + }; + + /** + * Handle change event + */ + const handleOnChange = (event) => { + const { + target: { value, selectionStart }, + } = event; + + processChange(value, selectionStart); + + onChange && onChange(event); + }; + + /** + * Handle focus event + */ + const handleOnFocus = (event) => { + onFocus && onFocus(event); + return stateValue ? stateValue.length : 0; + }; + + /** + * Handle blur event + * + * Format value by padding/trimming decimals if required by + */ + const handleOnBlur = (event) => { + const { + target: { value }, + } = event; + + const valueOnly = cleanValue({ value, ...cleanValueOptions }); + + if (valueOnly === "-" || !valueOnly) { + setStateValue(""); + onBlur && onBlur(event); + return; + } + + const fixedDecimals = fixedDecimalValue(valueOnly, decimalSeparator, fixedDecimalLength); + + const newValue = padTrimValue(fixedDecimals, decimalSeparator, decimalScale !== undefined ? decimalScale : fixedDecimalLength); + + const numberValue = parseFloat(newValue.replace(decimalSeparator, ".")); + + const formattedValue = formatValue({ + ...formatValueOptions, + value: newValue, + }); + + if (onValueChange) { + onValueChange(newValue, name, { + float: numberValue, + formatted: formattedValue, + value: newValue, + }); + } + + setStateValue(formattedValue); + + onBlur && onBlur(event); + }; + + /** + * Handle key down event + * + * Increase or decrease value by step + */ + const handleOnKeyDown = (event) => { + const { key } = event; + + setLastKeyStroke(key); + + if (step && (key === "ArrowUp" || key === "ArrowDown")) { + event.preventDefault(); + setCursor(stateValue.length); + + const currentValue = + parseFloat( + userValue !== undefined && userValue !== null + ? String(userValue).replace(decimalSeparator, ".") + : cleanValue({ value: stateValue, ...cleanValueOptions }) + ) || 0; + const newValue = key === "ArrowUp" ? currentValue + step : currentValue - step; + + if (min !== undefined && newValue < min) { + return; + } + + if (max !== undefined && newValue > max) { + return; + } + + const fixedLength = String(step).includes(".") ? Number(String(step).split(".")[1].length) : undefined; + + processChange(String(fixedLength ? newValue.toFixed(fixedLength) : newValue).replace(".", decimalSeparator)); + } + + onKeyDown && onKeyDown(event); + }; + + /** + * Handle key up event + * + * Move cursor if there is a suffix to prevent user typing past suffix + */ + const handleOnKeyUp = (event) => { + const { + key, + currentTarget: { selectionStart }, + } = event; + if (key !== "ArrowUp" && key !== "ArrowDown" && stateValue !== "-") { + const suffix = getSuffix(stateValue, { groupSeparator, decimalSeparator }); + + if (suffix && selectionStart && selectionStart > stateValue.length - suffix.length) { + /* istanbul ignore else */ + if (inputRef.current) { + const newCursor = stateValue.length - suffix.length; + inputRef.current.setSelectionRange(newCursor, newCursor); + } + } + } + + onKeyUp && onKeyUp(event); + }; + + useEffect(() => { + // prevent cursor jumping if editing value + if (dirty && stateValue !== "-" && inputRef.current && document.activeElement === inputRef.current) { + inputRef.current.setSelectionRange(cursor, cursor); + } + }, [stateValue, cursor, inputRef, dirty, changeCount]); + + /** + * If user has only entered "-" or decimal separator, + * keep the char to allow them to enter next value + */ + const getRenderValue = () => { + if (userValue !== undefined && userValue !== null && stateValue !== "-" && (!decimalSeparator || stateValue !== decimalSeparator)) { + return formatValue({ + ...formatValueOptions, + decimalScale: dirty ? undefined : decimalScale, + value: String(userValue), + }); + } + + return stateValue; + }; + + const inputProps = { + type: "text", + inputMode: "decimal", + id, + name, + className, + onChange: handleOnChange, + onBlur: handleOnBlur, + onFocus: handleOnFocus, + onKeyDown: handleOnKeyDown, + onKeyUp: handleOnKeyUp, + placeholder, + disabled, + value: getRenderValue(), + ref: inputRef, + ...props, + }; + + if (customInput) { + const CustomInput = customInput; + return ; + } + + return ; + } +); + +CurrencyInput.displayName = "CurrencyInput"; + +export const InputAmountWrapper = ({ ref, ...props }) => { + const limit = 1000; + const prefix = props?.prefix; + + const [errorMessage, setErrorMessage] = useState(""); + const [className, setClassName] = useState(""); + const [value, setValue] = useState(props?.defaultValue); + const [values, setValues] = useState(); + const [rawValue, setRawValue] = useState(" "); + + useEffect(() => { + props.onValueChange(value); + }, [value]); + + /** + * Handle validation + */ + const handleOnValueChange = (value, _, values) => { + setValues(values); + setRawValue(value === undefined ? "undefined" : value || " "); + + if (!value) { + setClassName(""); + setValue(""); + return; + } + + if (Number.isNaN(Number(value))) { + // setErrorMessage('Please enter a valid number'); + // setClassName('is-invalid'); + return; + } + + if (Number(value) > limit) { + // setErrorMessage(`Max: ${prefix}${limit}`); + // setClassName('is-invalid'); + setValue(value); + return; + } + + // setClassName('is-valid'); + setValue(value); + }; + + return ( + + ); +}; diff --git a/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/Paragraph.js b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/Paragraph.js new file mode 100644 index 00000000000..b65c7bd3bf6 --- /dev/null +++ b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/Paragraph.js @@ -0,0 +1,13 @@ +import React from "react"; + +const Paragraph = (props) => { + const { value, customClassName, customStyle, inputRef } = props; + + return ( +

+ {value} +

+ ); +}; + +export default Paragraph; diff --git a/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/amtUtils/index.js b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/amtUtils/index.js new file mode 100644 index 00000000000..38ef1bb5cb1 --- /dev/null +++ b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/atoms/amtUtils/index.js @@ -0,0 +1,414 @@ +/** + * Add group separator to value eg. 1000 > 1,000 + */ +export const addSeparators = (value, separator = ",") => { + return value.replace(/\B(?=(\d{3})+(?!\d))/g, separator); +}; + +/** + * Remove prefix, separators and extra decimals from value + */ +export const cleanValue = ({ + value, + groupSeparator = ",", + decimalSeparator = ".", + allowDecimals = true, + decimalsLimit = 2, + allowNegativeValue = true, + disableAbbreviations = false, + prefix = "", + transformRawValue = (rawValue) => rawValue, +}) => { + const transformedValue = transformRawValue(value); + + if (transformedValue === "-") { + return transformedValue; + } + + const abbreviations = disableAbbreviations ? [] : ["k", "m", "b"]; + const reg = new RegExp(`((^|\\D)-\\d)|(-${escapeRegExp(prefix)})`); + const isNegative = reg.test(transformedValue); + + // Is there a digit before the prefix? eg. 1$ + const [prefixWithValue, preValue] = RegExp(`(\\d+)-?${escapeRegExp(prefix)}`).exec(value) || []; + const withoutPrefix = prefix + ? prefixWithValue + ? transformedValue.replace(prefixWithValue, "").concat(preValue) + : transformedValue.replace(prefix, "") + : transformedValue; + const withoutSeparators = removeSeparators(withoutPrefix, groupSeparator); + const withoutInvalidChars = removeInvalidChars(withoutSeparators, [groupSeparator, decimalSeparator, ...abbreviations]); + + let valueOnly = withoutInvalidChars; + + if (!disableAbbreviations) { + // disallow letter without number + if (abbreviations.some((letter) => letter === withoutInvalidChars.toLowerCase())) { + return ""; + } + const parsed = parseAbbrValue(withoutInvalidChars, decimalSeparator); + if (parsed) { + valueOnly = String(parsed); + } + } + + const includeNegative = isNegative && allowNegativeValue ? "-" : ""; + + if (decimalSeparator && valueOnly.includes(decimalSeparator)) { + const [int, decimals] = withoutInvalidChars.split(decimalSeparator); + const trimmedDecimals = decimalsLimit && decimals ? decimals.slice(0, decimalsLimit) : decimals; + const includeDecimals = allowDecimals ? `${decimalSeparator}${trimmedDecimals}` : ""; + + return `${includeNegative}${int}${includeDecimals}`; + } + + return `${includeNegative}${valueOnly}`; +}; + +/** + * Escape regex char + * + * See: https://stackoverflow.com/questions/17885855/use-dynamic-variable-string-as-regex-pattern-in-javascript + */ +export const escapeRegExp = (stringToGoIntoTheRegex) => { + return stringToGoIntoTheRegex.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); +}; + +export const fixedDecimalValue = (value, decimalSeparator, fixedDecimalLength) => { + if (fixedDecimalLength && value.length > 1) { + if (value.includes(decimalSeparator)) { + const [int, decimals] = value.split(decimalSeparator); + if (decimals.length > fixedDecimalLength) { + return `${int}${decimalSeparator}${decimals.slice(0, fixedDecimalLength)}`; + } + } + + const reg = value.length > fixedDecimalLength ? new RegExp(`(\\d+)(\\d{${fixedDecimalLength}})`) : new RegExp(`(\\d)(\\d+)`); + + const match = value.match(reg); + if (match) { + const [, int, decimals] = match; + return `${int}${decimalSeparator}${decimals}`; + } + } + + return value; +}; + +/** + * Format value with decimal separator, group separator and prefix + */ +export const formatValue = (options) => { + const { value: _value, decimalSeparator, intlConfig, decimalScale, prefix = "", suffix = "" } = options; + + if (_value === "" || _value === undefined) { + return ""; + } + + if (_value === "-") { + return "-"; + } + + const isNegative = new RegExp(`^\\d?-${prefix ? `${escapeRegExp(prefix)}?` : ""}\\d`).test(_value); + + const value = decimalSeparator !== "." ? replaceDecimalSeparator(_value, decimalSeparator, isNegative) : _value; + + const defaultNumberFormatOptions = { + minimumFractionDigits: decimalScale || 0, + maximumFractionDigits: 20, + }; + + const numberFormatter = intlConfig + ? new Intl.NumberFormat( + intlConfig.locale, + intlConfig.currency + ? { + ...defaultNumberFormatOptions, + style: "currency", + currency: intlConfig.currency, + } + : defaultNumberFormatOptions + ) + : new Intl.NumberFormat(undefined, defaultNumberFormatOptions); + + const parts = numberFormatter.formatToParts(Number(value)); + + let formatted = replaceParts(parts, options); + + // Does intl formatting add a suffix? + const intlSuffix = getSuffix(formatted, { ...options }); + + // Include decimal separator if user input ends with decimal separator + const includeDecimalSeparator = _value.slice(-1) === decimalSeparator ? decimalSeparator : ""; + + const [, decimals] = value.match(RegExp("\\d+\\.(\\d+)")) || []; + + // Keep original decimal padding if no decimalScale + if (decimalScale === undefined && decimals && decimalSeparator) { + if (formatted.includes(decimalSeparator)) { + formatted = formatted.replace(RegExp(`(\\d+)(${escapeRegExp(decimalSeparator)})(\\d+)`, "g"), `$1$2${decimals}`); + } else { + if (intlSuffix && !suffix) { + formatted = formatted.replace(intlSuffix, `${decimalSeparator}${decimals}${intlSuffix}`); + } else { + formatted = `${formatted}${decimalSeparator}${decimals}`; + } + } + } + + if (suffix && includeDecimalSeparator) { + return `${formatted}${includeDecimalSeparator}${suffix}`; + } + + if (intlSuffix && includeDecimalSeparator) { + return formatted.replace(intlSuffix, `${includeDecimalSeparator}${intlSuffix}`); + } + + if (intlSuffix && suffix) { + return formatted.replace(intlSuffix, `${includeDecimalSeparator}${suffix}`); + } + + return [formatted, includeDecimalSeparator, suffix].join(""); +}; + +/** + * Before converting to Number, decimal separator has to be . + */ +const replaceDecimalSeparator = (value, decimalSeparator, isNegative) => { + let newValue = value; + if (decimalSeparator && decimalSeparator !== ".") { + newValue = newValue.replace(RegExp(escapeRegExp(decimalSeparator), "g"), "."); + if (isNegative && decimalSeparator === "-") { + newValue = `-${newValue.slice(1)}`; + } + } + return newValue; +}; + +const replaceParts = (parts, { prefix, groupSeparator, decimalSeparator, decimalScale, disableGroupSeparators = false }) => { + return parts + .reduce( + (prev, { type, value }, i) => { + if (i === 0 && prefix) { + if (type === "minusSign") { + return [value, prefix]; + } + + if (type === "currency") { + return [...prev, prefix]; + } + + return [prefix, value]; + } + + if (type === "currency") { + return prefix ? prev : [...prev, value]; + } + + if (type === "group") { + return !disableGroupSeparators ? [...prev, groupSeparator !== undefined ? groupSeparator : value] : prev; + } + + if (type === "decimal") { + if (decimalScale !== undefined && decimalScale === 0) { + return prev; + } + + return [...prev, decimalSeparator !== undefined ? decimalSeparator : value]; + } + + if (type === "fraction") { + return [...prev, decimalScale !== undefined ? value.slice(0, decimalScale) : value]; + } + + return [...prev, value]; + }, + [""] + ) + .join(""); +}; + +const defaultConfig = { + currencySymbol: "", + groupSeparator: "", + decimalSeparator: "", + prefix: "", + suffix: "", +}; + +/** + * Get locale config from input or default + */ +//tried locale hardcoded +export const getLocaleConfig = (intlConfig) => { + const { locale, currency } = intlConfig || {}; + + const numberFormatter = locale ? new Intl.NumberFormat(locale, currency ? { currency, style: "currency" } : undefined) : new Intl.NumberFormat(); + + return numberFormatter.formatToParts(1000.1).reduce((prev, curr, i) => { + if (curr.type === "currency") { + if (i === 0) { + return { ...prev, currencySymbol: curr.value, prefix: curr.value }; + } else { + return { ...prev, currencySymbol: curr.value, suffix: curr.value }; + } + } + if (curr.type === "group") { + return { ...prev, groupSeparator: curr.value }; + } + if (curr.type === "decimal") { + return { ...prev, decimalSeparator: curr.value }; + } + + return prev; + }, defaultConfig); +}; + +export const getSuffix = (value, { groupSeparator = ",", decimalSeparator = "." }) => { + const suffixReg = new RegExp(`\\d([^${escapeRegExp(groupSeparator)}${escapeRegExp(decimalSeparator)}0-9]+)`); + const suffixMatch = value.match(suffixReg); + return suffixMatch ? suffixMatch[1] : undefined; +}; + +export const isNumber = (input) => RegExp(/\d/, "gi").test(input); + +export const padTrimValue = (value, decimalSeparator = ".", decimalScale) => { + if (decimalScale === undefined || value === "" || value === undefined) { + return value; + } + + if (!value.match(/\d/g)) { + return ""; + } + + const [int, decimals] = value.split(decimalSeparator); + + if (decimalScale === 0) { + return int; + } + + let newValue = decimals || ""; + + if (newValue.length < decimalScale) { + while (newValue.length < decimalScale) { + newValue += "0"; + } + } else { + newValue = newValue.slice(0, decimalScale); + } + + return `${int}${decimalSeparator}${newValue}`; +}; + +/** + * Abbreviate number eg. 1000 = 1k + * + * Source: https://stackoverflow.com/a/9345181 + */ +export const abbrValue = (value, decimalSeparator = ".", _decimalPlaces = 10) => { + if (value > 999) { + let valueLength = ("" + value).length; + const p = Math.pow; + const d = p(10, _decimalPlaces); + valueLength -= valueLength % 3; + + const abbrValue = Math.round((value * d) / p(10, valueLength)) / d + " kMGTPE"[valueLength / 3]; + return abbrValue.replace(".", decimalSeparator); + } + + return String(value); +}; + +const abbrMap = { k: 1000, m: 1000000, b: 1000000000 }; + +/** + * Parse a value with abbreviation e.g 1k = 1000 + */ +export const parseAbbrValue = (value, decimalSeparator = ".") => { + const reg = new RegExp(`(\\d+(${escapeRegExp(decimalSeparator)}\\d*)?)([kmb])$`, "i"); + const match = value.match(reg); + + if (match) { + const [, digits, , abbr] = match; + const multiplier = abbrMap[abbr.toLowerCase()]; + + return Number(digits.replace(decimalSeparator, ".")) * multiplier; + } + + return undefined; +}; + +/** + * Remove invalid characters + */ +export const removeInvalidChars = (value, validChars) => { + const chars = escapeRegExp(validChars.join("")); + const reg = new RegExp(`[^\\d${chars}]`, "gi"); + return value.replace(reg, ""); +}; + +/** + * Remove group separator from value eg. 1,000 > 1000 + */ +export const removeSeparators = (value, separator = ",") => { + const reg = new RegExp(escapeRegExp(separator), "g"); + return value.replace(reg, ""); +}; + +/** + * Based on the last key stroke and the cursor position, update the value + * and reposition the cursor to the right place + */ +export const repositionCursor = ({ selectionStart, value, lastKeyStroke, stateValue, groupSeparator }) => { + let cursorPosition = selectionStart; + let modifiedValue = value; + if (stateValue && cursorPosition) { + const splitValue = value.split(""); + // if cursor is to right of groupSeparator and backspace pressed, delete the character to the left of the separator and reposition the cursor + if (lastKeyStroke === "Backspace" && stateValue[cursorPosition] === groupSeparator) { + splitValue.splice(cursorPosition - 1, 1); + cursorPosition -= 1; + } + // if cursor is to left of groupSeparator and delete pressed, delete the character to the right of the separator and reposition the cursor + if (lastKeyStroke === "Delete" && stateValue[cursorPosition] === groupSeparator) { + splitValue.splice(cursorPosition, 1); + cursorPosition += 1; + } + modifiedValue = splitValue.join(""); + return { modifiedValue, cursorPosition }; + } + + return { modifiedValue, cursorPosition: selectionStart }; +}; + +export const getIntlConfig = (prefix = "") => { + const currencyMatch = Object.keys(CURRENCY_MAP).filter((e) => prefix.includes(e)); + if (currencyMatch && currencyMatch?.length > 0) { + return CURRENCY_MAP[currencyMatch[0]]; + } else { + CURRENCY_MAP["₹"]; + } +}; + +const CURRENCY_MAP = { + "¥": { + locale: "ja-JP", + currency: "JPY", + }, + "₹": { + locale: "en-IN", + currency: "INR", + }, + "£": { + locale: "en-GB", + currency: "GBP", + }, + "€": { + locale: "de-DE", + currency: "EUR", + }, + $: { + locale: "en-US", + currency: "USD", + }, +}; diff --git a/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/hoc/FormComposerV2.js b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/hoc/FormComposerV2.js new file mode 100644 index 00000000000..11fb9535f61 --- /dev/null +++ b/frontend/micro-ui/web/micro-ui-internals/packages/react-components/src/hoc/FormComposerV2.js @@ -0,0 +1,906 @@ +import React, { useEffect, useMemo, useState, Fragment, useCallback } from "react"; +import { useForm, Controller } from "react-hook-form"; +import BreakLine from "../atoms/BreakLine"; +import Card from "../atoms/Card"; +import CardLabel from "../atoms/CardLabel"; +import CardText from "../atoms/CardText"; +// import CardLabelError from "../atoms/CardLabelError"; +import CardSubHeader from "../atoms/CardSubHeader"; +import CardSectionHeader from "../atoms/CardSectionHeader"; +import CardLabelDesc from "../atoms/CardLabelDesc"; +import CardLabelError from "../atoms/CardLabelError"; +import TextArea from "../atoms/TextArea"; +import TextInput from "../atoms/TextInput"; +import ActionBar from "../atoms/ActionBar"; +import SubmitBar from "../atoms/SubmitBar"; +import LabelFieldPair from "../atoms/LabelFieldPair"; +import LinkButton from "../atoms/LinkButton"; + +import { useTranslation } from "react-i18next"; +import MobileNumber from "../atoms/MobileNumber"; +import _ from "lodash"; +import CustomDropdown from "../molecules/CustomDropdown"; +import MultiUploadWrapper from "../molecules/MultiUploadWrapper"; +import HorizontalNav from "../atoms/HorizontalNav"; +import Toast from "../atoms/Toast"; +import UploadFileComposer from "./UploadFileComposer"; +import CheckBox from "../atoms/CheckBox"; +import MultiSelectDropdown from "../atoms/MultiSelectDropdown"; +import Paragraph from "../atoms/Paragraph"; +import InputTextAmount from "../atoms/InputTextAmount"; +import LocationDropdownWrapper from "../molecules/LocationDropdownWrapper"; +import ApiDropdown from "../molecules/ApiDropdown"; +import Header from "../atoms/Header"; +import Button from "../atoms/Button"; + +import { yupResolver } from "@hookform/resolvers/yup"; +// import { validateResolver } from "./validateResolver"; +import { buildYupConfig } from "./formUtils"; + +const wrapperStyles = { + // "display":"flex", + // "flexDirection":"column", + // "justifyContent":"center", + // "padding":"2rem", + // "margin":"1rem", + // "width":"80%", + // "backgroundColor":"#FAFAFA", + // "border": "1px solid #D6D5D4" + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + border: "solid", + borderRadius: "5px", + padding: "10px", + paddingTop: "20px", + marginTop: "10px", + borderColor: "#f3f3f3", + background: "#FAFAFA", + marginBottom: "20px", +}; + +/** + * formcomposer used to render forms + * + * @author jagankumar-egov + * + * @example + * + * refer this implementation of sample file + * frontend/micro-ui/web/micro-ui-internals/packages/modules/AttendenceMgmt/src/pages/citizen/Sample.js + * + */ + +export const FormComposerV2 = (props) => { + const { t } = useTranslation(); + /* added an enhancement for validate data with schema still some issue with sometype we have to solve it*/ + const inputProps = { + // context: "contactForm", + defaultValues: props.defaultValues, + // resolver: validateResolver, + }; + if (props?.jsonSchema) { + const yupSchema = props?.jsonSchema && buildYupConfig(props?.jsonSchema, t); + inputProps["resolver"] = yupResolver(yupSchema); + } + + const { + register, + handleSubmit, + setValue, + getValues, + reset, + watch, + trigger, + control, + formState, + errors, + setError, + clearErrors, + unregister, + } = useForm(inputProps); + // console.log(formState,'formState'); + const formData = watch(); + const selectedFormCategory = props?.currentFormCategory; + const [showErrorToast, setShowErrorToast] = useState(false); + const [customToast, setCustomToast] = useState(false); + + //clear all errors if user has changed the form category. + //This is done in case user first click on submit and have errors in cat 1, switches to cat 2 and hit submit with errors + //So, he should not get error prompts from previous cat 1 on cat 2 submit. + useEffect(() => { + clearErrors(); + }, [selectedFormCategory]); + + useEffect(() => { + if (Object.keys(formState?.errors).length > 0 && formState?.submitCount > 0) { + setShowErrorToast(true); + } + }, [formState?.errors, formState?.submitCount]); + + useEffect(() => { + if ( + props?.appData && + Object.keys(props?.appData)?.length > 0 && + (!_.isEqual(props?.appData, formData) || !_.isEqual(props?.appData?.ConnectionHolderDetails?.[0], formData?.ConnectionHolderDetails?.[0])) + ) { + reset({ ...props?.appData }); + } + }, [props?.appData, formData, props?.appData?.ConnectionHolderDetails]); + + useEffect(() => { + props.getFormAccessors && props.getFormAccessors({ setValue, getValues }); + }, []); + + useEffect(() => { + setCustomToast(props?.customToast); + }, [props?.customToast]); + function onSubmit(data) { + props.onSubmit(data, setValue); + } + + function onSecondayActionClick(data) { + props.onSecondayActionClick(); + } + + useEffect(() => { + props.onFormValueChange && props.onFormValueChange(setValue, formData, formState, reset, setError, clearErrors, trigger, getValues); + }, [formData]); + + const fieldSelector = (type, populators, isMandatory, disable = false, component, config, sectionFormCategory) => { + let disableFormValidation = false; + // disable form validation if section category does not matches with the current category + // this will avoid validation for the other categories other than the current category. + // sectionFormCategory comes as part of section config and selectedFormCategory is a state managed by the FormComposer consumer. + if (sectionFormCategory && selectedFormCategory) { + disableFormValidation = sectionFormCategory !== selectedFormCategory ? true : false; + } + const Component = typeof component === "string" ? Digit.ComponentRegistryService.getComponent(component) : component; + const customValidation = config?.populators?.validation?.customValidation; + const customRules = customValidation ? { validate: customValidation } : {}; + const customProps = config?.customProps; + switch (type) { + case "date": + case "text": + case "number": + case "password": + case "time": + // if (populators.defaultValue) setTimeout(setValue(populators?.name, populators.defaultValue)); + return ( +
+ {populators?.componentInFront ? ( + {populators.componentInFront} + ) : null} + ( + + )} + name={populators.name} + rules={!disableFormValidation ? { required: isMandatory, ...populators.validation, ...customRules } : {}} + control={control} + /> +
+ ); + case "amount": + // if (populators.defaultValue) setTimeout(setValue(populators?.name, populators.defaultValue)); + return ( +
+ {populators?.componentInFront ? ( + {populators.componentInFront} + ) : null} + ( + + )} + name={populators.name} + rules={!disableFormValidation ? { required: isMandatory, ...populators.validation, ...customRules } : {}} + control={control} + /> +
+ ); + case "textarea": + // if (populators.defaultValue) setTimeout(setValue(populators?.name, populators.defaultValue)); + return ( + ( +