mirror of
https://github.com/ansible/awx.git
synced 2026-01-25 00:11:23 -03:30
creat TagMultiSelect component; cleanup MultiSelect
This commit is contained in:
parent
9edc686ab5
commit
218348412b
@ -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;
|
||||
|
||||
@ -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('<MultiSelect />', () => {
|
||||
const associatedItems = [
|
||||
@ -11,11 +11,7 @@ describe('<MultiSelect />', () => {
|
||||
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(
|
||||
<MultiSelect
|
||||
onAddNewItem={jest.fn()}
|
||||
onRemoveItem={jest.fn()}
|
||||
@ -25,12 +21,11 @@ describe('<MultiSelect />', () => {
|
||||
);
|
||||
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(
|
||||
<MultiSelect
|
||||
onAddNewItem={jest.fn()}
|
||||
onRemoveItem={jest.fn()}
|
||||
@ -50,7 +45,7 @@ describe('<MultiSelect />', () => {
|
||||
test('handleAddItem adds a chip only when Tab is pressed', () => {
|
||||
const onAddNewItem = jest.fn();
|
||||
const onChange = jest.fn();
|
||||
const wrapper = mountWithContexts(
|
||||
const wrapper = mount(
|
||||
<MultiSelect
|
||||
onAddNewItem={onAddNewItem}
|
||||
onRemoveItem={jest.fn()}
|
||||
@ -79,7 +74,7 @@ describe('<MultiSelect />', () => {
|
||||
const onRemoveItem = jest.fn();
|
||||
const onChange = jest.fn();
|
||||
|
||||
const wrapper = mountWithContexts(
|
||||
const wrapper = mount(
|
||||
<MultiSelect
|
||||
onAddNewItem={jest.fn()}
|
||||
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 as TagMultiSelect } from './TagMultiSelect';
|
||||
|
||||
@ -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.`
|
||||
)}
|
||||
/>
|
||||
<FormGroup label={i18n._(t`Job Tags`)} fieldId="template-job-tags">
|
||||
<Tooltip
|
||||
position="right"
|
||||
content={i18n._(t`Tags are useful when you have a large
|
||||
playbook, and you want to run a specific part of a play
|
||||
or task. Use commas to separate multiple tags. Refer to
|
||||
Ansible Tower documentation for details on the usage of
|
||||
tags.`)}
|
||||
>
|
||||
<QuestionCircleIcon />
|
||||
</Tooltip>
|
||||
<MultiSelect
|
||||
onAddNewItem={this.handleNewLabel}
|
||||
onRemoveItem={this.removeLabel}
|
||||
associatedItems={template.job_tags.split(',')}
|
||||
options={loadedLabels}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Field
|
||||
name="job_tags"
|
||||
render={({ field, form }) => {
|
||||
return (
|
||||
<FormGroup
|
||||
label={i18n._(t`Job Tags`)}
|
||||
fieldId="template-job-tags"
|
||||
>
|
||||
<Tooltip
|
||||
position="right"
|
||||
content={i18n._(t`Tags are useful when you have a large
|
||||
playbook, and you want to run a specific part of a
|
||||
play or task. Use commas to separate multiple tags.
|
||||
Refer to Ansible Tower documentation for details on
|
||||
the usage of tags.`)}
|
||||
>
|
||||
<QuestionCircleIcon />
|
||||
</Tooltip>
|
||||
<TagMultiSelect
|
||||
onChange={value => form.setFieldValue(field.name, value)}
|
||||
value={field.value}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormGroup label={i18n._(t`Skip Tags`)} fieldId="template-skip-tags">
|
||||
<Tooltip
|
||||
position="right"
|
||||
@ -611,6 +619,8 @@ const FormikApp = withFormik({
|
||||
job_slicing,
|
||||
timeout,
|
||||
diff_mode,
|
||||
job_tags,
|
||||
skip_tags,
|
||||
summary_fields = { labels: { results: [] } },
|
||||
} = { ...template };
|
||||
|
||||
@ -628,6 +638,8 @@ const FormikApp = withFormik({
|
||||
job_slice_count: job_slicing || '',
|
||||
timout: timeout || '',
|
||||
diff_mode: diff_mode || false,
|
||||
job_tags: job_tags || '',
|
||||
skip_tags: skip_tags || '',
|
||||
};
|
||||
},
|
||||
handleSubmit: (values, bag) => bag.props.handleSubmit(values),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user