From a946c6a33de378c1cb6818efee8f0d891208ded7 Mon Sep 17 00:00:00 2001 From: sid0-0 <43578323+sid0-0@users.noreply.github.com> Date: Thu, 3 Oct 2024 22:25:29 +0530 Subject: [PATCH] fix: validate emails in record-fields (#7245) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: #7149 Introduced a minimal field validation framework for record-fields. Currently only shows errors for email field. image image --------- Co-authored-by: sid0-0 Co-authored-by: bosiraphael Co-authored-by: FĂ©lix Malfait --- .../input/components/EmailsFieldInput.tsx | 12 +++- .../input/components/LinksFieldInput.tsx | 5 +- .../input/components/MultiItemFieldInput.tsx | 26 ++++++- .../validation-schemas/emailSchema.ts | 3 + .../dropdown/components/DropdownMenuInput.tsx | 72 ++++++++++++------- 5 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/validation-schemas/emailSchema.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx index 32418cebc6..d933aeabcd 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx @@ -1,6 +1,7 @@ import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField'; import { EmailsFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem'; -import { useMemo } from 'react'; +import { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema'; +import { useCallback, useMemo } from 'react'; import { isDefined } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { MultiItemFieldInput } from './MultiItemFieldInput'; @@ -29,6 +30,14 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => { }); }; + const validateInput = useCallback( + (input: string) => ({ + isValid: emailSchema.safeParse(input).success, + errorMessage: '', + }), + [], + ); + const isPrimaryEmail = (index: number) => index === 0 && emails?.length > 1; return ( @@ -38,6 +47,7 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => { onCancel={onCancel} placeholder="Email" fieldMetadataType={FieldMetadataType.Emails} + validateInput={validateInput} renderItem={({ value: email, index, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx index 97bc9578c9..e52cc95c04 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx @@ -51,7 +51,10 @@ export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => { onCancel={onCancel} placeholder="URL" fieldMetadataType={FieldMetadataType.Links} - validateInput={(input) => absoluteUrlSchema.safeParse(input).success} + validateInput={(input) => ({ + isValid: absoluteUrlSchema.safeParse(input).success, + errorMessage: '', + })} formatInput={(input) => ({ url: input, label: '' })} renderItem={({ value: link, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx index a73e494988..7e3e93ec2c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx @@ -30,7 +30,7 @@ type MultiItemFieldInputProps = { onPersist: (updatedItems: T[]) => void; onCancel?: () => void; placeholder: string; - validateInput?: (input: string) => boolean; + validateInput?: (input: string) => { isValid: boolean; errorMessage: string }; formatInput?: (input: string) => T; renderItem: (props: { value: T; @@ -74,8 +74,21 @@ export const MultiItemFieldInput = ({ const [isInputDisplayed, setIsInputDisplayed] = useState(false); const [inputValue, setInputValue] = useState(''); const [itemToEditIndex, setItemToEditIndex] = useState(-1); + const [errorData, setErrorData] = useState({ + isValid: true, + errorMessage: '', + }); const isAddingNewItem = itemToEditIndex === -1; + const handleOnChange = (value: string) => { + setInputValue(value); + if (!validateInput) return; + + if (errorData.isValid) { + setErrorData(errorData); + } + }; + const handleAddButtonClick = () => { setItemToEditIndex(-1); setIsInputDisplayed(true); @@ -105,7 +118,13 @@ export const MultiItemFieldInput = ({ }; const handleSubmitInput = () => { - if (validateInput !== undefined && !validateInput(inputValue)) return; + if (validateInput !== undefined) { + const validationData = validateInput(inputValue) ?? { isValid: true }; + if (!validationData.isValid) { + setErrorData(validationData); + return; + } + } const newItem = formatInput ? formatInput(inputValue) @@ -160,6 +179,7 @@ export const MultiItemFieldInput = ({ placeholder={placeholder} value={inputValue} hotkeyScope={hotkeyScope} + hasError={!errorData.isValid} renderInput={ renderInput ? (props) => @@ -170,7 +190,7 @@ export const MultiItemFieldInput = ({ }) : undefined } - onChange={(event) => setInputValue(event.target.value)} + onChange={(event) => handleOnChange(event.target.value)} onEnter={handleSubmitInput} rightComponent={ ` +const StyledInput = styled.input<{ + withRightComponent?: boolean; + hasError?: boolean; +}>` ${TEXT_INPUT_STYLE} - border: 1px solid ${({ theme }) => theme.border.color.medium}; + border: 1px solid ${({ theme, hasError }) => + hasError ? theme.border.color.danger : theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.sm}; box-sizing: border-box; font-weight: ${({ theme }) => theme.font.weight.medium}; @@ -19,8 +23,10 @@ const StyledInput = styled.input<{ withRightComponent?: boolean }>` width: 100%; &:focus { - border-color: ${({ theme }) => theme.color.blue}; - box-shadow: 0px 0px 0px 3px ${({ theme }) => RGBA(theme.color.blue, 0.1)}; + ${({ theme, hasError = false }) => { + if (hasError) return ''; + return `box-shadow: 0px 0px 0px 3px ${RGBA(theme.color.blue, 0.1)}`; + }}; } ${({ withRightComponent }) => @@ -44,6 +50,12 @@ const StyledRightContainer = styled.div` transform: translateY(-50%); `; +const StyledErrorDiv = styled.div` + color: ${({ theme }) => theme.color.red}; + padding: 0 ${({ theme }) => theme.spacing(2)} + ${({ theme }) => theme.spacing(1)}; +`; + type HTMLInputProps = InputHTMLAttributes; export type DropdownMenuInputProps = HTMLInputProps & { @@ -60,6 +72,8 @@ export type DropdownMenuInputProps = HTMLInputProps & { autoFocus: HTMLInputProps['autoFocus']; placeholder: HTMLInputProps['placeholder']; }) => React.ReactNode; + error?: string | null; + hasError?: boolean; }; export const DropdownMenuInput = forwardRef< @@ -81,6 +95,8 @@ export const DropdownMenuInput = forwardRef< onTab, rightComponent, renderInput, + error = '', + hasError = false, }, ref, ) => { @@ -99,28 +115,32 @@ export const DropdownMenuInput = forwardRef< }); return ( - - {renderInput ? ( - renderInput({ - value, - onChange, - autoFocus, - placeholder, - }) - ) : ( - - )} - {!!rightComponent && ( - {rightComponent} - )} - + <> + + {renderInput ? ( + renderInput({ + value, + onChange, + autoFocus, + placeholder, + }) + ) : ( + + )} + {!!rightComponent && ( + {rightComponent} + )} + + {error && {error}} + ); }, );