Fixes failing test and addresses PR issues

This commit is contained in:
Alex Corey 2019-08-01 14:25:41 -04:00
parent 74a1ebff32
commit 8b35642b08
9 changed files with 111 additions and 79 deletions

View File

@ -1,8 +1,6 @@
import axios from 'axios';
import {
encodeQueryString
} from '@util/qs';
import { encodeQueryString } from '@util/qs';
const defaultHttp = axios.create({
xsrfCookieName: 'csrftoken',
@ -28,7 +26,7 @@ class Base {
read(params) {
return this.http.get(this.baseUrl, {
params
params,
});
}

View File

@ -8,7 +8,9 @@ class JobTemplates extends InstanceGroupsMixin(Base) {
this.launch = this.launch.bind(this);
this.readLaunch = this.readLaunch.bind(this);
this.updateLabels = this.updateLabels.bind(this);
this.associateLabel = this.associateLabel.bind(this);
this.disassociateLabel = this.disassociateLabel.bind(this);
this.generateLabel = this.generateLabel.bind(this);
}
launch(id, data) {
@ -19,8 +21,16 @@ class JobTemplates extends InstanceGroupsMixin(Base) {
return this.http.get(`${this.baseUrl}${id}/launch/`);
}
updateLabels(id, data) {
return this.http.post(`${this.baseUrl}${id}/labels/`, data)
associateLabel(id, label) {
return this.http.post(`${this.baseUrl}${id}/labels/`, label);
}
disassociateLabel(id, label) {
return this.http.post(`${this.baseUrl}${id}/labels/`, label);
}
generateLabel(orgId, label) {
return this.http.post(`${this.baseUrl}${orgId}/labels/`, label);
}
}

View File

@ -51,10 +51,9 @@ class MultiSelect extends Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.state = {
input: '',
chipItems: [],
chipItems: this.getInitialChipItems(),
isExpanded: false,
};
this.handleAddItem = this.handleAddItem.bind(this);
@ -65,57 +64,58 @@ class MultiSelect extends Component {
}
componentDidMount() {
this.renderChips();
document.addEventListener('mousedown', this.handleClick, false);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClick, false);
}
getInitialChipItems() {
const { associatedItems } = this.props;
return associatedItems.map(item => ({
name: item.name,
id: item.id,
organization: item.organization,
}));
}
handleClick(e, option) {
if (this.node && this.node.contains(e.target)) {
if (option) {
this.handleSelection(e, option);
}
this.setState({ isExpanded: true });
} else {
this.setState({ isExpanded: false });
}
}
renderChips() {
const { associatedItems } = this.props;
const items = associatedItems.map(item => ({
name: item.name,
id: item.id,
organization: item.organization,
}));
this.setState({
chipItems: items,
});
}
handleSelection(e, item) {
const { chipItems } = this.state;
const { onAddNewItem } = this.props;
e.preventDefault();
this.setState({
chipItems: chipItems.concat({ name: item.name, id: item.id }),
isExpanded: false,
});
onAddNewItem(item);
e.preventDefault();
}
handleAddItem(event) {
const { input, chipItems } = this.state;
const { onAddNewItem } = this.props;
const newChip = { name: input, id: Math.random() };
if (event.key === 'Tab') {
this.setState({
chipItems: chipItems.concat(newChip),
isExpanded: false,
input: '',
});
onAddNewItem(input);
if (event.key !== 'Tab') {
return;
}
this.setState({
chipItems: chipItems.concat(newChip),
isExpanded: false,
input: '',
});
onAddNewItem(input);
}
handleInputChange(e) {

View File

@ -1,4 +1,5 @@
import React from 'react';
import { sleep } from '@testUtils/testUtils';
import MultiSelect, { _MultiSelect } from './MultiSelect';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
@ -10,7 +11,10 @@ describe('<MultiSelect />', () => {
const options = [{ name: 'Angry', id: 3 }, { name: 'Potato', id: 4 }];
test('Initially render successfully', () => {
const renderChips = jest.spyOn(_MultiSelect.prototype, 'renderChips');
const getInitialChipItems = jest.spyOn(
_MultiSelect.prototype,
'getInitialChipItems'
);
const wrapper = mountWithContexts(
<MultiSelect
onAddNewItem={jest.fn()}
@ -21,7 +25,7 @@ describe('<MultiSelect />', () => {
);
const component = wrapper.find('MultiSelect');
expect(renderChips).toBeCalled();
expect(getInitialChipItems).toBeCalled();
expect(component.state().chipItems.length).toBe(2);
});
test('handleSelection add item to chipItems', async () => {
@ -33,9 +37,12 @@ describe('<MultiSelect />', () => {
options={options}
/>
);
const event = { preventDefault: () => {} };
const component = wrapper.find('MultiSelect');
component.instance().handleSelection(event, { name: 'Apollo', id: 5 });
component
.find('input[aria-label="labels"]')
.simulate('keydown', { key: 'Tab' });
component.update();
await sleep(1);
expect(component.state().chipItems.length).toBe(3);
});
test('handleAddItem adds a chip only when Tab is pressed', () => {

View File

@ -1,4 +1 @@
export {
default
}
from './MultiSelect';
export { default } from './MultiSelect';

View File

@ -21,28 +21,27 @@ class JobTemplateEdit extends Component {
this.handleSubmit = this.handleSubmit.bind(this);
}
async handleSubmit(values, newLabels, removedLabels) {
async handleSubmit(values, newLabels = [], removedLabels = []) {
const {
template: { id, type },
history,
} = this.props;
const disassociatedLabels = removedLabels
? removedLabels.forEach(removedLabel =>
JobTemplatesAPI.updateLabels(id, removedLabel)
)
: null;
const disassociatedLabels = removedLabels.forEach(removedLabel =>
JobTemplatesAPI.disassociateLabel(id, removedLabel)
);
const associatedLabels = newLabels
? newLabels.forEach(newLabel =>
JobTemplatesAPI.updateLabels(id, newLabel)
)
: null;
.filter(newLabel => !newLabel.organization)
.forEach(newLabel => JobTemplatesAPI.associateLabel(id, newLabel));
const generatedLabels = newLabels
.filter(newLabel => newLabel.organization)
.forEach(newLabel => JobTemplatesAPI.generateLabel(id, newLabel));
try {
await Promise.all([
JobTemplatesAPI.update(id, { ...values }),
disassociatedLabels,
associatedLabels,
generatedLabels,
]);
history.push(`/templates/${type}/${id}/details`);
} catch (error) {

View File

@ -36,9 +36,26 @@ describe('<JobTemplateEdit />', () => {
description: 'new description',
job_type: 'check',
};
const newLabels = [
{ associate: true, id: 3 },
{ associate: true, id: 3 },
{ name: 'Mapel', organization: 1 },
{ name: 'Tree', organization: 1 },
];
const removedLabels = [
{ disassociate: true, id: 1 },
{ disassociate: true, id: 2 },
];
wrapper.find('JobTemplateForm').prop('handleSubmit')(updatedTemplateData);
wrapper.find('JobTemplateForm').prop('handleSubmit')(
updatedTemplateData,
newLabels,
removedLabels
);
expect(JobTemplatesAPI.update).toHaveBeenCalledWith(1, updatedTemplateData);
expect(JobTemplatesAPI.disassociateLabel).toHaveBeenCalledTimes(2);
expect(JobTemplatesAPI.associateLabel).toHaveBeenCalledTimes(2);
expect(JobTemplatesAPI.generateLabel).toHaveBeenCalledTimes(2);
});
test('should navigate to job template detail when cancel is clicked', () => {

View File

@ -67,13 +67,18 @@ class JobTemplateForm extends Component {
};
this.handleNewLabel = this.handleNewLabel.bind(this);
this.loadLabels = this.loadLabels.bind(this);
this.disassociateLabel = this.disassociateLabel.bind(this);
this.removeLabel = this.removeLabel.bind(this);
}
componentDidMount() {
this.loadLabels(QSConfig);
}
// The function below assumes that the user has no more than 400
// labels. For the vast majority of users this will be more thans
// enough.This can be updated to allow more than 400 labels if we
// decide it is necessary.
async loadLabels(QueryConfig) {
this.setState({ contentError: null, hasContentLoading: true });
let loadedLabels;
@ -127,23 +132,26 @@ class JobTemplateForm extends Component {
}
}
disassociateLabel(label) {
const { removedLabels, loadedLabels, newLabels } = this.state;
const isNewCreatedLabel = loadedLabels.some(
loadedLabel => loadedLabel.name !== label.name
removeLabel(label) {
const { removedLabels, newLabels } = this.state;
const { template } = this.props;
const isAssociatedLabel = template.summary_fields.labels.results.some(
tempLabel => tempLabel.id === label.id
);
if (isNewCreatedLabel) {
const filteredLabels = newLabels.filter(
newLabel => newLabel.name !== label.name
);
this.setState({ newLabels: filteredLabels });
} else {
if (isAssociatedLabel) {
this.setState({
removedLabels: removedLabels.concat({
disassociate: true,
id: label.id,
}),
});
} else {
const filteredLabels = newLabels.filter(
newLabel => newLabel.name !== label.name
);
this.setState({ newLabels: filteredLabels });
}
}
@ -285,7 +293,7 @@ class JobTemplateForm extends Component {
</Tooltip>
<MultiSelect
onAddNewItem={this.handleNewLabel}
onRemoveItem={this.disassociateLabel}
onRemoveItem={this.removeLabel}
associatedItems={template.summary_fields.labels.results}
options={loadedLabels}
/>

View File

@ -36,14 +36,16 @@ describe('<JobTemplateForm />', () => {
});
test('initially renders successfully', () => {
mountWithContexts(
const wrapper = mountWithContexts(
<JobTemplateForm
template={mockData}
handleSubmit={jest.fn()}
handleCancel={jest.fn()}
/>
);
const component = wrapper.find('ChipGroup');
expect(LabelsAPI.read).toHaveBeenCalled();
expect(component.find('span#pf-random-id-1').text()).toEqual('Sushi');
});
test('should update form values on input changes', async () => {
@ -131,8 +133,7 @@ describe('<JobTemplateForm />', () => {
wrapper.setState({ newLabels: [], loadedLabels: [], removedLabels: [] });
multiSelect.setState({ input: 'Foo' });
wrapper.find('input[aria-label="labels"]').prop('onKeyDown')(event);
component.find('input[aria-label="labels"]').prop('onKeyDown')(event);
expect(handleNewLabel).toHaveBeenCalledWith('Foo');
component.instance().handleNewLabel({ name: 'Bar', id: 2 });
@ -149,21 +150,16 @@ describe('<JobTemplateForm />', () => {
handleCancel={jest.fn()}
/>
);
const multiSelect = wrapper.find('MultiSelect');
const component = wrapper.find('JobTemplateForm');
component.setState({
newLabels: [{ name: 'Foo', id: 1 }],
loadedLabels: [{ name: 'Bar', id: 3 }],
removedLabels: [],
});
component.update();
multiSelect.setState({ input: 'Wowza' });
component.instance().disassociateLabel({ name: 'Foo', id: 1 });
// This asserts that the user generated a label or clicked
// on a label option, and then changed their mind and
// removed the label.
component.instance().removeLabel({ name: 'Alex', id: 17 });
expect(component.state().newLabels.length).toBe(0);
expect(component.state().removedLabels.length).toBe(0);
component.instance().disassociateLabel({ name: 'Bar', id: 3 });
// This asserts that the user removed a label that was associated
// with the template when the template loaded.
component.instance().removeLabel({ name: 'Sushi', id: 1 });
expect(component.state().newLabels.length).toBe(0);
expect(component.state().removedLabels.length).toBe(1);
});