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

View File

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

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