From f4bd49b5927de5f15882f63064338e92b722abad Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 15 Mar 2022 08:55:38 +0100 Subject: [PATCH] [Cases] Only enable save if changes were made to the connector form (#127455) --- .../components/edit_connector/index.test.tsx | 115 ++++++++++++++++-- .../components/edit_connector/index.tsx | 24 +++- 2 files changed, 123 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx index bcc700c543f7e..0512501bf5e11 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.test.tsx @@ -14,6 +14,8 @@ import { TestProviders } from '../../common/mock'; import { connectorsMock } from '../../containers/configure/mock'; import { basicCase, basicPush, caseUserActions } from '../../containers/mock'; import { useKibana } from '../../common/lib/kibana'; +import { CaseConnector } from '../../containers/configure/types'; +import userEvent from '@testing-library/user-event'; jest.mock('../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; @@ -29,18 +31,20 @@ const caseServices = { hasDataToPush: true, }, }; -const defaultProps: EditConnectorProps = { - caseData: basicCase, - caseServices, - connectorName: connectorsMock[0].name, - connectors: connectorsMock, - hasDataToPush: true, - isLoading: false, - isValidConnector: true, - onSubmit, - updateCase, - userActions: caseUserActions, - userCanCrud: true, +const getDefaultProps = (): EditConnectorProps => { + return { + caseData: basicCase, + caseServices, + connectorName: connectorsMock[0].name, + connectors: connectorsMock, + hasDataToPush: true, + isLoading: false, + isValidConnector: true, + onSubmit, + updateCase, + userActions: caseUserActions, + userCanCrud: true, + }; }; describe('EditConnector ', () => { @@ -53,6 +57,7 @@ describe('EditConnector ', () => { }); it('Renders servicenow connector from case initially', async () => { + const defaultProps = getDefaultProps(); const serviceNowProps = { ...defaultProps, caseData: { @@ -71,6 +76,7 @@ describe('EditConnector ', () => { }); it('Renders no connector, and then edit', async () => { + const defaultProps = getDefaultProps(); const wrapper = mount( @@ -92,6 +98,7 @@ describe('EditConnector ', () => { }); it('Edit external service on submit', async () => { + const defaultProps = getDefaultProps(); const wrapper = mount( @@ -111,6 +118,7 @@ describe('EditConnector ', () => { }); it('Revert to initial external service on error', async () => { + const defaultProps = getDefaultProps(); onSubmit.mockImplementation((connector, onSuccess, onError) => { onError(new Error('An error has occurred')); }); @@ -155,11 +163,15 @@ describe('EditConnector ', () => { }); it('Resets selector on cancel', async () => { + const defaultProps = getDefaultProps(); const props = { ...defaultProps, caseData: { ...defaultProps.caseData, - connector: { ...defaultProps.caseData.connector, id: 'servicenow-1' }, + connector: { + ...defaultProps.caseData.connector, + id: 'servicenow-1', + }, }, }; @@ -185,6 +197,7 @@ describe('EditConnector ', () => { }); it('Renders loading spinner', async () => { + const defaultProps = getDefaultProps(); const props = { ...defaultProps, isLoading: true }; const wrapper = mount( @@ -197,6 +210,7 @@ describe('EditConnector ', () => { }); it('does not allow the connector to be edited when the user does not have write permissions', async () => { + const defaultProps = getDefaultProps(); const props = { ...defaultProps, userCanCrud: false }; const wrapper = mount( @@ -211,6 +225,7 @@ describe('EditConnector ', () => { }); it('displays the permissions error message when one is provided', async () => { + const defaultProps = getDefaultProps(); const props = { ...defaultProps, permissionsError: 'error message' }; const wrapper = mount( @@ -232,6 +247,7 @@ describe('EditConnector ', () => { }); it('displays the callout message when none is selected', async () => { + const defaultProps = getDefaultProps(); const props = { ...defaultProps, connectors: [] }; const wrapper = mount( @@ -247,4 +263,77 @@ describe('EditConnector ', () => { expect(wrapper.find(`[data-test-subj="push-callouts"]`).exists()).toEqual(true); }); }); + + it('disables the save button until changes are done ', async () => { + const defaultProps = getDefaultProps(); + const serviceNowProps = { + ...defaultProps, + caseData: { + ...defaultProps.caseData, + connector: { + ...defaultProps.caseData.connector, + id: 'servicenow-1', + fields: { + urgency: null, + severity: null, + impact: null, + category: null, + subcategory: null, + }, + } as CaseConnector, + }, + }; + const result = render( + + + + ); + + // the save button should be disabled + userEvent.click(result.getByTestId('connector-edit-button')); + expect(result.getByTestId('edit-connectors-submit')).toBeDisabled(); + + // simulate changing the connector + userEvent.click(result.getByTestId('dropdown-connectors')); + userEvent.click(result.getAllByTestId('dropdown-connector-no-connector')[0]); + expect(result.getByTestId('edit-connectors-submit')).toBeEnabled(); + + // this strange assertion is required because of existing race conditions inside the EditConnector component + await waitFor(() => { + expect(true).toBeTruthy(); + }); + }); + + it('disables the save button when no connector is the default', async () => { + const defaultProps = getDefaultProps(); + const noneConnector = { + ...defaultProps, + caseData: { + ...defaultProps.caseData, + connector: { + id: 'none', + fields: null, + } as CaseConnector, + }, + }; + const result = render( + + + + ); + + // the save button should be disabled + userEvent.click(result.getByTestId('connector-edit-button')); + expect(result.getByTestId('edit-connectors-submit')).toBeDisabled(); + + // simulate changing the connector + userEvent.click(result.getByTestId('dropdown-connectors')); + userEvent.click(result.getAllByTestId('dropdown-connector-resilient-2')[0]); + expect(result.getByTestId('edit-connectors-submit')).toBeEnabled(); + + // this strange assertion is required because of existing race conditions inside the EditConnector component + await waitFor(() => { + expect(true).toBeTruthy(); + }); + }); }); diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index 5ab27e3e32009..7db20170c7857 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -4,8 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import React, { useCallback, useEffect, useReducer } from 'react'; +import React, { useCallback, useEffect, useReducer, useState } from 'react'; import deepEqual from 'fast-deep-equal'; import { EuiText, @@ -22,7 +21,7 @@ import { isEmpty, noop } from 'lodash/fp'; import { FieldConfig, Form, UseField, useForm } from '../../common/shared_imports'; import { Case } from '../../../common/ui/types'; -import { ActionConnector, ConnectorTypeFields } from '../../../common/api'; +import { ActionConnector, ConnectorTypeFields, NONE_CONNECTOR_ID } from '../../../common/api'; import { ConnectorSelector } from '../connector_selector/form'; import { ConnectorFieldsForm } from '../connectors/fields_form'; import { CaseUserActions } from '../../containers/types'; @@ -133,6 +132,9 @@ export const EditConnector = React.memo( schema, }); + // by default save if disabled + const [enableSave, setEnableSave] = useState(false); + const { setFieldValue, submit } = form; const [{ currentConnector, fields, editConnector }, dispatch] = useReducer( @@ -140,6 +142,21 @@ export const EditConnector = React.memo( { ...initialState, fields: caseFields } ); + // only enable the save button if changes were made to the previous selected + // connector or its fields + useEffect(() => { + // null and none are equivalent to `no connector`. + // This makes sure we don't enable the button when the "no connector" option is selected + // by default. e.g. when a case is created without a selector + const isNoConnectorDeafultValue = + currentConnector === null && selectedConnector === NONE_CONNECTOR_ID; + const enable = + (!isNoConnectorDeafultValue && currentConnector?.id !== selectedConnector) || + !deepEqual(fields, caseFields); + + setEnableSave(enable); + }, [caseFields, currentConnector, fields, selectedConnector]); + useEffect(() => { // Initialize the current connector with the connector information attached to the case if we can find that // connector in the retrieved connectors from the API call @@ -330,6 +347,7 @@ export const EditConnector = React.memo(