fixing forms not no be able to submit until all required fields are f… (#2221)

* fixing forms not no be able to submit until all required fields are filled

* fixing topic form validation issue
This commit is contained in:
Robert Azizbekyan 2022-07-18 17:08:32 +04:00 committed by GitHub
parent 002e4db355
commit 7845476af1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 56 additions and 26 deletions

View file

@ -32,12 +32,12 @@ const New: React.FC = () => {
const { clusterName } = useAppParams<ClusterNameRoute>(); const { clusterName } = useAppParams<ClusterNameRoute>();
const navigate = useNavigate(); const navigate = useNavigate();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const methods = useForm<NewSchemaSubjectRaw>(); const methods = useForm<NewSchemaSubjectRaw>({ mode: 'onChange' });
const { const {
register, register,
handleSubmit, handleSubmit,
control, control,
formState: { isDirty, isSubmitting, errors }, formState: { isDirty, isSubmitting, errors, isValid },
} = methods; } = methods;
const onSubmit = async ({ const onSubmit = async ({
@ -99,15 +99,15 @@ const New: React.FC = () => {
<div> <div>
<InputLabel>Schema Type *</InputLabel> <InputLabel>Schema Type *</InputLabel>
<Controller <Controller
defaultValue={SchemaTypeOptions[0].value as SchemaType}
control={control} control={control}
rules={{ required: 'Schema Type is required.' }} rules={{ required: 'Schema Type is required.' }}
name="schemaType" name="schemaType"
render={({ field: { name, onChange } }) => ( render={({ field: { name, onChange, value } }) => (
<Select <Select
selectSize="M" selectSize="M"
name={name} name={name}
value={SchemaTypeOptions[0].value} defaultValue={SchemaTypeOptions[0].value}
value={value}
onChange={onChange} onChange={onChange}
minWidth="50%" minWidth="50%"
disabled={isSubmitting} disabled={isSubmitting}
@ -124,7 +124,7 @@ const New: React.FC = () => {
buttonSize="M" buttonSize="M"
buttonType="primary" buttonType="primary"
type="submit" type="submit"
disabled={isSubmitting || !isDirty} disabled={!isValid || isSubmitting || !isDirty}
> >
Submit Submit
</Button> </Button>

View file

@ -80,7 +80,12 @@ describe('New', () => {
it('validates form', async () => { it('validates form', async () => {
await act(() => renderComponent(clusterTopicNewPath(clusterName))); await act(() => renderComponent(clusterTopicNewPath(clusterName)));
userEvent.click(screen.getByText('Create topic')); await waitFor(() => {
userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
});
await waitFor(() => {
userEvent.clear(screen.getByPlaceholderText('Topic Name'));
});
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('name is a required field')).toBeInTheDocument(); expect(screen.getByText('name is a required field')).toBeInTheDocument();
}); });
@ -96,8 +101,13 @@ describe('New', () => {
await act(() => renderComponent(clusterTopicNewPath(clusterName))); await act(() => renderComponent(clusterTopicNewPath(clusterName)));
userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName); await act(() => {
userEvent.click(screen.getByText('Create topic')); userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
});
await act(() => {
userEvent.click(screen.getByText('Create topic'));
});
await waitFor(() => expect(mockNavigate).toBeCalledTimes(1)); await waitFor(() => expect(mockNavigate).toBeCalledTimes(1));
expect(mockNavigate).toHaveBeenLastCalledWith(`../${topicName}`); expect(mockNavigate).toHaveBeenLastCalledWith(`../${topicName}`);
@ -127,6 +137,8 @@ describe('New', () => {
await act(() => renderComponent(clusterTopicNewPath(clusterName))); await act(() => renderComponent(clusterTopicNewPath(clusterName)));
await act(() => { await act(() => {
userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName); userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
});
await act(() => {
userEvent.click(screen.getByText('Create topic')); userEvent.click(screen.getByText('Create topic'));
}); });

View file

@ -92,6 +92,7 @@ const Edit: React.FC<Props> = ({
const methods = useForm<TopicFormData>({ const methods = useForm<TopicFormData>({
defaultValues, defaultValues,
resolver: yupResolver(topicFormValidationSchema), resolver: yupResolver(topicFormValidationSchema),
mode: 'onChange',
}); });
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false); const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false);

View file

@ -117,13 +117,15 @@ describe('Edit Component', () => {
renderComponent({ updateTopic: updateTopicMock }, undefined); renderComponent({ updateTopic: updateTopicMock }, undefined);
const btn = screen.getAllByText(/Save/i)[0]; const btn = screen.getAllByText(/Save/i)[0];
expect(btn).toBeEnabled();
await act(() => { await act(() => {
userEvent.type( userEvent.type(
screen.getByPlaceholderText('Min In Sync Replicas'), screen.getByPlaceholderText('Min In Sync Replicas'),
'1' '1'
); );
});
await act(() => {
userEvent.click(btn); userEvent.click(btn);
}); });
expect(updateTopicMock).toHaveBeenCalledTimes(1); expect(updateTopicMock).toHaveBeenCalledTimes(1);
@ -145,6 +147,8 @@ describe('Edit Component', () => {
screen.getByPlaceholderText('Min In Sync Replicas'), screen.getByPlaceholderText('Min In Sync Replicas'),
'1' '1'
); );
});
await act(() => {
userEvent.click(btn); userEvent.click(btn);
}); });
expect(updateTopicMock).toHaveBeenCalledTimes(1); expect(updateTopicMock).toHaveBeenCalledTimes(1);

View file

@ -51,14 +51,14 @@ const TopicForm: React.FC<Props> = ({
}) => { }) => {
const { const {
control, control,
formState: { errors }, formState: { errors, isDirty, isValid },
} = useFormContext(); } = useFormContext();
const getCleanUpPolicy = const getCleanUpPolicy =
CleanupPolicyOptions.find((option: SelectOption) => { CleanupPolicyOptions.find((option: SelectOption) => {
return option.value === cleanUpPolicy?.toLowerCase(); return option.value === cleanUpPolicy?.toLowerCase();
})?.value || CleanupPolicyOptions[0].value; })?.value || CleanupPolicyOptions[0].value;
return ( return (
<StyledForm onSubmit={onSubmit}> <StyledForm onSubmit={onSubmit} aria-label="topic form">
<fieldset disabled={isSubmitting}> <fieldset disabled={isSubmitting}>
<fieldset disabled={isEditing}> <fieldset disabled={isEditing}>
<S.Column> <S.Column>
@ -125,10 +125,10 @@ const TopicForm: React.FC<Props> = ({
placeholder="Min In Sync Replicas" placeholder="Min In Sync Replicas"
min="1" min="1"
defaultValue={inSyncReplicas} defaultValue={inSyncReplicas}
name="minInsyncReplicas" name="minInSyncReplicas"
/> />
<FormError> <FormError>
<ErrorMessage errors={errors} name="minInsyncReplicas" /> <ErrorMessage errors={errors} name="minInSyncReplicas" />
</FormError> </FormError>
</div> </div>
<div> <div>
@ -209,7 +209,12 @@ const TopicForm: React.FC<Props> = ({
<S.CustomParamsHeading>Custom parameters</S.CustomParamsHeading> <S.CustomParamsHeading>Custom parameters</S.CustomParamsHeading>
<CustomParams isSubmitting={isSubmitting} /> <CustomParams isSubmitting={isSubmitting} />
<S.ButtonWrapper> <S.ButtonWrapper>
<Button type="submit" buttonType="primary" buttonSize="L"> <Button
type="submit"
buttonType="primary"
buttonSize="L"
disabled={!isValid || isSubmitting || !isDirty}
>
{isEditing ? 'Save' : 'Create topic'} {isEditing ? 'Save' : 'Create topic'}
</Button> </Button>
<Button type="button" buttonType="primary" buttonSize="L"> <Button type="button" buttonType="primary" buttonSize="L">

View file

@ -1,9 +1,10 @@
import React, { PropsWithChildren } from 'react'; import React, { PropsWithChildren } from 'react';
import { render } from 'lib/testHelpers'; import { render } from 'lib/testHelpers';
import { screen } from '@testing-library/dom'; import { fireEvent, screen } from '@testing-library/dom';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import TopicForm, { Props } from 'components/Topics/shared/Form/TopicForm'; import TopicForm, { Props } from 'components/Topics/shared/Form/TopicForm';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { act } from 'react-dom/test-utils';
const isSubmitting = false; const isSubmitting = false;
const onSubmit = jest.fn(); const onSubmit = jest.fn();
@ -60,12 +61,19 @@ describe('TopicForm', () => {
expectByRoleAndNameToBeInDocument('button', 'Create topic'); expectByRoleAndNameToBeInDocument('button', 'Create topic');
}); });
it('submits', () => { it('submits', async () => {
renderComponent({ renderComponent({
isSubmitting, isSubmitting,
onSubmit: onSubmit.mockImplementation((e) => e.preventDefault()), onSubmit: onSubmit.mockImplementation((e) => e.preventDefault()),
}); });
await act(() => {
userEvent.type(screen.getByPlaceholderText('Topic Name'), 'topicName');
});
await act(() => {
fireEvent.submit(screen.getByLabelText('topic form'));
});
userEvent.click(screen.getByRole('button', { name: 'Create topic' })); userEvent.click(screen.getByRole('button', { name: 'Create topic' }));
expect(onSubmit).toBeCalledTimes(1); expect(onSubmit).toBeCalledTimes(1);
}); });

View file

@ -62,7 +62,7 @@ export const topicFormValidationSchema = yup.object().shape({
.min(1) .min(1)
.required() .required()
.typeError('Replication factor is required and must be a number'), .typeError('Replication factor is required and must be a number'),
minInsyncReplicas: yup minInSyncReplicas: yup
.number() .number()
.min(1) .min(1)
.required() .required()

View file

@ -64,7 +64,7 @@ export interface TopicFormDataRaw {
name: string; name: string;
partitions: number; partitions: number;
replicationFactor: number; replicationFactor: number;
minInsyncReplicas: number; minInSyncReplicas: number;
cleanupPolicy: string; cleanupPolicy: string;
retentionMs: number; retentionMs: number;
retentionBytes: number; retentionBytes: number;
@ -76,7 +76,7 @@ export interface TopicFormData {
name: string; name: string;
partitions: number; partitions: number;
replicationFactor: number; replicationFactor: number;
minInsyncReplicas: number; minInSyncReplicas: number;
cleanupPolicy: string; cleanupPolicy: string;
retentionMs: number; retentionMs: number;
retentionBytes: number; retentionBytes: number;

View file

@ -806,7 +806,7 @@ describe('topics Slice', () => {
name: 'newTopic', name: 'newTopic',
partitions: 0, partitions: 0,
replicationFactor: 0, replicationFactor: 0,
minInsyncReplicas: 0, minInSyncReplicas: 0,
cleanupPolicy: 'DELETE', cleanupPolicy: 'DELETE',
retentionMs: 1, retentionMs: 1,
retentionBytes: 1, retentionBytes: 1,
@ -864,7 +864,7 @@ describe('topics Slice', () => {
name: topicName, name: topicName,
partitions: 0, partitions: 0,
replicationFactor: 0, replicationFactor: 0,
minInsyncReplicas: 0, minInSyncReplicas: 0,
cleanupPolicy: 'DELETE', cleanupPolicy: 'DELETE',
retentionMs: 0, retentionMs: 0,
retentionBytes: 0, retentionBytes: 0,

View file

@ -92,7 +92,7 @@ export const formatTopicCreation = (form: TopicFormData): TopicCreation => {
retentionBytes, retentionBytes,
retentionMs, retentionMs,
maxMessageBytes, maxMessageBytes,
minInsyncReplicas, minInSyncReplicas,
customParams, customParams,
} = form; } = form;
@ -105,7 +105,7 @@ export const formatTopicCreation = (form: TopicFormData): TopicCreation => {
'retention.ms': retentionMs.toString(), 'retention.ms': retentionMs.toString(),
'retention.bytes': retentionBytes.toString(), 'retention.bytes': retentionBytes.toString(),
'max.message.bytes': maxMessageBytes.toString(), 'max.message.bytes': maxMessageBytes.toString(),
'min.insync.replicas': minInsyncReplicas.toString(), 'min.insync.replicas': minInSyncReplicas.toString(),
...Object.values(customParams || {}).reduce(topicReducer, {}), ...Object.values(customParams || {}).reduce(topicReducer, {}),
}, },
}; };
@ -153,7 +153,7 @@ const formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => {
retentionBytes, retentionBytes,
retentionMs, retentionMs,
maxMessageBytes, maxMessageBytes,
minInsyncReplicas, minInSyncReplicas,
customParams, customParams,
} = form; } = form;
@ -163,7 +163,7 @@ const formatTopicUpdate = (form: TopicFormDataRaw): TopicUpdate => {
'retention.ms': retentionMs, 'retention.ms': retentionMs,
'retention.bytes': retentionBytes, 'retention.bytes': retentionBytes,
'max.message.bytes': maxMessageBytes, 'max.message.bytes': maxMessageBytes,
'min.insync.replicas': minInsyncReplicas, 'min.insync.replicas': minInSyncReplicas,
...Object.values(customParams || {}).reduce(topicReducer, {}), ...Object.values(customParams || {}).reduce(topicReducer, {}),
}, },
}; };