diff --git a/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx b/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx
index 0448f569e4..9bc8ad2a86 100644
--- a/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx
+++ b/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx
@@ -1,5 +1,7 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
+import { withI18n } from '@lingui/react';
+import { withRouter } from 'react-router-dom';
import { Chip, ChipGroup } from '@components/Chip';
import {
Dropdown as PFDropdown,
@@ -123,7 +125,7 @@ class MultiSelect extends Component {
removeChip(e, item) {
const { onRemoveItem } = this.props;
const { chipItems } = this.state;
- const chips = chipItems.filter(chip => chip.name !== item.name);
+ const chips = chipItems.filter(chip => chip.id !== item.id);
this.setState({ chipItems: chips });
onRemoveItem(item);
@@ -199,4 +201,5 @@ class MultiSelect extends Component {
);
}
}
-export default MultiSelect;
+export { MultiSelect as _MultiSelect };
+export default withI18n()(withRouter(MultiSelect));
diff --git a/awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx b/awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx
new file mode 100644
index 0000000000..368f5ca27f
--- /dev/null
+++ b/awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import MultiSelect, { _MultiSelect } from './MultiSelect';
+import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+
+describe('', () => {
+ const associatedItems = [
+ { name: 'Foo', id: 1, organization: 1 },
+ { name: 'Bar', id: 2, organization: 1 },
+ ];
+ const options = [{ name: 'Angry', id: 3 }, { name: 'Potato', id: 4 }];
+
+ test('Initially render successfully', () => {
+ const renderChips = jest.spyOn(_MultiSelect.prototype, 'renderChips');
+ const wrapper = mountWithContexts(
+
+ );
+ const component = wrapper.find('MultiSelect');
+
+ expect(renderChips).toBeCalled();
+ expect(component.state().chipItems.length).toBe(2);
+ });
+ test('handleSelection add item to chipItems', async () => {
+ const wrapper = mountWithContexts(
+
+ );
+ const event = { preventDefault: () => {} };
+ const component = wrapper.find('MultiSelect');
+ component.instance().handleSelection(event, { name: 'Apollo', id: 5 });
+ expect(component.state().chipItems.length).toBe(3);
+ });
+ test('handleAddItem adds a chip only when Tab is pressed', () => {
+ const onAddNewItem = jest.fn();
+ const wrapper = mountWithContexts(
+
+ );
+ const event = {
+ preventDefault: () => {},
+ key: 'Tab',
+ };
+ const component = wrapper.find('MultiSelect');
+
+ component.setState({ input: 'newLabel' });
+ component.update();
+ component.instance().handleAddItem(event);
+ expect(component.state().chipItems.length).toBe(3);
+ expect(component.state().input.length).toBe(0);
+ expect(component.state().isExpanded).toBe(false);
+ expect(onAddNewItem).toBeCalled();
+ });
+ test('removeChip removes chip properly', () => {
+ const onRemoveItem = jest.fn();
+
+ const wrapper = mountWithContexts(
+
+ );
+ const event = {
+ preventDefault: () => {},
+ };
+ const component = wrapper.find('MultiSelect');
+ component
+ .instance()
+ .removeChip(event, { name: 'Foo', id: 1, organization: 1 });
+ expect(component.state().chipItems.length).toBe(1);
+ expect(onRemoveItem).toBeCalled();
+ });
+});
diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
index 98eaa6735e..c1a041d4eb 100644
--- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
@@ -75,19 +75,22 @@ class JobTemplateForm extends Component {
}
async loadLabels(QueryConfig) {
- const { loadedLabels } = this.state;
this.setState({ contentError: null, hasContentLoading: true });
+ let loadedLabels;
try {
const { data } = await LabelsAPI.read(QueryConfig);
- const labels = [...data.results];
- this.setState({ loadedLabels: loadedLabels.concat(labels) });
+ loadedLabels = [...data.results];
if (data.next && data.next.includes('page=2')) {
- this.loadLabels({
+ const {
+ data: { results },
+ } = await LabelsAPI.read({
page: 2,
page_size: 200,
order_by: 'name',
});
+ loadedLabels = loadedLabels.concat(results);
}
+ this.setState({ loadedLabels });
} catch (err) {
this.setState({ contentError: err });
} finally {
@@ -116,19 +119,22 @@ class JobTemplateForm extends Component {
});
} else {
this.setState({
- newLabels: [...newLabels, { associate: true, id: label.id }],
+ newLabels: [
+ ...newLabels,
+ { name: label.name, associate: true, id: label.id },
+ ],
});
}
}
disassociateLabel(label) {
- const { removedLabels, newLabels } = this.state;
- const isNewCreatedLabel = newLabels.some(
- newLabel => newLabel === label.name
+ const { removedLabels, loadedLabels, newLabels } = this.state;
+ const isNewCreatedLabel = loadedLabels.some(
+ loadedLabel => loadedLabel.name !== label.name
);
if (isNewCreatedLabel) {
const filteredLabels = newLabels.filter(
- newLabel => newLabel !== label.name
+ newLabel => newLabel.name !== label.name
);
this.setState({ newLabels: filteredLabels });
} else {
@@ -277,21 +283,17 @@ class JobTemplateForm extends Component {
>
- (
-
- )}
+
handleSubmit(values)}
+ onSubmit={formik.handleSubmit}
/>
)}
diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx
index 2350298dbf..b0833f8168 100644
--- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.test.jsx
@@ -1,7 +1,8 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils';
-import JobTemplateForm from './JobTemplateForm';
+import JobTemplateForm, { _JobTemplateForm } from './JobTemplateForm';
+import { LabelsAPI } from '@api';
jest.mock('@api');
@@ -19,10 +20,16 @@ describe('', () => {
inventory: {
id: 2,
name: 'foo',
+ organization_id: 1,
},
labels: { results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }] },
},
};
+ beforeEach(() => {
+ LabelsAPI.read.mockReturnValue({
+ data: mockData.summary_fields.labels,
+ });
+ });
afterEach(() => {
jest.clearAllMocks();
@@ -36,6 +43,7 @@ describe('', () => {
handleCancel={jest.fn()}
/>
);
+ expect(LabelsAPI.read).toHaveBeenCalled();
});
test('should update form values on input changes', async () => {
@@ -104,4 +112,59 @@ describe('', () => {
wrapper.find('button[aria-label="Cancel"]').prop('onClick')();
expect(handleCancel).toBeCalled();
});
+
+ test('handleNewLabel should arrange new labels properly', async () => {
+ const handleNewLabel = jest.spyOn(
+ _JobTemplateForm.prototype,
+ 'handleNewLabel'
+ );
+ const event = { key: 'Tab' };
+ const wrapper = mountWithContexts(
+
+ );
+ const multiSelect = wrapper.find('MultiSelect');
+ const component = wrapper.find('JobTemplateForm');
+
+ wrapper.setState({ newLabels: [], loadedLabels: [], removedLabels: [] });
+ multiSelect.setState({ input: 'Foo' });
+
+ wrapper.find('input[aria-label="labels"]').prop('onKeyDown')(event);
+ expect(handleNewLabel).toHaveBeenCalledWith('Foo');
+
+ component.instance().handleNewLabel({ name: 'Bar', id: 2 });
+ expect(component.state().newLabels).toEqual([
+ { name: 'Foo', organization: 1 },
+ { associate: true, id: 2, name: 'Bar' },
+ ]);
+ });
+ test('disassociateLabel should arrange new labels properly', async () => {
+ const wrapper = mountWithContexts(
+
+ );
+ const multiSelect = wrapper.find('MultiSelect');
+ const component = wrapper.find('JobTemplateForm');
+
+ component.setState({
+ newLabels: [{ name: 'Foo', id: 1 }],
+ loadedLabels: [{ name: 'Bar', id: 3 }],
+ removedLabels: [],
+ });
+ component.update();
+ multiSelect.setState({ input: 'Wowza' });
+ component.instance().disassociateLabel({ name: 'Foo', id: 1 });
+ expect(component.state().newLabels.length).toBe(0);
+ expect(component.state().removedLabels.length).toBe(0);
+
+ component.instance().disassociateLabel({ name: 'Bar', id: 3 });
+ expect(component.state().newLabels.length).toBe(0);
+ expect(component.state().removedLabels.length).toBe(1);
+ });
});