From 9572644a53bd2d11aec5c734694a632fb5cfd443 Mon Sep 17 00:00:00 2001 From: Kavin Venkatachalam Date: Mon, 25 Nov 2024 14:41:56 +0530 Subject: [PATCH] Extracted the common logic of input components and moved it to the AppBuilder folder --- .../Widgets/BaseComponents/BaseInput.jsx | 215 ++++++++++++++++++ .../Widgets/BaseComponents/hooks/useInput.js | 179 +++++++++++++++ .../src/AppBuilder/Widgets/NumberInput.jsx | 163 +++++++++++++ .../src/AppBuilder/Widgets/PasswordInput.jsx | 43 ++++ frontend/src/AppBuilder/Widgets/TextInput.jsx | 9 + .../src/AppBuilder/_helpers/editorHelpers.js | 6 +- 6 files changed, 612 insertions(+), 3 deletions(-) create mode 100644 frontend/src/AppBuilder/Widgets/BaseComponents/BaseInput.jsx create mode 100644 frontend/src/AppBuilder/Widgets/BaseComponents/hooks/useInput.js create mode 100644 frontend/src/AppBuilder/Widgets/NumberInput.jsx create mode 100644 frontend/src/AppBuilder/Widgets/PasswordInput.jsx create mode 100644 frontend/src/AppBuilder/Widgets/TextInput.jsx diff --git a/frontend/src/AppBuilder/Widgets/BaseComponents/BaseInput.jsx b/frontend/src/AppBuilder/Widgets/BaseComponents/BaseInput.jsx new file mode 100644 index 0000000000..2247d11c03 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/BaseComponents/BaseInput.jsx @@ -0,0 +1,215 @@ +import React from 'react'; +import Label from '@/_ui/Label'; +import Loader from '@/ToolJetUI/Loader/Loader'; +import * as Icons from '@tabler/icons-react'; +const tinycolor = require('tinycolor2'); + +export const BaseInput = ({ + height, + styles, + properties, + darkMode, + componentName, + dataCy, + // From useInput hook + inputRef, + labelRef, + visibility, + loading, + labelWidth, + validationError, + showValidationError, + isFocused, + isMandatory, + disable, + value, + handleChange, + handleBlur, + handleFocus, + handleKeyUp, + isValid, + // Input specific props + inputType = 'text', + additionalInputProps = {}, + rightIcon, + getCustomStyles, +}) => { + const { + padding, + borderRadius, + borderColor, + backgroundColor, + textColor, + boxShadow, + width, + alignment, + direction, + color, + auto, + errTextColor, + iconColor, + accentColor, + iconVisibility: showLeftIcon, + icon, + } = styles; + + const { label, placeholder } = properties; + const _width = (width / 100) * 70; + const defaultAlignment = alignment === 'side' || alignment === 'top' ? alignment : 'side'; + + const computedStyles = { + height: height == 36 ? (padding == 'default' ? '36px' : '40px') : padding == 'default' ? height : height + 4, + borderRadius: `${borderRadius}px`, + color: !['#1B1F24', '#000', '#000000ff'].includes(textColor) + ? textColor + : disable || loading + ? 'var(--text-disabled)' + : 'var(--text-primary)', + borderColor: isFocused + ? accentColor != '4368E3' + ? accentColor + : 'var(--primary-accent-strong)' + : borderColor != '#CCD1D5' + ? borderColor + : disable || loading + ? '1px solid var(--borders-disabled-on-white)' + : 'var(--borders-default)', + '--tblr-input-border-color-darker': tinycolor(borderColor).darken(24).toString(), + backgroundColor: + backgroundColor != '#fff' + ? backgroundColor + : disable || loading + ? darkMode + ? 'var(--surfaces-app-bg-default)' + : 'var(--surfaces-surface-03)' + : 'var(--surfaces-surface-01)', + boxShadow, + padding: showLeftIcon ? '8px 10px 8px 29px' : '8px 10px', + overflow: 'hidden', + textOverflow: 'ellipsis', + }; + + const loaderStyle = { + right: + direction === 'right' && + defaultAlignment === 'side' && + ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) + ? `${labelWidth + 11}px` + : '11px', + top: + defaultAlignment === 'top' + ? ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) && + 'calc(50% + 10px)' + : '', + transform: + defaultAlignment === 'top' && + ((label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)) && + ' translateY(-50%)', + zIndex: 3, + }; + + // eslint-disable-next-line import/namespace + const IconElement = Icons[icon] ?? Icons['IconHome2']; + + const finalStyles = getCustomStyles ? getCustomStyles(computedStyles) : computedStyles; + + return ( + <> +
+
+ + {showValidationError && visibility && ( +
+ {validationError} +
+ )} + + ); +}; diff --git a/frontend/src/AppBuilder/Widgets/BaseComponents/hooks/useInput.js b/frontend/src/AppBuilder/Widgets/BaseComponents/hooks/useInput.js new file mode 100644 index 0000000000..f298265cc7 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/BaseComponents/hooks/useInput.js @@ -0,0 +1,179 @@ +import { useState, useRef, useEffect } from 'react'; +import { useGridStore } from '@/_stores/gridStore'; + +export const useInput = ({ + id, + properties, + styles, + validation, + validate, + setExposedVariable, + setExposedVariables, + fireEvent, +}) => { + const isInitialRender = useRef(true); + const inputRef = useRef(); + const labelRef = useRef(); + + const { loadingState, disabledState, label, visibility: initialVisibility } = properties; + const isResizing = useGridStore((state) => state.resizingComponentId === id); + + const [value, setValue] = useState(properties.value ?? ''); + const [visibility, setVisibility] = useState(initialVisibility); + const [loading, setLoading] = useState(loadingState); + const [disable, setDisable] = useState(disabledState || loadingState); + const [validationStatus, setValidationStatus] = useState(validate(value)); + const [showValidationError, setShowValidationError] = useState(false); + const [isFocused, setIsFocused] = useState(false); + const [labelWidth, setLabelWidth] = useState(0); + const [iconVisibility, setIconVisibility] = useState(false); + + const { isValid, validationError } = validationStatus; + const isMandatory = validation?.mandatory ?? false; + + useEffect(() => { + if (labelRef?.current) { + const absolutewidth = labelRef?.current?.getBoundingClientRect()?.width; + setLabelWidth(absolutewidth); + } else setLabelWidth(0); + }, [ + isResizing, + styles.width, + styles.auto, + styles.alignment, + styles.iconVisibility, + label?.length, + isMandatory, + styles.padding, + styles.direction, + labelRef?.current?.getBoundingClientRect()?.width, + ]); + + useEffect(() => { + if (isInitialRender.current) return; + setExposedVariable('label', label); + }, [label]); + + useEffect(() => { + disable !== disabledState && setDisable(disabledState); + }, [disabledState]); + + useEffect(() => { + visibility !== properties.visibility && setVisibility(properties.visibility); + }, [properties.visibility]); + + useEffect(() => { + loading !== loadingState && setLoading(loadingState); + }, [loadingState]); + + useEffect(() => { + if (isInitialRender.current) return; + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariable('isValid', validationStatus?.isValid); + }, [validate]); + + useEffect(() => { + if (isInitialRender.current) return; + setInputValue(properties.value ?? ''); + }, [properties.value]); + + useEffect(() => { + const exposedVariables = { + setText: async function (text) { + setInputValue(text); + fireEvent('onChange'); + }, + clear: async function () { + setInputValue(''); + fireEvent('onChange'); + }, + setFocus: async function () { + inputRef.current.focus(); + }, + setBlur: async function () { + inputRef.current.blur(); + }, + setVisibility: async function (state) { + setVisibility(state); + setExposedVariable('isVisible', state); + }, + setDisable: async function (disable) { + setDisable(disable); + setExposedVariable('isDisabled', disable); + }, + setLoading: async function (loading) { + setLoading(loading); + setExposedVariable('isLoading', loading); + }, + label, + isValid, + value: properties.value ?? '', + isMandatory, + isLoading: loading, + isVisible: visibility, + isDisabled: disable, + }; + + setExposedVariables(exposedVariables); + isInitialRender.current = false; + }, []); + + const setInputValue = (value) => { + setValue(value); + setExposedVariable('value', value); + const validationStatus = validate(value); + setValidationStatus(validationStatus); + setExposedVariable('isValid', validationStatus?.isValid); + }; + + const handleChange = (e) => { + setInputValue(e.target.value); + fireEvent('onChange'); + }; + + const handleBlur = (e) => { + setShowValidationError(true); + setIsFocused(false); + e.stopPropagation(); + fireEvent('onBlur'); + }; + + const handleFocus = (e) => { + setIsFocused(true); + e.stopPropagation(); + setTimeout(() => { + fireEvent('onFocus'); + }, 0); + }; + + const handleKeyUp = (e) => { + if (e.key === 'Enter') { + setInputValue(e.target.value); + fireEvent('onEnterPressed'); + } + }; + + return { + inputRef, + labelRef, + value, + visibility, + loading, + disable, + validationStatus, + showValidationError, + isFocused, + labelWidth, + iconVisibility, + setIconVisibility, + isValid, + validationError, + isMandatory, + setInputValue, + handleChange, + handleBlur, + handleFocus, + handleKeyUp, + }; +}; diff --git a/frontend/src/AppBuilder/Widgets/NumberInput.jsx b/frontend/src/AppBuilder/Widgets/NumberInput.jsx new file mode 100644 index 0000000000..0a30095621 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/NumberInput.jsx @@ -0,0 +1,163 @@ +import React from 'react'; +import { BaseInput } from './BaseComponents/BaseInput'; +import { useInput } from './BaseComponents/hooks/useInput'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; + +export const NumberInput = (props) => { + const inputLogic = useInput({ + ...props, + properties: { + ...props.properties, + value: Number(parseFloat(props.properties.value).toFixed(props.properties.decimalPlaces)), + }, + }); + + const handleChange = (e) => { + if (e.target.value === '') { + inputLogic.setInputValue(null); + props.fireEvent('onChange'); + } else { + const newValue = Number(parseFloat(e.target.value)); + inputLogic.setInputValue(newValue); + if (!isNaN(newValue)) { + props.fireEvent('onChange'); + } + } + }; + + const handleBlur = (e) => { + const value = Number(parseFloat(e.target.value).toFixed(props.properties.decimalPlaces)); + inputLogic.setInputValue(value); + e.stopPropagation(); + props.fireEvent('onBlur'); + inputLogic.setIsFocused(false); + }; + + const handleIncrement = (e) => { + e.preventDefault(); + const newValue = (inputLogic.value || 0) + 1; + inputLogic.setInputValue(newValue); + if (!isNaN(newValue)) { + props.fireEvent('onChange'); + } + }; + + const handleDecrement = (e) => { + e.preventDefault(); + const newValue = (inputLogic.value || 0) - 1; + inputLogic.setInputValue(newValue); + if (!isNaN(newValue)) { + props.fireEvent('onChange'); + } + }; + + // Override the base input styles to account for number controls + const getCustomStyles = (baseStyles) => { + return { + ...baseStyles, + paddingRight: '20px', // Make room for number controls + }; + }; + + const numberControls = !inputLogic.isResizing && ( +
+
+ 0 && props.styles.width > 0 + ? '21px' + : '1px', + right: '1px', + borderLeft: + inputLogic.disable || inputLogic.loading + ? '1px solid var(--borders-weak-disabled)' + : '1px solid var(--borders-default)', + borderBottom: + inputLogic.disable || inputLogic.loading + ? '1px solid var(--borders-weak-disabled)' + : '.5px solid var(--borders-default)', + borderTopRightRadius: props.styles.borderRadius - 1, + backgroundColor: 'transparent', + }} + className="numberinput-up-arrow arrow number-input-arrow" + name="TriangleDownCenter" + /> +
+
+ +
+
+ ); + + return ( + + ); +}; diff --git a/frontend/src/AppBuilder/Widgets/PasswordInput.jsx b/frontend/src/AppBuilder/Widgets/PasswordInput.jsx new file mode 100644 index 0000000000..6ca13f2048 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/PasswordInput.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { BaseInput } from './BaseComponents/BaseInput'; +import { useInput } from './BaseComponents/hooks/useInput'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; + +export const PasswordInput = (props) => { + const inputLogic = useInput(props); + + const toggleVisibility = () => { + inputLogic.setIconVisibility(!inputLogic.iconVisibility); + }; + + const passwordIcon = ( +
+ +
+ ); + + return ( + + ); +}; diff --git a/frontend/src/AppBuilder/Widgets/TextInput.jsx b/frontend/src/AppBuilder/Widgets/TextInput.jsx new file mode 100644 index 0000000000..119add300e --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/TextInput.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { BaseInput } from './BaseComponents/BaseInput'; +import { useInput } from './BaseComponents/hooks/useInput'; + +export const TextInput = (props) => { + const inputLogic = useInput(props); + + return ; +}; diff --git a/frontend/src/AppBuilder/_helpers/editorHelpers.js b/frontend/src/AppBuilder/_helpers/editorHelpers.js index 38e9abd955..e2b7f3428d 100644 --- a/frontend/src/AppBuilder/_helpers/editorHelpers.js +++ b/frontend/src/AppBuilder/_helpers/editorHelpers.js @@ -4,8 +4,8 @@ import { Text } from '@/Editor/Components/Text'; // import { Table } from '@/Editor/Components/Table/Table'; import { Table } from '@/AppBuilder/Widgets/Table/Table'; -import { TextInput } from '@/Editor/Components/TextInput'; -import { NumberInput } from '@/Editor/Components/NumberInput'; +import { TextInput } from '@/AppBuilder/Widgets/TextInput'; +import { NumberInput } from '@/AppBuilder/Widgets/NumberInput'; import { TextArea } from '@/Editor/Components/TextArea'; import { RichTextEditor } from '@/Editor/Components/RichTextEditor'; import { DropDown } from '@/Editor/Components/DropDown'; @@ -29,7 +29,7 @@ import { RadioButtonV2 } from '@/Editor/Components/RadioButtonV2/RadioButtonV2'; import { StarRating } from '@/Editor/Components/StarRating'; import { Divider } from '@/Editor/Components/Divider'; import { FilePicker } from '@/Editor/Components/FilePicker'; -import { PasswordInput } from '@/Editor/Components/PasswordInput'; +import { PasswordInput } from '@/AppBuilder/Widgets/PasswordInput'; // import { Calendar } from '@/Editor/Components/Calendar'; // import { Listview } from '@/Editor/Components/Listview'; import { IFrame } from '@/Editor/Components/IFrame';