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
name="job_tags"
render={({ field, form }) => {
return (
<FormGroup
label={i18n._(t`Job Tags`)}
fieldId="template-job-tags"
>
<Tooltip <Tooltip
position="right" position="right"
content={i18n._(t`Tags are useful when you have a large content={i18n._(t`Tags are useful when you have a large
playbook, and you want to run a specific part of a play playbook, and you want to run a specific part of a
or task. Use commas to separate multiple tags. Refer to play or task. Use commas to separate multiple tags.
Ansible Tower documentation for details on the usage of Refer to Ansible Tower documentation for details on
tags.`)} the usage of tags.`)}
> >
<QuestionCircleIcon /> <QuestionCircleIcon />
</Tooltip> </Tooltip>
<MultiSelect <TagMultiSelect
onAddNewItem={this.handleNewLabel} onChange={value => form.setFieldValue(field.name, value)}
onRemoveItem={this.removeLabel} value={field.value}
associatedItems={template.job_tags.split(',')}
options={loadedLabels}
/> />
</FormGroup> </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),