mirror of
https://github.com/ansible/awx.git
synced 2026-05-19 14:57:39 -02:30
creat TagMultiSelect component; cleanup MultiSelect
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { shape, number, string, func, arrayOf, oneOfType } from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import { Chip, ChipGroup } from '@components/Chip';
|
import { Chip, ChipGroup } from '@components/Chip';
|
||||||
import {
|
import {
|
||||||
Dropdown as PFDropdown,
|
Dropdown as PFDropdown,
|
||||||
@@ -15,11 +13,13 @@ const InputGroup = styled.div`
|
|||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TextInput = styled(PFTextInput)`
|
const TextInput = styled(PFTextInput)`
|
||||||
border: none;
|
border: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Dropdown = styled(PFDropdown)`
|
const Dropdown = styled(PFDropdown)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.pf-c-dropdown__toggle.pf-m-plain {
|
.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 {
|
class MultiSelect extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
associatedItems: PropTypes.arrayOf(
|
associatedItems: arrayOf(Item).isRequired,
|
||||||
PropTypes.shape({
|
options: arrayOf(Item),
|
||||||
name: PropTypes.string.isRequired,
|
onAddNewItem: func,
|
||||||
})
|
onRemoveItem: func,
|
||||||
).isRequired,
|
onChange: func,
|
||||||
onAddNewItem: PropTypes.func,
|
createNewItem: func,
|
||||||
onRemoveItem: PropTypes.func,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onAddNewItem: () => {},
|
onAddNewItem: () => {},
|
||||||
onRemoveItem: () => {},
|
onRemoveItem: () => {},
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
}
|
options: [],
|
||||||
|
createNewItem: null,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -68,6 +73,7 @@ class MultiSelect extends Component {
|
|||||||
this.handleSelection = this.handleSelection.bind(this);
|
this.handleSelection = this.handleSelection.bind(this);
|
||||||
this.removeChip = this.removeChip.bind(this);
|
this.removeChip = this.removeChip.bind(this);
|
||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
|
this.createNewItem = this.createNewItem.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -80,11 +86,7 @@ class MultiSelect extends Component {
|
|||||||
|
|
||||||
getInitialChipItems() {
|
getInitialChipItems() {
|
||||||
const { associatedItems } = this.props;
|
const { associatedItems } = this.props;
|
||||||
return associatedItems.map(item => ({
|
return associatedItems.map(item => ({ ...item }));
|
||||||
name: item.name,
|
|
||||||
id: item.id,
|
|
||||||
organization: item.organization,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(e, option) {
|
handleClick(e, option) {
|
||||||
@@ -111,9 +113,21 @@ class MultiSelect extends Component {
|
|||||||
onChange(items);
|
onChange(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createNewItem(name) {
|
||||||
|
const { createNewItem } = this.props;
|
||||||
|
if (createNewItem) {
|
||||||
|
return createNewItem(name);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: Math.random(),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
handleAddItem(event) {
|
handleAddItem(event) {
|
||||||
const { input, chipItems } = this.state;
|
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);
|
const isIncluded = chipItems.some(chipItem => chipItem.name === input);
|
||||||
|
|
||||||
if (!input) {
|
if (!input) {
|
||||||
@@ -127,23 +141,25 @@ class MultiSelect extends Component {
|
|||||||
this.setState({ input: '', isExpanded: false });
|
this.setState({ input: '', isExpanded: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Enter') {
|
const isNewItem = !match || !chipItems.find(item => item.id === match.id);
|
||||||
|
if (event.key === 'Enter' && isNewItem) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const items = chipItems.concat({ name: input, id: input });
|
const items = chipItems.concat({ name: input, id: input });
|
||||||
|
const newItem = match || this.createNewItem(input);
|
||||||
this.setState({
|
this.setState({
|
||||||
chipItems: items,
|
chipItems: items,
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
input: '',
|
input: '',
|
||||||
});
|
});
|
||||||
onAddNewItem(input);
|
onAddNewItem(newItem);
|
||||||
onChange(items);
|
onChange(items);
|
||||||
} else if (event.key === 'Tab') {
|
} else if (!isNewItem || event.key === 'Tab') {
|
||||||
this.setState({ input: '' });
|
this.setState({ isExpanded: false, input: '' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputChange(e) {
|
handleInputChange(value) {
|
||||||
this.setState({ input: e, isExpanded: true });
|
this.setState({ input: value, isExpanded: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
removeChip(e, item) {
|
removeChip(e, item) {
|
||||||
@@ -226,5 +242,4 @@ class MultiSelect extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export { MultiSelect as _MultiSelect };
|
export default MultiSelect;
|
||||||
export default withI18n()(withRouter(MultiSelect));
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
import { sleep } from '@testUtils/testUtils';
|
import { sleep } from '@testUtils/testUtils';
|
||||||
import MultiSelect, { _MultiSelect } from './MultiSelect';
|
import MultiSelect from './MultiSelect';
|
||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|
||||||
|
|
||||||
describe('<MultiSelect />', () => {
|
describe('<MultiSelect />', () => {
|
||||||
const associatedItems = [
|
const associatedItems = [
|
||||||
@@ -11,11 +11,7 @@ describe('<MultiSelect />', () => {
|
|||||||
const options = [{ name: 'Angry', id: 3 }, { name: 'Potato', id: 4 }];
|
const options = [{ name: 'Angry', id: 3 }, { name: 'Potato', id: 4 }];
|
||||||
|
|
||||||
test('Initially render successfully', () => {
|
test('Initially render successfully', () => {
|
||||||
const getInitialChipItems = jest.spyOn(
|
const wrapper = mount(
|
||||||
_MultiSelect.prototype,
|
|
||||||
'getInitialChipItems'
|
|
||||||
);
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
onAddNewItem={jest.fn()}
|
onAddNewItem={jest.fn()}
|
||||||
onRemoveItem={jest.fn()}
|
onRemoveItem={jest.fn()}
|
||||||
@@ -25,12 +21,11 @@ describe('<MultiSelect />', () => {
|
|||||||
);
|
);
|
||||||
const component = wrapper.find('MultiSelect');
|
const component = wrapper.find('MultiSelect');
|
||||||
|
|
||||||
expect(getInitialChipItems).toBeCalled();
|
|
||||||
expect(component.state().chipItems.length).toBe(2);
|
expect(component.state().chipItems.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSelection add item to chipItems', async () => {
|
test('handleSelection add item to chipItems', async () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mount(
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
onAddNewItem={jest.fn()}
|
onAddNewItem={jest.fn()}
|
||||||
onRemoveItem={jest.fn()}
|
onRemoveItem={jest.fn()}
|
||||||
@@ -50,7 +45,7 @@ describe('<MultiSelect />', () => {
|
|||||||
test('handleAddItem adds a chip only when Tab is pressed', () => {
|
test('handleAddItem adds a chip only when Tab is pressed', () => {
|
||||||
const onAddNewItem = jest.fn();
|
const onAddNewItem = jest.fn();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mount(
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
onAddNewItem={onAddNewItem}
|
onAddNewItem={onAddNewItem}
|
||||||
onRemoveItem={jest.fn()}
|
onRemoveItem={jest.fn()}
|
||||||
@@ -79,7 +74,7 @@ describe('<MultiSelect />', () => {
|
|||||||
const onRemoveItem = jest.fn();
|
const onRemoveItem = jest.fn();
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
|
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mount(
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
onAddNewItem={jest.fn()}
|
onAddNewItem={jest.fn()}
|
||||||
onRemoveItem={onRemoveItem}
|
onRemoveItem={onRemoveItem}
|
||||||
|
|||||||
39
awx/ui_next/src/components/MultiSelect/TagMultiSelect.jsx
Normal file
39
awx/ui_next/src/components/MultiSelect/TagMultiSelect.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { func, string } from 'prop-types';
|
||||||
|
import MultiSelect from './MultiSelect';
|
||||||
|
|
||||||
|
function arrayToString(tags) {
|
||||||
|
return tags.map(v => 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 (
|
||||||
|
<MultiSelect
|
||||||
|
onChange={val => { 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;
|
||||||
@@ -1 +1,2 @@
|
|||||||
export { default } from './MultiSelect';
|
export { default } from './MultiSelect';
|
||||||
|
export { default as TagMultiSelect } from './TagMultiSelect';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-ic
|
|||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import AnsibleSelect from '@components/AnsibleSelect';
|
import AnsibleSelect from '@components/AnsibleSelect';
|
||||||
import MultiSelect from '@components/MultiSelect';
|
import MultiSelect, { TagMultiSelect } from '@components/MultiSelect';
|
||||||
import FormActionGroup from '@components/FormActionGroup';
|
import FormActionGroup from '@components/FormActionGroup';
|
||||||
import FormField from '@components/FormField';
|
import FormField from '@components/FormField';
|
||||||
import FormRow from '@components/FormRow';
|
import FormRow from '@components/FormRow';
|
||||||
@@ -552,24 +552,32 @@ class JobTemplateForm extends Component {
|
|||||||
t`Select the Instance Groups for this Organization to run on.`
|
t`Select the Instance Groups for this Organization to run on.`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormGroup label={i18n._(t`Job Tags`)} fieldId="template-job-tags">
|
<Field
|
||||||
<Tooltip
|
name="job_tags"
|
||||||
position="right"
|
render={({ field, form }) => {
|
||||||
content={i18n._(t`Tags are useful when you have a large
|
return (
|
||||||
playbook, and you want to run a specific part of a play
|
<FormGroup
|
||||||
or task. Use commas to separate multiple tags. Refer to
|
label={i18n._(t`Job Tags`)}
|
||||||
Ansible Tower documentation for details on the usage of
|
fieldId="template-job-tags"
|
||||||
tags.`)}
|
>
|
||||||
>
|
<Tooltip
|
||||||
<QuestionCircleIcon />
|
position="right"
|
||||||
</Tooltip>
|
content={i18n._(t`Tags are useful when you have a large
|
||||||
<MultiSelect
|
playbook, and you want to run a specific part of a
|
||||||
onAddNewItem={this.handleNewLabel}
|
play or task. Use commas to separate multiple tags.
|
||||||
onRemoveItem={this.removeLabel}
|
Refer to Ansible Tower documentation for details on
|
||||||
associatedItems={template.job_tags.split(',')}
|
the usage of tags.`)}
|
||||||
options={loadedLabels}
|
>
|
||||||
/>
|
<QuestionCircleIcon />
|
||||||
</FormGroup>
|
</Tooltip>
|
||||||
|
<TagMultiSelect
|
||||||
|
onChange={value => form.setFieldValue(field.name, value)}
|
||||||
|
value={field.value}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<FormGroup label={i18n._(t`Skip Tags`)} fieldId="template-skip-tags">
|
<FormGroup label={i18n._(t`Skip Tags`)} fieldId="template-skip-tags">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position="right"
|
position="right"
|
||||||
@@ -611,6 +619,8 @@ const FormikApp = withFormik({
|
|||||||
job_slicing,
|
job_slicing,
|
||||||
timeout,
|
timeout,
|
||||||
diff_mode,
|
diff_mode,
|
||||||
|
job_tags,
|
||||||
|
skip_tags,
|
||||||
summary_fields = { labels: { results: [] } },
|
summary_fields = { labels: { results: [] } },
|
||||||
} = { ...template };
|
} = { ...template };
|
||||||
|
|
||||||
@@ -628,6 +638,8 @@ const FormikApp = withFormik({
|
|||||||
job_slice_count: job_slicing || '',
|
job_slice_count: job_slicing || '',
|
||||||
timout: timeout || '',
|
timout: timeout || '',
|
||||||
diff_mode: diff_mode || false,
|
diff_mode: diff_mode || false,
|
||||||
|
job_tags: job_tags || '',
|
||||||
|
skip_tags: skip_tags || '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSubmit: (values, bag) => bag.props.handleSubmit(values),
|
handleSubmit: (values, bag) => bag.props.handleSubmit(values),
|
||||||
|
|||||||
Reference in New Issue
Block a user