Skip to content

Commit a946c6a

Browse files
sid0-0sid0-0bosiraphaelFelixMalfait
authored
fix: validate emails in record-fields (#7245)
fix: #7149 Introduced a minimal field validation framework for record-fields. Currently only shows errors for email field. <img width="350" alt="image" src="https://github.com/user-attachments/assets/1a1fa790-71a4-4764-a791-9878be3274f1"> <img width="347" alt="image" src="https://github.com/user-attachments/assets/e22d24f2-d1a7-4303-8c41-7aac3cde9ce8"> --------- Co-authored-by: sid0-0 <[email protected]> Co-authored-by: bosiraphael <[email protected]> Co-authored-by: Félix Malfait <[email protected]>
1 parent 0457914 commit a946c6a

File tree

5 files changed

+87
-31
lines changed

5 files changed

+87
-31
lines changed

packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField';
22
import { EmailsFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem';
3-
import { useMemo } from 'react';
3+
import { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema';
4+
import { useCallback, useMemo } from 'react';
45
import { isDefined } from 'twenty-ui';
56
import { FieldMetadataType } from '~/generated-metadata/graphql';
67
import { MultiItemFieldInput } from './MultiItemFieldInput';
@@ -29,6 +30,14 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => {
2930
});
3031
};
3132

33+
const validateInput = useCallback(
34+
(input: string) => ({
35+
isValid: emailSchema.safeParse(input).success,
36+
errorMessage: '',
37+
}),
38+
[],
39+
);
40+
3241
const isPrimaryEmail = (index: number) => index === 0 && emails?.length > 1;
3342

3443
return (
@@ -38,6 +47,7 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => {
3847
onCancel={onCancel}
3948
placeholder="Email"
4049
fieldMetadataType={FieldMetadataType.Emails}
50+
validateInput={validateInput}
4151
renderItem={({
4252
value: email,
4353
index,

packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => {
5151
onCancel={onCancel}
5252
placeholder="URL"
5353
fieldMetadataType={FieldMetadataType.Links}
54-
validateInput={(input) => absoluteUrlSchema.safeParse(input).success}
54+
validateInput={(input) => ({
55+
isValid: absoluteUrlSchema.safeParse(input).success,
56+
errorMessage: '',
57+
})}
5558
formatInput={(input) => ({ url: input, label: '' })}
5659
renderItem={({
5760
value: link,

packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx

+23-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type MultiItemFieldInputProps<T> = {
3030
onPersist: (updatedItems: T[]) => void;
3131
onCancel?: () => void;
3232
placeholder: string;
33-
validateInput?: (input: string) => boolean;
33+
validateInput?: (input: string) => { isValid: boolean; errorMessage: string };
3434
formatInput?: (input: string) => T;
3535
renderItem: (props: {
3636
value: T;
@@ -74,8 +74,21 @@ export const MultiItemFieldInput = <T,>({
7474
const [isInputDisplayed, setIsInputDisplayed] = useState(false);
7575
const [inputValue, setInputValue] = useState('');
7676
const [itemToEditIndex, setItemToEditIndex] = useState(-1);
77+
const [errorData, setErrorData] = useState({
78+
isValid: true,
79+
errorMessage: '',
80+
});
7781
const isAddingNewItem = itemToEditIndex === -1;
7882

83+
const handleOnChange = (value: string) => {
84+
setInputValue(value);
85+
if (!validateInput) return;
86+
87+
if (errorData.isValid) {
88+
setErrorData(errorData);
89+
}
90+
};
91+
7992
const handleAddButtonClick = () => {
8093
setItemToEditIndex(-1);
8194
setIsInputDisplayed(true);
@@ -105,7 +118,13 @@ export const MultiItemFieldInput = <T,>({
105118
};
106119

107120
const handleSubmitInput = () => {
108-
if (validateInput !== undefined && !validateInput(inputValue)) return;
121+
if (validateInput !== undefined) {
122+
const validationData = validateInput(inputValue) ?? { isValid: true };
123+
if (!validationData.isValid) {
124+
setErrorData(validationData);
125+
return;
126+
}
127+
}
109128

110129
const newItem = formatInput
111130
? formatInput(inputValue)
@@ -160,6 +179,7 @@ export const MultiItemFieldInput = <T,>({
160179
placeholder={placeholder}
161180
value={inputValue}
162181
hotkeyScope={hotkeyScope}
182+
hasError={!errorData.isValid}
163183
renderInput={
164184
renderInput
165185
? (props) =>
@@ -170,7 +190,7 @@ export const MultiItemFieldInput = <T,>({
170190
})
171191
: undefined
172192
}
173-
onChange={(event) => setInputValue(event.target.value)}
193+
onChange={(event) => handleOnChange(event.target.value)}
174194
onEnter={handleSubmitInput}
175195
rightComponent={
176196
<LightIconButton
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { z } from 'zod';
2+
3+
export const emailSchema = z.string().email();

packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuInput.tsx

+46-26
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import { RGBA, TEXT_INPUT_STYLE } from 'twenty-ui';
77
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
88
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
99

10-
const StyledInput = styled.input<{ withRightComponent?: boolean }>`
10+
const StyledInput = styled.input<{
11+
withRightComponent?: boolean;
12+
hasError?: boolean;
13+
}>`
1114
${TEXT_INPUT_STYLE}
1215
13-
border: 1px solid ${({ theme }) => theme.border.color.medium};
16+
border: 1px solid ${({ theme, hasError }) =>
17+
hasError ? theme.border.color.danger : theme.border.color.medium};
1418
border-radius: ${({ theme }) => theme.border.radius.sm};
1519
box-sizing: border-box;
1620
font-weight: ${({ theme }) => theme.font.weight.medium};
@@ -19,8 +23,10 @@ const StyledInput = styled.input<{ withRightComponent?: boolean }>`
1923
width: 100%;
2024
2125
&:focus {
22-
border-color: ${({ theme }) => theme.color.blue};
23-
box-shadow: 0px 0px 0px 3px ${({ theme }) => RGBA(theme.color.blue, 0.1)};
26+
${({ theme, hasError = false }) => {
27+
if (hasError) return '';
28+
return `box-shadow: 0px 0px 0px 3px ${RGBA(theme.color.blue, 0.1)}`;
29+
}};
2430
}
2531
2632
${({ withRightComponent }) =>
@@ -44,6 +50,12 @@ const StyledRightContainer = styled.div`
4450
transform: translateY(-50%);
4551
`;
4652

53+
const StyledErrorDiv = styled.div`
54+
color: ${({ theme }) => theme.color.red};
55+
padding: 0 ${({ theme }) => theme.spacing(2)}
56+
${({ theme }) => theme.spacing(1)};
57+
`;
58+
4759
type HTMLInputProps = InputHTMLAttributes<HTMLInputElement>;
4860

4961
export type DropdownMenuInputProps = HTMLInputProps & {
@@ -60,6 +72,8 @@ export type DropdownMenuInputProps = HTMLInputProps & {
6072
autoFocus: HTMLInputProps['autoFocus'];
6173
placeholder: HTMLInputProps['placeholder'];
6274
}) => React.ReactNode;
75+
error?: string | null;
76+
hasError?: boolean;
6377
};
6478

6579
export const DropdownMenuInput = forwardRef<
@@ -81,6 +95,8 @@ export const DropdownMenuInput = forwardRef<
8195
onTab,
8296
rightComponent,
8397
renderInput,
98+
error = '',
99+
hasError = false,
84100
},
85101
ref,
86102
) => {
@@ -99,28 +115,32 @@ export const DropdownMenuInput = forwardRef<
99115
});
100116

101117
return (
102-
<StyledInputContainer className={className}>
103-
{renderInput ? (
104-
renderInput({
105-
value,
106-
onChange,
107-
autoFocus,
108-
placeholder,
109-
})
110-
) : (
111-
<StyledInput
112-
autoFocus={autoFocus}
113-
value={value}
114-
placeholder={placeholder}
115-
onChange={onChange}
116-
ref={combinedRef}
117-
withRightComponent={!!rightComponent}
118-
/>
119-
)}
120-
{!!rightComponent && (
121-
<StyledRightContainer>{rightComponent}</StyledRightContainer>
122-
)}
123-
</StyledInputContainer>
118+
<>
119+
<StyledInputContainer className={className}>
120+
{renderInput ? (
121+
renderInput({
122+
value,
123+
onChange,
124+
autoFocus,
125+
placeholder,
126+
})
127+
) : (
128+
<StyledInput
129+
hasError={hasError}
130+
autoFocus={autoFocus}
131+
value={value}
132+
placeholder={placeholder}
133+
onChange={onChange}
134+
ref={combinedRef}
135+
withRightComponent={!!rightComponent}
136+
/>
137+
)}
138+
{!!rightComponent && (
139+
<StyledRightContainer>{rightComponent}</StyledRightContainer>
140+
)}
141+
</StyledInputContainer>
142+
{error && <StyledErrorDiv>{error}</StyledErrorDiv>}
143+
</>
124144
);
125145
},
126146
);

0 commit comments

Comments
 (0)