Repository: provectus/kafka-ui
Issue: Make Submit button inactive when required fields aren't filled
Labels: type/bug
good first issue
scope/frontend
status/accepted
status/confirmed

**Describe the bug**
Submit button is inactive by default in create forms, it should stay inactive also in case of once pressing the input fields and letting it empty

**Set up**
https://www.kafka-ui.provectus.io/


**Steps to Reproduce**
Steps to reproduce the behavior:

1. Navigate to schema registry 
2. Create schema
3. Make sure submit button is inactive
4. Click into required input field
5. Make sure submit button became active
6. Click out of the field

**Expected behavior**
Submit button should stay inactive if required data is missing from the forms: _**Create new Topic, Produce Message, Create new schema, Create Connector**_


**Screenshots**
![inactive submit](https://user-images.githubusercontent.com/104780608/170481725-0937c7ee-9711-4a14-82ae-00945fe626a0.png)






Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/kafka-ui-react-app/src/components/Schemas/New/New.tsx b/kafka-ui-react-app/src/components/Schemas/New/New.tsx
index 0d9d171b1f2..1aa843d6b59 100644
--- a/kafka-ui-react-app/src/components/Schemas/New/New.tsx
+++ b/kafka-ui-react-app/src/components/Schemas/New/New.tsx
@@ -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>
diff --git a/kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx b/kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx
index aec534ce3e3..e03a2cfa0b1 100644
--- a/kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx
+++ b/kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx
@@ -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)));
 
-    userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
-    userEvent.click(screen.getByText('Create topic'));
+    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'));
     });
 
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx
index 6de0b52545a..807c6e9c7b1 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx
@@ -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);
diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx
index 83214b376c8..02e11b9c4cf 100644
--- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx
+++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx
@@ -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);
diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx
index d9d6427a25c..d2cb51fae5d 100644
--- a/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx
+++ b/kafka-ui-react-app/src/components/Topics/shared/Form/TopicForm.tsx
@@ -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">
diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx
index b224c8cd8e1..fa12e8728c1 100644
--- a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx
+++ b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.spec.tsx
@@ -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);
   });
diff --git a/kafka-ui-react-app/src/lib/yupExtended.ts b/kafka-ui-react-app/src/lib/yupExtended.ts
index db185821095..967bc0bbafe 100644
--- a/kafka-ui-react-app/src/lib/yupExtended.ts
+++ b/kafka-ui-react-app/src/lib/yupExtended.ts
@@ -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()
diff --git a/kafka-ui-react-app/src/redux/interfaces/topic.ts b/kafka-ui-react-app/src/redux/interfaces/topic.ts
index 647b0e69f9c..13aa74cf407 100644
--- a/kafka-ui-react-app/src/redux/interfaces/topic.ts
+++ b/kafka-ui-react-app/src/redux/interfaces/topic.ts
@@ -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;
diff --git a/kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts b/kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts
index 9d6ccc8e596..b1bcae7ebc4 100644
--- a/kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts
+++ b/kafka-ui-react-app/src/redux/reducers/topics/__test__/reducer.spec.ts
@@ -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,
diff --git a/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts b/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts
index 2a13822b764..74362e1012d 100644
--- a/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts
+++ b/kafka-ui-react-app/src/redux/reducers/topics/topicsSlice.ts
@@ -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, {}),
     },
   };


Test patch: