creat TagMultiSelect component; cleanup MultiSelect

This commit is contained in:
Keith Grant 2019-08-28 15:45:35 -07:00
parent 9edc686ab5
commit 218348412b
5 changed files with 118 additions and 56 deletions

View File

@ -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;

View File

@ -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}

View 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;

View File

@ -1 +1,2 @@
export { default } from './MultiSelect';
export { default as TagMultiSelect } from './TagMultiSelect';

View File

@ -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),