diff --git a/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx b/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx
index 62352b0036..930664334d 100644
--- a/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx
+++ b/awx/ui_next/src/components/MultiSelect/MultiSelect.jsx
@@ -1,7 +1,5 @@
import React, { Component, Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withI18n } from '@lingui/react';
-import { withRouter } from 'react-router-dom';
+import { shape, number, string, func, arrayOf, oneOfType } from 'prop-types';
import { Chip, ChipGroup } from '@components/Chip';
import {
Dropdown as PFDropdown,
@@ -15,11 +13,13 @@ const InputGroup = styled.div`
border: 1px solid black;
margin-top: 2px;
`;
+
const TextInput = styled(PFTextInput)`
border: none;
width: 100%;
padding-left: 8px;
`;
+
const Dropdown = styled(PFDropdown)`
width: 100%;
.pf-c-dropdown__toggle.pf-m-plain {
@@ -38,23 +38,28 @@ const Dropdown = styled(PFDropdown)`
}
`;
+const Item = shape({
+ id: oneOfType([number, string]).isRequired,
+ name: string.isRequired,
+});
+
class MultiSelect extends Component {
static propTypes = {
- associatedItems: PropTypes.arrayOf(
- PropTypes.shape({
- name: PropTypes.string.isRequired,
- })
- ).isRequired,
- onAddNewItem: PropTypes.func,
- onRemoveItem: PropTypes.func,
- onChange: PropTypes.func,
+ associatedItems: arrayOf(Item).isRequired,
+ options: arrayOf(Item),
+ onAddNewItem: func,
+ onRemoveItem: func,
+ onChange: func,
+ createNewItem: func,
};
static defaultProps = {
onAddNewItem: () => {},
onRemoveItem: () => {},
onChange: () => {},
- }
+ options: [],
+ createNewItem: null,
+ };
constructor(props) {
super(props);
@@ -68,6 +73,7 @@ class MultiSelect extends Component {
this.handleSelection = this.handleSelection.bind(this);
this.removeChip = this.removeChip.bind(this);
this.handleClick = this.handleClick.bind(this);
+ this.createNewItem = this.createNewItem.bind(this);
}
componentDidMount() {
@@ -80,11 +86,7 @@ class MultiSelect extends Component {
getInitialChipItems() {
const { associatedItems } = this.props;
- return associatedItems.map(item => ({
- name: item.name,
- id: item.id,
- organization: item.organization,
- }));
+ return associatedItems.map(item => ({ ...item }));
}
handleClick(e, option) {
@@ -111,9 +113,21 @@ class MultiSelect extends Component {
onChange(items);
}
+ createNewItem(name) {
+ const { createNewItem } = this.props;
+ if (createNewItem) {
+ return createNewItem(name);
+ }
+ return {
+ id: Math.random(),
+ name,
+ };
+ }
+
handleAddItem(event) {
const { input, chipItems } = this.state;
- const { onAddNewItem, onChange } = this.props;
+ const { options, onAddNewItem, onChange } = this.props;
+ const match = options.find(item => item.name === input);
const isIncluded = chipItems.some(chipItem => chipItem.name === input);
if (!input) {
@@ -127,23 +141,25 @@ class MultiSelect extends Component {
this.setState({ input: '', isExpanded: false });
return;
}
- if (event.key === 'Enter') {
+ const isNewItem = !match || !chipItems.find(item => item.id === match.id);
+ if (event.key === 'Enter' && isNewItem) {
event.preventDefault();
const items = chipItems.concat({ name: input, id: input });
+ const newItem = match || this.createNewItem(input);
this.setState({
chipItems: items,
isExpanded: false,
input: '',
});
- onAddNewItem(input);
+ onAddNewItem(newItem);
onChange(items);
- } else if (event.key === 'Tab') {
- this.setState({ input: '' });
+ } else if (!isNewItem || event.key === 'Tab') {
+ this.setState({ isExpanded: false, input: '' });
}
}
- handleInputChange(e) {
- this.setState({ input: e, isExpanded: true });
+ handleInputChange(value) {
+ this.setState({ input: value, isExpanded: true });
}
removeChip(e, item) {
@@ -226,5 +242,4 @@ class MultiSelect extends Component {
);
}
}
-export { MultiSelect as _MultiSelect };
-export default withI18n()(withRouter(MultiSelect));
+export default MultiSelect;
diff --git a/awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx b/awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx
index 3832cea6fe..e52e56b3c1 100644
--- a/awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx
+++ b/awx/ui_next/src/components/MultiSelect/MultiSelect.test.jsx
@@ -1,7 +1,7 @@
import React from 'react';
+import { mount } from 'enzyme';
import { sleep } from '@testUtils/testUtils';
-import MultiSelect, { _MultiSelect } from './MultiSelect';
-import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
+import MultiSelect from './MultiSelect';
describe('', () => {
const associatedItems = [
@@ -11,11 +11,7 @@ describe('', () => {
const options = [{ name: 'Angry', id: 3 }, { name: 'Potato', id: 4 }];
test('Initially render successfully', () => {
- const getInitialChipItems = jest.spyOn(
- _MultiSelect.prototype,
- 'getInitialChipItems'
- );
- const wrapper = mountWithContexts(
+ const wrapper = mount(
', () => {
);
const component = wrapper.find('MultiSelect');
- expect(getInitialChipItems).toBeCalled();
expect(component.state().chipItems.length).toBe(2);
});
test('handleSelection add item to chipItems', async () => {
- const wrapper = mountWithContexts(
+ const wrapper = mount(
', () => {
test('handleAddItem adds a chip only when Tab is pressed', () => {
const onAddNewItem = jest.fn();
const onChange = jest.fn();
- const wrapper = mountWithContexts(
+ const wrapper = mount(
', () => {
const onRemoveItem = jest.fn();
const onChange = jest.fn();
- const wrapper = mountWithContexts(
+ const wrapper = mount(
v.name).join(',');
+}
+
+function stringToArray(value) {
+ return value.split(',').map(v => ({
+ id: v,
+ name: v,
+ }));
+}
+
+function TagMultiSelect ({ onChange, value }) {
+ const [options, setOptions] = useState(stringToArray(value));
+
+ return (
+ { onChange(arrayToString(val)) }}
+ onAddNewItem={(newItem) => {
+ if (!options.find(o => o.name === newItem.name)) {
+ setOptions(options.concat(newItem));
+ }
+ }}
+ associatedItems={stringToArray(value)}
+ options={options}
+ createNewItem={(name) => ({ id: name, name })}
+ />
+ )
+}
+
+TagMultiSelect.propTypes = {
+ onChange: func.isRequired,
+ value: string.isRequired,
+}
+
+export default TagMultiSelect;
diff --git a/awx/ui_next/src/components/MultiSelect/index.js b/awx/ui_next/src/components/MultiSelect/index.js
index 8cda42c7cb..d983765272 100644
--- a/awx/ui_next/src/components/MultiSelect/index.js
+++ b/awx/ui_next/src/components/MultiSelect/index.js
@@ -1 +1,2 @@
export { default } from './MultiSelect';
+export { default as TagMultiSelect } from './TagMultiSelect';
diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
index 518f0ebc72..bd686bf3cf 100644
--- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
+++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx
@@ -9,7 +9,7 @@ import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-ic
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import AnsibleSelect from '@components/AnsibleSelect';
-import MultiSelect from '@components/MultiSelect';
+import MultiSelect, { TagMultiSelect } from '@components/MultiSelect';
import FormActionGroup from '@components/FormActionGroup';
import FormField from '@components/FormField';
import FormRow from '@components/FormRow';
@@ -552,24 +552,32 @@ class JobTemplateForm extends Component {
t`Select the Instance Groups for this Organization to run on.`
)}
/>
-
-
-
-
-
-
+ {
+ return (
+
+
+
+
+ form.setFieldValue(field.name, value)}
+ value={field.value}
+ />
+
+ );
+ }}
+ />
bag.props.handleSubmit(values),