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:
parent
002e4db355
commit
7845476af1
10 changed files with 56 additions and 26 deletions
|
@ -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>
|
||||||
|
|
|
@ -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'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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, {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue