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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,10 @@
import React, { PropsWithChildren } from 'react';
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 TopicForm, { Props } from 'components/Topics/shared/Form/TopicForm';
import userEvent from '@testing-library/user-event';
import { act } from 'react-dom/test-utils';
const isSubmitting = false;
const onSubmit = jest.fn();
@ -60,12 +61,19 @@ describe('TopicForm', () => {
expectByRoleAndNameToBeInDocument('button', 'Create topic');
});
it('submits', () => {
it('submits', async () => {
renderComponent({
isSubmitting,
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' }));
expect(onSubmit).toBeCalledTimes(1);
});

View file

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

View file

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

View file

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

View file

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