diff --git a/awx/ui_next/src/screens/Inventory/InventoryAdd/InventoryAdd.jsx b/awx/ui_next/src/screens/Inventory/InventoryAdd/InventoryAdd.jsx index aaf28bcd57..da666db200 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryAdd/InventoryAdd.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryAdd/InventoryAdd.jsx @@ -43,20 +43,29 @@ function InventoryAdd({ history, i18n }) { }; const handleSubmit = async values => { + const { + instanceGroups, + organization, + insights_credential, + ...remainingValues + } = values; try { - let response; - if (values.instance_groups) { - response = await InventoriesAPI.create(values); - const associatePromises = values.instance_groups.map(async ig => - InventoriesAPI.associateInstanceGroup(response.data.id, ig.id) + const { + data: { id: inventoryId }, + } = await InventoriesAPI.create({ + organization: organization.id, + insights_credential: insights_credential.id, + ...remainingValues, + }); + if (instanceGroups) { + const associatePromises = instanceGroups.map(async ig => + InventoriesAPI.associateInstanceGroup(inventoryId, ig.id) ); - await Promise.all([response, ...associatePromises]); - } else { - response = await InventoriesAPI.create(values); + await Promise.all(associatePromises); } const url = history.location.pathname.search('smart') - ? `/inventories/smart_inventory/${response.data.id}/details` - : `/inventories/inventory/${response.data.id}/details`; + ? `/inventories/smart_inventory/${inventoryId}/details` + : `/inventories/inventory/${inventoryId}/details`; history.push(`${url}`); } catch (err) { @@ -75,11 +84,11 @@ function InventoryAdd({ history, i18n }) { @@ -87,8 +96,8 @@ function InventoryAdd({ history, i18n }) { diff --git a/awx/ui_next/src/screens/Inventory/InventoryAdd/InventoryAdd.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryAdd/InventoryAdd.test.jsx index fcb2d2965c..157e6b9a12 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryAdd/InventoryAdd.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryAdd/InventoryAdd.test.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { sleep } from '@testUtils/testUtils'; import { InventoriesAPI, CredentialTypesAPI } from '@api'; import InventoryAdd from './InventoryAdd'; @@ -18,6 +19,7 @@ CredentialTypesAPI.read.mockResolvedValue({ ], }, }); +InventoriesAPI.create.mockResolvedValue({ data: { id: 13 } }); describe('', () => { let wrapper; @@ -39,18 +41,27 @@ describe('', () => { expect(wrapper.length).toBe(1); }); test('handleSubmit should call the api', async () => { + const instanceGroups = [{ name: 'Bizz', id: 1 }, { name: 'Buzz', id: 2 }]; await waitForElement(wrapper, 'isLoading', el => el.length === 0); - wrapper.update(); - await act(async () => { - wrapper.find('InventoryForm').prop('handleSubmit')({ - name: 'Foo', - id: 1, - organization: 2, - }); + wrapper.find('InventoryForm').prop('onSubmit')({ + name: 'new Foo', + organization: { id: 2 }, + insights_credential: { id: 47 }, + instanceGroups, }); - - expect(InventoriesAPI.create).toHaveBeenCalledTimes(1); + await sleep(1); + expect(InventoriesAPI.create).toHaveBeenCalledWith({ + name: 'new Foo', + organization: 2, + insights_credential: 47, + }); + instanceGroups.map(IG => + expect(InventoriesAPI.associateInstanceGroup).toHaveBeenCalledWith( + 13, + IG.id + ) + ); }); test('handleCancel should return the user back to the inventories list', async () => { await waitForElement(wrapper, 'isLoading', el => el.length === 0); diff --git a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx index b60f4f6207..131787ae95 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.jsx @@ -14,7 +14,7 @@ import { getAddedAndRemoved } from '../../../util/lists'; function InventoryEdit({ history, i18n, inventory }) { const [error, setError] = useState(null); - const [instanceGroups, setInstanceGroups] = useState(null); + const [associatedInstanceGroups, setInstanceGroups] = useState(null); const [isLoading, setIsLoading] = useState(true); const [credentialTypeId, setCredentialTypeId] = useState(null); @@ -50,26 +50,31 @@ function InventoryEdit({ history, i18n, inventory }) { }; const handleSubmit = async values => { + const { + instanceGroups, + insights_credential, + organization, + ...remainingValues + } = values; try { - if (values.instance_groups) { + await InventoriesAPI.update(inventory.id, { + insights_credential: insights_credential.id, + organization: organization.id, + ...remainingValues, + }); + if (instanceGroups) { const { added, removed } = getAddedAndRemoved( - instanceGroups, - values.instance_groups + associatedInstanceGroups, + instanceGroups ); - const update = InventoriesAPI.update(inventory.id, values); + const associatePromises = added.map(async ig => InventoriesAPI.associateInstanceGroup(inventory.id, ig.id) ); - const disAssociatePromises = removed.map(async ig => + const disassociatePromises = removed.map(async ig => InventoriesAPI.disassociateInstanceGroup(inventory.id, ig.id) ); - await Promise.all([ - update, - ...associatePromises, - ...disAssociatePromises, - ]); - } else { - await InventoriesAPI.update(inventory.id, values); + await Promise.all([...associatePromises, ...disassociatePromises]); } } catch (err) { setError(err); @@ -90,11 +95,11 @@ function InventoryEdit({ history, i18n, inventory }) { <> @@ -102,10 +107,10 @@ function InventoryEdit({ history, i18n, inventory }) { diff --git a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.test.jsx index 38d36b7848..38a2fbdd42 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryEdit/InventoryEdit.test.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import { sleep } from '@testUtils/testUtils'; import { InventoriesAPI, CredentialTypesAPI } from '@api'; import InventoryEdit from './InventoryEdit'; @@ -59,15 +60,15 @@ CredentialTypesAPI.read.mockResolvedValue({ ], }, }); - +const associatedInstanceGroups = [ + { + id: 1, + name: 'Foo', + }, +]; InventoriesAPI.readInstanceGroups.mockResolvedValue({ data: { - results: [ - { - id: 1, - name: 'Foo', - }, - ], + results: associatedInstanceGroups, }, }); @@ -89,24 +90,39 @@ describe('', () => { test('initially renders successfully', async () => { expect(wrapper.find('InventoryEdit').length).toBe(1); }); + test('called InventoriesAPI.readInstanceGroups', async () => { expect(InventoriesAPI.readInstanceGroups).toBeCalledWith(1); - await waitForElement(wrapper, 'isLoading', el => el.length === 0); }); + test('handleCancel returns the user to the inventories list', async () => { await waitForElement(wrapper, 'isLoading', el => el.length === 0); wrapper.find('CardCloseButton').simulate('click'); expect(history.location.pathname).toEqual('/inventories'); }); + test('handleSubmit should post to the api', async () => { await waitForElement(wrapper, 'isLoading', el => el.length === 0); - wrapper.find('InventoryForm').prop('handleSubmit')({ + const instanceGroups = [{ name: 'Bizz', id: 2 }, { name: 'Buzz', id: 3 }]; + wrapper.find('InventoryForm').prop('onSubmit')({ name: 'Foo', - id: 1, - organization: 2, + id: 13, + organization: { id: 1 }, + insights_credential: { id: 13 }, + instanceGroups, }); - wrapper.update(); - - expect(InventoriesAPI.update).toHaveBeenCalledTimes(1); + await sleep(0); + instanceGroups.map(IG => + expect(InventoriesAPI.associateInstanceGroup).toHaveBeenCalledWith( + 1, + IG.id + ) + ); + associatedInstanceGroups.map(async aIG => + expect(InventoriesAPI.disassociateInstanceGroup).toHaveBeenCalledWith( + 1, + aIG.id + ) + ); }); }); diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx index a28456688b..3f186431ec 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Formik, Field } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; @@ -17,33 +17,29 @@ import CredentialLookup from '@components/Lookup/CredentialLookup'; function InventoryForm({ inventory = {}, i18n, - handleCancel, - handleSubmit, + onCancel, + onSubmit, instanceGroups, credentialTypeId, }) { - const [organization, setOrganization] = useState( - inventory.summary_fields ? inventory.summary_fields.organization : null - ); - const [insights_credential, setInsights_Credential] = useState( - inventory.summary_fields - ? inventory.summary_fields.insights_credential - : null - ); - const initialValues = { name: inventory.name || '', description: inventory.description || '', variables: inventory.variables || '---', - organization: organization ? organization.id : null, - instance_groups: instanceGroups || [], - insights_credential: insights_credential ? insights_credential.id : '', + organization: + (inventory.summary_fields && inventory.summary_fields.organization) || + null, + instanceGroups: instanceGroups || [], + insights_credential: + (inventory.summary_fields && + inventory.summary_fields.insights_credential) || + null, }; return ( { - handleSubmit(values); + onSubmit(values); }} render={formik => (
@@ -70,7 +66,7 @@ function InventoryForm({ i18n._(t`Select a value for this field`), i18n )} - render={({ form }) => ( + render={({ form, field }) => ( form.setFieldTouched('organization')} onChange={value => { - form.setFieldValue('organization', value.id); - setOrganization(value); + form.setFieldValue('organization', value); }} - value={organization} + value={field.value} required /> )} /> - - ( + render={({ field, form }) => ( { - form.setFieldValue('insights_credential', value.id); - setInsights_Credential(value); + // TODO: BELOW SHOULD BE REFACTORED AND REMOVED ONCE THE LOOKUP REFACTOR + // GOES INTO PLACE. + if (value[0] === field.value) { + return form.setFieldValue('insights_credential', null); + } + return form.setFieldValue('insights_credential', value); }} - value={insights_credential} + value={field.value} /> )} /> @@ -109,12 +106,12 @@ function InventoryForm({ ( { - form.setFieldValue('instance_groups', value); + form.setFieldValue('instanceGroups', value); }} /> )} @@ -132,7 +129,7 @@ function InventoryForm({ @@ -147,11 +144,10 @@ InventoryForm.proptype = { handleCancel: func.isRequired, instanceGroups: shape(), inventory: shape(), - credentialTypeId: number, + credentialTypeId: number.isRequired, }; InventoryForm.defaultProps = { - credentialTypeId: 14, inventory: {}, instanceGroups: [], }; diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.test.jsx index 3efb5a3dbe..4abf6fb98d 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.test.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import { sleep } from '@testUtils/testUtils'; import InventoryForm from './InventoryForm'; @@ -48,15 +49,18 @@ const inventory = { const instanceGroups = [{ name: 'Foo', id: 1 }, { name: 'Bar', id: 2 }]; describe('', () => { let wrapper; - let handleCancel; + let onCancel; + let onSubmit; beforeEach(() => { - handleCancel = jest.fn(); + onCancel = jest.fn(); + onSubmit = jest.fn(); wrapper = mountWithContexts( ); }); @@ -76,16 +80,23 @@ describe('', () => { ); expect(wrapper.find('VariablesField[label="Variables"]').length).toBe(1); }); - test('should update from values onChange', () => { + test('should update from values onChange', async () => { const form = wrapper.find('Formik'); act(() => { wrapper.find('OrganizationLookup').invoke('onBlur')(); wrapper.find('OrganizationLookup').invoke('onChange')({ - id: 1, + id: 3, name: 'organization', }); }); - expect(form.state('values').organization).toEqual(1); + expect(form.state('values').organization).toEqual({ + id: 3, + name: 'organization', + }); + wrapper.find('input#inventory-name').simulate('change', { + target: { value: 'new Foo', name: 'name' }, + }); + expect(form.state('values').name).toEqual('new Foo'); act(() => { wrapper.find('CredentialLookup').invoke('onBlur')(); wrapper.find('CredentialLookup').invoke('onChange')({ @@ -93,12 +104,26 @@ describe('', () => { name: 'credential', }); }); - expect(form.state('values').insights_credential).toEqual(10); + expect(form.state('values').insights_credential).toEqual({ + id: 10, + name: 'credential', + }); + + form.find('button[aria-label="Save"]').simulate('click'); + await sleep(1); + expect(onSubmit).toHaveBeenCalledWith({ + description: '', + insights_credential: { id: 10, name: 'credential' }, + instanceGroups: [{ id: 1, name: 'Foo' }, { id: 2, name: 'Bar' }], + name: 'new Foo', + organization: { id: 3, name: 'organization' }, + variables: '---', + }); }); test('should call handleCancel when Cancel button is clicked', async () => { - expect(handleCancel).not.toHaveBeenCalled(); + expect(onCancel).not.toHaveBeenCalled(); wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); - expect(handleCancel).toBeCalled(); + expect(onCancel).toBeCalled(); }); });