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.
---------
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}}
+ >
);
},
);