Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wizard: add user information step (HMS-4903) #2551

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/Components/CreateImageWizard/CreateImageWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,6 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
const firstBootValidation = useFirstBootValidation();
// Details
const detailsValidation = useDetailsValidation();

let startIndex = 1; // default index
if (isEdit) {
startIndex = 20;
Expand Down
200 changes: 175 additions & 25 deletions src/Components/CreateImageWizard/ValidatedTextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import React, { useState } from 'react';
import React, { ChangeEvent, useState } from 'react';

import {
HelperText,
HelperTextItem,
TextInput,
TextInputProps,
Button,
TextAreaProps,
TextArea,
} from '@patternfly/react-core';
import { EyeSlashIcon, EyeIcon } from '@patternfly/react-icons';

import type { StepValidation } from './utilities/useValidation';

Expand All @@ -30,6 +34,62 @@ interface HookValidatedTextInputPropTypes extends TextInputProps {
warning?: string;
}

interface HookValidatedTextAreaPropTypes extends TextAreaProps {
dataTestId?: string | undefined;
ariaLabel: string | undefined;
value: string;
placeholder?: string;
stepValidation: StepValidation;
fieldName: string;
isEmpty: boolean;
}

interface HookValidatedTextInputWithButtonPropTypes
extends HookValidatedTextInputPropTypes {
togglePasswordVisibility: () => void;
isPasswordVisible: boolean;
isEmpty: boolean;
}

const getValidationState = (
value: string,
stepValidation: StepValidation,
fieldName: string,
isPristine: boolean,
isEmpty?: boolean
): 'default' | 'error' | 'success' => {
if (isEmpty) return 'default';
if (isPristine) return 'default';
if (stepValidation.errors[fieldName] === 'default') return 'default';
return stepValidation.errors[fieldName] ? 'error' : 'success';
};

const ErrorHelperText = ({
errorMessage,
}: {
errorMessage?: string | undefined;
}) => {
if (!errorMessage) return null;

return (
<HelperText>
<HelperTextItem variant="error" hasIcon>
{errorMessage}
</HelperTextItem>
</HelperText>
);
};

const usePristineState = (initialValue: string) => {
const [isPristine, setIsPristine] = useState(!initialValue);

const handleBlur = () => {
setIsPristine(false);
};

return { isPristine, handleBlur };
};

export const HookValidatedInput = ({
dataTestId,
ouiaId,
Expand All @@ -43,20 +103,21 @@ export const HookValidatedInput = ({
type = 'text',
warning = undefined,
}: HookValidatedTextInputPropTypes) => {
const [isPristine, setIsPristine] = useState(!value ? true : false);
const { isPristine, handleBlur } = usePristineState(value);
// Do not surface validation on pristine state components
// Allow step validation to be set on pristine state, when needed
const validated = isPristine
? 'default'
: stepValidation.errors[fieldName] === 'default'
? 'default'
: stepValidation.errors[fieldName]
? 'error'
: 'success';

const handleBlur = () => {
setIsPristine(false);
};
const errorMessage =
!isPristine && stepValidation.errors[fieldName]
? stepValidation.errors[fieldName]
: undefined;

const validationState = getValidationState(
value,
stepValidation,
fieldName,
isPristine
);

return (
<>
Expand All @@ -66,19 +127,13 @@ export const HookValidatedInput = ({
ouiaId={ouiaId || ''}
type={type}
onChange={onChange!}
validated={validated}
validated={validationState}
aria-label={ariaLabel || ''}
onBlur={handleBlur}
placeholder={placeholder || ''}
isDisabled={isDisabled || false}
/>
{validated === 'error' && (
<HelperText>
<HelperTextItem variant="error" hasIcon>
{stepValidation.errors[fieldName]}
</HelperTextItem>
</HelperText>
)}
<ErrorHelperText errorMessage={errorMessage} />
{warning !== undefined && (
<HelperText>
<HelperTextItem variant="warning" hasIcon>
Expand All @@ -90,6 +145,57 @@ export const HookValidatedInput = ({
);
};

export const HookValidatedInputWithPasswordVisibilityButton = (
props: HookValidatedTextInputWithButtonPropTypes
) => {
const { togglePasswordVisibility, isPasswordVisible, ...restProps } = props;
const { isPristine, handleBlur } = usePristineState(restProps.value);

const errorMessage =
!isPristine && restProps.stepValidation.errors[restProps.fieldName]
? restProps.stepValidation.errors[restProps.fieldName]
: undefined;

const validationState = getValidationState(
restProps.value,
restProps.stepValidation,
restProps.fieldName,
isPristine
);
return (
<>
<div
style={{ position: 'relative', display: 'flex', alignItems: 'center' }}
>
<TextInput
value={restProps.value}
data-testid={restProps.dataTestId}
onChange={restProps.onChange!}
ouiaId={restProps.ouiaId || ''}
aria-label={restProps.ariaLabel || ''}
validated={validationState}
type={restProps.type || 'text'}
placeholder={restProps.placeholder || ''}
onBlur={handleBlur}
style={{ paddingRight: '2rem' }}
/>
<Button
variant="plain"
onClick={togglePasswordVisibility}
aria-label="Toggle password visibility"
style={{
position: 'absolute',
right: '0.5rem',
}}
>
{isPasswordVisible ? <EyeSlashIcon /> : <EyeIcon />}
</Button>
</div>
<ErrorHelperText errorMessage={errorMessage} />
</>
);
};

export const ValidatedTextInput = ({
dataTestId,
ouiaId,
Expand All @@ -100,11 +206,7 @@ export const ValidatedTextInput = ({
placeholder,
onChange,
}: ValidatedTextInputPropTypes) => {
const [isPristine, setIsPristine] = useState(!value ? true : false);

const handleBlur = () => {
setIsPristine(false);
};
const { isPristine, handleBlur } = usePristineState(value);

const handleValidation = () => {
// Prevent premature validation during user's first entry
Expand Down Expand Up @@ -137,3 +239,51 @@ export const ValidatedTextInput = ({
</>
);
};

export const HookValidatedTextArea = ({
dataTestId,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also has a lot of duplicate code. Let’s try to DRY this out too. Consider extracting common logic like the validated const into a new function.

Copy link
Collaborator Author

@mgold1234 mgold1234 Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HookValidatedTextAreaPropTypes extend the TextAreaProps and its different from TextInputProps
so I am not sure we can reuse here

ariaLabel,
value,
placeholder,
onChange,
stepValidation,
fieldName,
type = 'text',
isDisabled = false,
isEmpty,
}: HookValidatedTextAreaPropTypes) => {
const { isPristine, handleBlur } = usePristineState(value);
const validationState = getValidationState(
value,
stepValidation,
fieldName,
isPristine,
isEmpty
);
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
if (onChange) {
onChange(event, event.target.value);
}
};
const errorMessage =
!isPristine && stepValidation.errors[fieldName]
? stepValidation.errors[fieldName]
: undefined;

return (
<>
<TextArea
value={value}
data-testid={dataTestId}
type={type}
onChange={handleChange}
validated={validationState}
aria-label={ariaLabel || ''}
onBlur={handleBlur}
placeholder={placeholder}
isDisabled={isDisabled}
/>
<ErrorHelperText errorMessage={errorMessage} />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,22 @@ import {
} from '@patternfly/react-core';
import UserIcon from '@patternfly/react-icons/dist/esm/icons/user-icon';

import { useAppDispatch } from '../../../../../store/hooks';
import { addUser } from '../../../../../store/wizardSlice';

const EmptyUserState = () => {
const dispatch = useAppDispatch();
const onAddUserClick = () => {
dispatch(addUser());
};
return (
<EmptyState variant={EmptyStateVariant.lg}>
<EmptyStateHeader
icon={<EmptyStateIcon icon={UserIcon} />}
headingLevel="h4"
/>
<EmptyStateFooter>
<Button variant="secondary" onClick={() => {}}>
<Button variant="secondary" onClick={onAddUserClick}>
mgold1234 marked this conversation as resolved.
Show resolved Hide resolved
Add a user
</Button>
</EmptyStateFooter>
Expand Down
Loading
Loading