Extracted the common logic of input components and moved it to the AppBuilder folder

This commit is contained in:
Kavin Venkatachalam 2024-11-25 14:41:56 +05:30 committed by devanshu052000
parent e4ffbfe21d
commit 9572644a53
6 changed files with 612 additions and 3 deletions

View file

@ -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 (
<>
<div
data-cy={`label-${String(componentName).toLowerCase()}`}
className={`text-input d-flex ${
defaultAlignment === 'top' &&
((width != 0 && label?.length != 0) || (auto && width == 0 && label && label?.length != 0))
? 'flex-column'
: 'align-items-center'
} ${direction === 'right' && defaultAlignment === 'side' ? 'flex-row-reverse' : ''}
${direction === 'right' && defaultAlignment === 'top' ? 'text-right' : ''}
${visibility || 'invisible'}`}
style={{
position: 'relative',
whiteSpace: 'nowrap',
width: '100%',
}}
>
<Label
label={label}
width={width}
labelRef={labelRef}
darkMode={darkMode}
color={color}
defaultAlignment={defaultAlignment}
direction={direction}
auto={auto}
isMandatory={isMandatory}
_width={_width}
labelWidth={labelWidth}
/>
{showLeftIcon && (
<IconElement
data-cy={'text-input-icon'}
style={{
width: '16px',
height: '16px',
left:
direction === 'right'
? '11px'
: defaultAlignment === 'top'
? '11px'
: (label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)
? `${labelWidth + 11}px`
: '11px',
position: 'absolute',
top:
defaultAlignment === 'side'
? '50%'
: (label?.length > 0 && width > 0) || (auto && width == 0 && label && label?.length != 0)
? 'calc(50% + 10px)'
: '50%',
transform: 'translateY(-50%)',
color: iconColor !== '#CFD3D859' ? iconColor : 'var(--icons-weak-disabled)',
zIndex: 3,
}}
stroke={1.5}
/>
)}
<input
data-cy={dataCy}
ref={inputRef}
type={inputType}
className={`tj-text-input-widget ${
!isValid && showValidationError ? 'is-invalid' : ''
} validation-without-icon`}
value={value}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyUp={handleKeyUp}
disabled={disable || loading}
placeholder={placeholder}
style={finalStyles}
{...additionalInputProps}
/>
{rightIcon}
{loading && <Loader style={loaderStyle} width="16" />}
</div>
{showValidationError && visibility && (
<div
data-cy={`${String(componentName).toLowerCase()}-invalid-feedback`}
style={{
color: errTextColor !== '#D72D39' ? errTextColor : 'var(--status-error-strong)',
textAlign: direction == 'left' && 'end',
fontSize: '11px',
fontWeight: '400',
lineHeight: '16px',
}}
>
{validationError}
</div>
)}
</>
);
};

View file

@ -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,
};
};

View file

@ -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 && (
<div
style={{
position: 'absolute',
right:
inputLogic.labelWidth === 0
? 0
: props.styles.alignment === 'side' && props.styles.direction === 'right'
? `${inputLogic.labelWidth}px`
: 0,
top: 0,
height: '100%',
width: '20px',
display: 'flex',
flexDirection: 'column',
zIndex: 2,
}}
>
<div
onClick={handleIncrement}
style={{
height: '50%',
cursor: 'pointer',
position: 'relative',
}}
>
<SolidIcon
width={props.styles.padding === 'default' ? `${props.height / 2 - 1}px` : `${props.height / 2 + 1}px`}
height={props.styles.padding === 'default' ? `${props.height / 2 - 1}px` : `${props.height / 2 + 1}px`}
fill={'var(--icons-default)'}
style={{
position: 'absolute',
top:
props.styles.alignment === 'top' && props.properties.label?.length > 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"
/>
</div>
<div
onClick={handleDecrement}
style={{
height: '50%',
cursor: 'pointer',
position: 'relative',
}}
>
<SolidIcon
fill={'var(--icons-default)'}
style={{
position: 'absolute',
right: '1px',
bottom: '1px',
borderLeft:
inputLogic.disable || inputLogic.loading
? '1px solid var(--borders-weak-disabled)'
: '1px solid var(--borders-default)',
borderTop:
inputLogic.disable || inputLogic.loading
? '1px solid var(--borders-weak-disabled)'
: '.5px solid var(--borders-default)',
borderBottomRightRadius: props.styles.borderRadius - 1,
backgroundColor: 'transparent',
}}
width={props.styles.padding === 'default' ? `${props.height / 2 - 1}px` : `${props.height / 2 + 1}px`}
height={props.styles.padding === 'default' ? `${props.height / 2 - 1}px` : `${props.height / 2 + 1}px`}
className="numberinput-down-arrow arrow number-input-arrow"
name="TriangleUpCenter"
/>
</div>
</div>
);
return (
<BaseInput
{...props}
{...inputLogic}
inputType="number"
handleChange={handleChange}
handleBlur={handleBlur}
additionalInputProps={{
min: props.validation?.minValue ?? null,
max: props.validation?.maxValue ?? null,
}}
rightIcon={numberControls}
getCustomStyles={getCustomStyles}
/>
);
};

View file

@ -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 = (
<div
onClick={toggleVisibility}
style={{
width: '16px',
height: '16px',
position: 'absolute',
right: '11px',
display: 'flex',
zIndex: 3,
}}
>
<SolidIcon
width={16}
fill={'var(--icons-weak-disabled)'}
className="password-component-eye"
name={!inputLogic.iconVisibility ? 'eye1' : 'eyedisable'}
/>
</div>
);
return (
<BaseInput
{...props}
{...inputLogic}
inputType={inputLogic.iconVisibility ? 'text' : 'password'}
additionalInputProps={{ autoComplete: 'new-password' }}
rightIcon={!inputLogic.loading && passwordIcon}
/>
);
};

View file

@ -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 <BaseInput {...props} {...inputLogic} inputType="text" />;
};

View file

@ -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';