Wizard: Refactor HookValidatedInput component

This commit splits the HookValidatedInput component into three separate functions to improve modularity and readability:

- `getValidationState`: Calculates the validation state ('default', 'success', 'error') based on whether the input is pristine and if there is an error message.
- `ValidatedInputAndTextArea`: Renders the TextInput or TextArea component, utilizing the `getValidationState` output.
- `ErrorMessage`: Displays validation error messages.

This refactoring enhances code maintainability and testability, and the updated structure is now implemented for the username field.
This commit is contained in:
Michal Gold 2025-02-20 12:28:04 +02:00 committed by Lucas Garfield
parent ba233f2c69
commit 49fa0ee735
4 changed files with 101 additions and 11 deletions

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import {
HelperText, HelperText,
@ -15,7 +15,7 @@ import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
import type { StepValidation } from './utilities/useValidation'; import type { StepValidation } from './utilities/useValidation';
interface ValidatedTextInputPropTypes extends TextInputProps { type ValidatedTextInputPropTypes = TextInputProps & {
dataTestId?: string | undefined; dataTestId?: string | undefined;
ouiaId?: string; ouiaId?: string;
ariaLabel: string | undefined; ariaLabel: string | undefined;
@ -23,7 +23,7 @@ interface ValidatedTextInputPropTypes extends TextInputProps {
validator: (value: string | undefined) => boolean; validator: (value: string | undefined) => boolean;
value: string; value: string;
placeholder?: string; placeholder?: string;
} };
type HookValidatedInputPropTypes = TextInputProps & type HookValidatedInputPropTypes = TextInputProps &
TextAreaProps & { TextAreaProps & {
@ -38,6 +38,26 @@ type HookValidatedInputPropTypes = TextInputProps &
inputType?: 'textInput' | 'textArea'; inputType?: 'textInput' | 'textArea';
}; };
type ValidationInputProp = TextInputProps &
TextAreaProps & {
value: string;
placeholder: string;
stepValidation: StepValidation;
fieldName: string;
inputType?: 'textInput' | 'textArea';
ariaLabel: string;
onChange: (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
value: string
) => void;
};
type ErrorMessageProps = {
errorMessage: string;
};
type ValidationResult = 'default' | 'success' | 'error';
export const HookPasswordValidatedInput = ({ export const HookPasswordValidatedInput = ({
ariaLabel, ariaLabel,
placeholder, placeholder,
@ -89,6 +109,78 @@ export const HookPasswordValidatedInput = ({
); );
}; };
export const ValidatedInputAndTextArea = ({
value,
stepValidation,
fieldName,
placeholder,
onChange,
ariaLabel,
inputType = 'textInput',
}: ValidationInputProp) => {
const errorMessage = stepValidation.errors[fieldName];
const hasError = errorMessage !== '';
const [isPristine, setIsPristine] = useState(!value);
const validated = getValidationState(isPristine, errorMessage);
const handleBlur = () => {
if (value) {
setIsPristine(false);
}
};
useEffect(() => {
if (!value) {
setIsPristine(true);
}
}, [value, setIsPristine]);
return (
<>
{inputType === 'textArea' ? (
<TextArea
value={value}
onChange={onChange}
validated={validated}
onBlur={handleBlur}
placeholder={placeholder}
aria-label={ariaLabel}
/>
) : (
<TextInput
value={value}
onChange={onChange}
validated={validated}
onBlur={handleBlur}
placeholder={placeholder}
aria-label={ariaLabel}
/>
)}
{hasError && <ErrorMessage errorMessage={errorMessage} />}
</>
);
};
const getValidationState = (
isPristine: boolean,
errorMessage: string
): ValidationResult => {
const validated = isPristine ? 'default' : errorMessage ? 'error' : 'success';
return validated;
};
export const ErrorMessage = ({ errorMessage }: ErrorMessageProps) => {
return (
<HelperText>
<HelperTextItem variant="error" hasIcon>
{errorMessage}
</HelperTextItem>
</HelperText>
);
};
export const HookValidatedInput = ({ export const HookValidatedInput = ({
dataTestId, dataTestId,
ouiaId, ouiaId,

View file

@ -20,6 +20,7 @@ import { useUsersValidation } from '../../../utilities/useValidation';
import { import {
HookPasswordValidatedInput, HookPasswordValidatedInput,
HookValidatedInput, HookValidatedInput,
ValidatedInputAndTextArea,
} from '../../../ValidatedInput'; } from '../../../ValidatedInput';
const UserInfo = () => { const UserInfo = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -72,7 +73,7 @@ const UserInfo = () => {
return ( return (
<> <>
<FormGroup isRequired label="Username"> <FormGroup isRequired label="Username">
<HookValidatedInput <ValidatedInputAndTextArea
ariaLabel="blueprint user name" ariaLabel="blueprint user name"
value={userName || ''} value={userName || ''}
placeholder="Enter username" placeholder="Enter username"

View file

@ -414,12 +414,10 @@ export function useUsersValidation(): StepValidation {
return { return {
errors: { errors: {
userName: !isUserNameValid(userName) ? 'Invalid user name' : '', userName:
userSshKey: !userSshKey userName && !isUserNameValid(userName) ? 'Invalid user name' : '',
? '' userSshKey:
: !isSshKeyValid(userSshKey) userSshKey && !isSshKeyValid(userSshKey) ? 'Invalid SSH key' : '',
? 'Invalid SSH key'
: '',
}, },
disabledNext: !canProceed, disabledNext: !canProceed,
}; };

View file

@ -58,7 +58,6 @@ export const isFileSystemConfigValid = (partitions: Partition[]) => {
}; };
export const isUserNameValid = (userName: string) => { export const isUserNameValid = (userName: string) => {
if (userName === undefined) return false;
const isLengthValid = userName.length <= 32; const isLengthValid = userName.length <= 32;
const isNotNumericOnly = !/^\d+$/.test(userName); const isNotNumericOnly = !/^\d+$/.test(userName);
const isPatternValid = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*[a-zA-Z0-9_$]$/.test( const isPatternValid = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*[a-zA-Z0-9_$]$/.test(