mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-26 04:17:15 +03:00
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 <a@b.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
parent
04579144ca
commit
a946c6a33d
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -30,7 +30,7 @@ type MultiItemFieldInputProps<T> = {
|
||||
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 = <T,>({
|
||||
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 = <T,>({
|
||||
};
|
||||
|
||||
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 = <T,>({
|
||||
placeholder={placeholder}
|
||||
value={inputValue}
|
||||
hotkeyScope={hotkeyScope}
|
||||
hasError={!errorData.isValid}
|
||||
renderInput={
|
||||
renderInput
|
||||
? (props) =>
|
||||
@ -170,7 +190,7 @@ export const MultiItemFieldInput = <T,>({
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
onChange={(event) => handleOnChange(event.target.value)}
|
||||
onEnter={handleSubmitInput}
|
||||
rightComponent={
|
||||
<LightIconButton
|
||||
|
@ -0,0 +1,3 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const emailSchema = z.string().email();
|
@ -7,10 +7,14 @@ import { RGBA, TEXT_INPUT_STYLE } from 'twenty-ui';
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
||||
|
||||
const StyledInput = styled.input<{ withRightComponent?: boolean }>`
|
||||
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<HTMLInputElement>;
|
||||
|
||||
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 (
|
||||
<StyledInputContainer className={className}>
|
||||
{renderInput ? (
|
||||
renderInput({
|
||||
value,
|
||||
onChange,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
})
|
||||
) : (
|
||||
<StyledInput
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
ref={combinedRef}
|
||||
withRightComponent={!!rightComponent}
|
||||
/>
|
||||
)}
|
||||
{!!rightComponent && (
|
||||
<StyledRightContainer>{rightComponent}</StyledRightContainer>
|
||||
)}
|
||||
</StyledInputContainer>
|
||||
<>
|
||||
<StyledInputContainer className={className}>
|
||||
{renderInput ? (
|
||||
renderInput({
|
||||
value,
|
||||
onChange,
|
||||
autoFocus,
|
||||
placeholder,
|
||||
})
|
||||
) : (
|
||||
<StyledInput
|
||||
hasError={hasError}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
ref={combinedRef}
|
||||
withRightComponent={!!rightComponent}
|
||||
/>
|
||||
)}
|
||||
{!!rightComponent && (
|
||||
<StyledRightContainer>{rightComponent}</StyledRightContainer>
|
||||
)}
|
||||
</StyledInputContainer>
|
||||
{error && <StyledErrorDiv>{error}</StyledErrorDiv>}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user