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

Add CreateNewOrgScreenBase Component #514

Merged
merged 4 commits into from
Nov 20, 2023
Merged
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
2 changes: 2 additions & 0 deletions login-workflow/example/src/navigation/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { routes } from './Routing';
import { ExampleHome } from '../screens/ExampleHome';
import i18nAppInstance from '../translations/i18n';
import { ChangePassword } from '../components/ChangePassword';
import { CreateNewOrgScreenBaseDemo } from '../screens/demo-components/CreateNewOrgScreenBaseDemo';

export const AppRouter: React.FC = () => {
const navigate = useNavigate();
Expand All @@ -42,6 +43,7 @@ export const AppRouter: React.FC = () => {
</AuthContextProvider>
}
>
<Route path={'/create-new-org-base-screen-demo'} element={<CreateNewOrgScreenBaseDemo />} />
<Route
path={'/login'}
element={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { CreateNewOrgScreenBase } from '@brightlayer-ui/react-auth-workflow';

export const CreateNewOrgScreenBaseDemo: React.FC = () => (
<CreateNewOrgScreenBase
WorkflowCardHeaderProps={{ title: 'Create An Organization' }}
WorkflowCardInstructionProps={{
instructions: 'Enter your organization name to continue with account creation.',
}}
orgNameLabel="Organization Name"
orgNameValidator={(orgName: string): boolean | string => {
if (orgName?.length > 0) {
return true;
}
return 'Please enter a valid organization name';
}}
WorkflowCardActionsProps={{
onNext: (): void => {},
showNext: true,
nextLabel: 'Next',
onPrevious: (): void => {},
showPrevious: true,
previousLabel: 'Back',
canGoNext: true,
currentStep: 0,
totalSteps: 2,
}}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React from 'react';
import '@testing-library/jest-dom';
import { cleanup, render, screen, fireEvent, RenderResult } from '@testing-library/react';
import { CreateNewOrgScreenBase } from './CreateNewOrgScreenBase';
import { CreateNewOrgScreenProps } from './types';
import { RegistrationContextProvider } from '../../contexts';
import { RegistrationWorkflow } from '../../components';
import { registrationContextProviderProps } from '../../testUtils';

afterEach(cleanup);

describe('Create New Organization Screen Base', () => {
let mockOnNext: any;

afterEach(() => {
jest.clearAllMocks();
});

beforeEach(() => {
mockOnNext = jest.fn();
});

const renderer = (props?: CreateNewOrgScreenProps): RenderResult =>
render(
<RegistrationContextProvider {...registrationContextProviderProps}>
<RegistrationWorkflow initialScreenIndex={0}>
<CreateNewOrgScreenBase {...props} />
</RegistrationWorkflow>
</RegistrationContextProvider>
);

it('renders without crashing', () => {
renderer();
render(
<CreateNewOrgScreenBase
WorkflowCardHeaderProps={{ title: 'Join an Organization' }}
WorkflowCardInstructionProps={{
instructions: 'Create new organization instructions',
}}
initialValue={'Acme Inc.'}
WorkflowCardActionsProps={{
showNext: true,
nextLabel: 'Next',
canGoNext: true,
showPrevious: true,
previousLabel: 'Back',
canGoPrevious: true,
currentStep: 2,
totalSteps: 6,
}}
></CreateNewOrgScreenBase>
);
expect(screen.getByText('Join an Organization')).toBeInTheDocument();
expect(screen.getByText('Create new organization instructions')).toBeInTheDocument();
expect(screen.getByText('Next')).toBeInTheDocument();
expect(screen.getByText(/Next/i)).toBeEnabled();
expect(screen.getByText('Back')).toBeInTheDocument();
expect(screen.getByText(/Back/i)).toBeEnabled();
});

it('sets error state when organization name is too short', () => {
const { getByLabelText, rerender } = render(
<CreateNewOrgScreenBase
orgNameLabel="Organization Name"
initialValue={'test'}
orgNameValidator={(orgName: string): boolean | string => {
if (orgName?.length > 6) {
return true;
}
return 'Organization Name';
}}
/>
);

const verifyOrgNameInput = getByLabelText('Organization Name');
fireEvent.change(verifyOrgNameInput, { target: { value: 't' } });
fireEvent.blur(verifyOrgNameInput);

// Rerender to ensure state changes have taken effect
rerender(
<CreateNewOrgScreenBase
orgNameLabel="Organization Name"
orgNameValidator={(orgName: string): boolean | string => {
if (orgName?.length > 6) {
return true;
}
return 'Please enter a valid organization name';
}}
/>
);
expect(verifyOrgNameInput).toHaveAttribute('aria-invalid', 'true');
});

it('does not set error state when organization name is long enough', () => {
const { getByLabelText, rerender } = render(
<CreateNewOrgScreenBase
orgNameLabel="Organization Name"
initialValue={'test'}
orgNameValidator={(orgName: string): boolean | string => {
if (orgName?.length > 2) {
return true;
}
return 'Please enter a valid organization name';
}}
/>
);

const verifyEmailInput = getByLabelText('Organization Name');
fireEvent.change(verifyEmailInput, { target: { value: 'test' } });
fireEvent.blur(verifyEmailInput);

// Rerender to ensure state changes have taken effect
rerender(
<CreateNewOrgScreenBase
orgNameLabel="Organization Name"
orgNameValidator={(orgName: string): boolean | string => {
if (orgName?.length > 1) {
return true;
}
return 'Please enter a valid organization name';
}}
/>
);
expect(verifyEmailInput).not.toHaveAttribute('aria-invalid', 'true');
});

it('calls onNext when the next button is clicked', () => {
const { getByText } = render(
<CreateNewOrgScreenBase
WorkflowCardActionsProps={{
onNext: mockOnNext(),
showNext: true,
nextLabel: 'Next',
canGoNext: true,
currentStep: 1,
totalSteps: 6,
}}
/>
);

const nextButton = getByText('Next');
fireEvent.click(nextButton);

expect(mockOnNext).toHaveBeenCalled();
});

it('pre-populates the organization name input field with initialValue', () => {
const { getByLabelText } = render(
<CreateNewOrgScreenBase
orgNameLabel="Organization Name"
initialValue="Acme"
orgNameValidator={(orgName: string): boolean | string => {
if (orgName?.length > 1) {
return true;
}
return 'Please enter a organization name';
}}
/>
);

const orgNameInput = getByLabelText('Organization Name');
expect(orgNameInput).toHaveValue('Acme');
});

it('displays title, instructions and orgNameLabel correctly', () => {
const { getByText } = render(
<CreateNewOrgScreenBase
WorkflowCardHeaderProps={{ title: 'Title' }}
WorkflowCardInstructionProps={{ instructions: 'Instructions' }}
orgNameLabel="Organization Name"
/>
);

expect(getByText('Title')).toBeInTheDocument();
expect(getByText('Instructions')).toBeInTheDocument();
expect(getByText('Organization Name')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useCallback, useEffect } from 'react';
import { CreateNewOrgScreenProps } from './types';
import TextField from '@mui/material/TextField';
import ErrorManager from '../../components/Error/ErrorManager';
import {
WorkflowCard,
WorkflowCardHeader,
WorkflowCardInstructions,
WorkflowCardBody,
WorkflowCardActions,
} from '../../components';

/**
* Component that renders a screen for the user to enter an organization name to start the
* organization creation process.
*
* @param orgNameLabel label for the organization name field
* @param initialValue initial value for the orgName text field
* @param orgNameValidator function used to test the organization name input for valid formatting
* @param orgNameTextFieldProps props to pass to the organization name text field
* @param WorkflowCardBaseProps props that will be passed to the WorkflowCard component
* @param WorkflowCardHeaderProps props that will be passed to the WorkflowCardHeader component
* @param WorkflowCardInstructionProps props that will be passed to the WorkflowCardInstructions component
* @param WorkflowCardActionsProps props that will be passed to the WorkflowCardActions component
* @param errorDisplayConfig configuration for customizing how errors are displayed
*
* @category Component
*/

export const CreateNewOrgScreenBase: React.FC<React.PropsWithChildren<CreateNewOrgScreenProps & { inputRef?: any }>> = (
props
) => {
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
orgNameValidator = (orgName: string): boolean | string => true,
orgNameLabel,
initialValue,
orgNameTextFieldProps,
inputRef,
errorDisplayConfig,
} = props;

const cardBaseProps = props.WorkflowCardBaseProps || {};
const headerProps = props.WorkflowCardHeaderProps || {};
const instructionsProps = props.WorkflowCardInstructionProps || {};
const actionsProps = props.WorkflowCardActionsProps || {};

const [orgNameInput, setOrgNameInput] = React.useState(initialValue ? initialValue : '');
const [isOrgNameValid, setIsOrgNameValid] = React.useState(orgNameValidator(initialValue ?? '') ?? false);
const [orgNameError, setOrgNameError] = React.useState('');
const [shouldValidateOrgName, setShouldValidateOrgName] = React.useState(false);

const handleOrgNameInputChange = useCallback(
(orgName: string) => {
setOrgNameInput(orgName);
const orgNameValidatorResponse = orgNameValidator(orgName);

setIsOrgNameValid(typeof orgNameValidatorResponse === 'boolean' ? orgNameValidatorResponse : false);
setOrgNameError(typeof orgNameValidatorResponse === 'string' ? orgNameValidatorResponse : '');
},
[orgNameValidator]
);
useEffect(() => {
if (orgNameInput.length > 0) {
setShouldValidateOrgName(true);
handleOrgNameInputChange(orgNameInput);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<WorkflowCard {...cardBaseProps}>
<WorkflowCardHeader {...headerProps} />
<WorkflowCardInstructions {...instructionsProps} />
<WorkflowCardBody>
<ErrorManager {...errorDisplayConfig}>
<TextField
ref={inputRef}
type={'orgName'}
label={orgNameLabel}
fullWidth
value={orgNameInput}
variant="filled"
error={shouldValidateOrgName && !isOrgNameValid}
helperText={shouldValidateOrgName && orgNameError}
{...orgNameTextFieldProps}
onChange={(e): void => {
orgNameTextFieldProps?.onChange?.(e);
handleOrgNameInputChange(e.target.value);
}}
onKeyUp={(e): void => {
if (
e.key === 'Enter' &&
((orgNameInput.length > 0 && isOrgNameValid) || actionsProps.canGoNext)
)
actionsProps?.onNext?.();
}}
onBlur={(e): void => {
orgNameTextFieldProps?.onBlur?.(e);
setShouldValidateOrgName(true);
}}
/>
</ErrorManager>
</WorkflowCardBody>
<WorkflowCardActions
{...actionsProps}
canGoNext={(orgNameInput.length > 0 && isOrgNameValid && actionsProps.canGoNext) as any}
/>
</WorkflowCard>
);
};
2 changes: 2 additions & 0 deletions login-workflow/src/screens/CreateNewOrgScreen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './CreateNewOrgScreenBase';
export * from './types';
33 changes: 33 additions & 0 deletions login-workflow/src/screens/CreateNewOrgScreen/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TextFieldProps } from '@mui/material';
import { WorkflowCardProps } from '../../components/WorkflowCard/WorkflowCard.types';
import { ErrorManagerProps } from '../../components/Error';

export type CreateNewOrgScreenProps = WorkflowCardProps & {
/**
* The label for the organization name field
*/
orgNameLabel?: string;

/**
* The initial value for the organization name text field
*/
initialValue?: string;

/**
* The function used to test the organization name input for valid formatting
* @param {string} orgName
* @returns boolean | string
*/
orgNameValidator?: (orgName: string) => boolean | string;

/**
* The props to pass to the organization name text field.
* See [MUI's TextFieldProps API](https://mui.com/material-ui/api/text-field/) for more details.
*/
orgNameTextFieldProps?: TextFieldProps;

/**
* The configuration for customizing how errors are displayed
*/
errorDisplayConfig?: ErrorManagerProps;
};
1 change: 1 addition & 0 deletions login-workflow/src/screens/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from './ForgotPasswordScreen';
export * from './ContactScreen';
export * from './RegistrationSuccessScreen';
export * from './ExistingAccountSuccessScreen';
export * from './CreateNewOrgScreen';
Loading