Merge pull request #5288 from AlexSCorey/5270-InventoryAddLinks

Adds AddDropDownButton removes TemplateAddButton

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2019-11-12 21:08:56 +00:00 committed by GitHub
commit b0d31a64aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 111 additions and 138 deletions

View File

@ -6,4 +6,6 @@ export const asyncFlush = () => new Promise((resolve) => setImmediate(resolve));
const enzyme = require('enzyme');
const Adapter = require('enzyme-adapter-react-16');
jest.setTimeout(5000 * 4);
enzyme.configure({ adapter: new Adapter() });

View File

@ -1,11 +1,10 @@
import React, { useState, useRef, useEffect } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { Dropdown, DropdownPosition } from '@patternfly/react-core';
import { ToolbarAddButton } from '@components/PaginatedDataList';
function TemplateAddButton({ match, i18n }) {
function AddDropDownButton({ dropdownItems }) {
const [isOpen, setIsOpen] = useState(false);
const element = useRef(null);
@ -29,26 +28,28 @@ function TemplateAddButton({ match, i18n }) {
isOpen={isOpen}
position={DropdownPosition.right}
toggle={<ToolbarAddButton onClick={() => setIsOpen(!isOpen)} />}
dropdownItems={[
dropdownItems={dropdownItems.map(item => (
<Link
key="job"
className="pf-c-dropdown__menu-item"
to={`${match.url}/job_template/add/`}
key={item.url}
to={item.url}
>
{i18n._(t`Job Template`)}
</Link>,
<Link
key="workflow"
className="pf-c-dropdown__menu-item"
to={`${match.url}_workflow/add/`}
>
{i18n._(t`Workflow Template`)}
</Link>,
]}
{item.label}
</Link>
))}
/>
</div>
);
}
export { TemplateAddButton as _TemplateAddButton };
export default withI18n()(withRouter(TemplateAddButton));
AddDropDownButton.propTypes = {
dropdownItems: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
})
).isRequired,
};
export { AddDropDownButton as _AddDropDownButton };
export default AddDropDownButton;

View File

@ -1,22 +1,35 @@
import React from 'react';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
import TemplateAddButton from './TemplateAddButton';
import AddDropDownButton from './AddDropDownButton';
describe('<TemplateAddButton />', () => {
describe('<AddDropDownButton />', () => {
const dropdownItems = [
{
key: 'inventory',
label: 'Inventory',
url: `inventory/inventory/add/`,
},
];
test('should be closed initially', () => {
const wrapper = mountWithContexts(<TemplateAddButton />);
const wrapper = mountWithContexts(
<AddDropDownButton dropdownItems={dropdownItems} />
);
expect(wrapper.find('Dropdown').prop('isOpen')).toEqual(false);
});
test('should render two links', () => {
const wrapper = mountWithContexts(<TemplateAddButton />);
const wrapper = mountWithContexts(
<AddDropDownButton dropdownItems={dropdownItems} />
);
wrapper.find('button').simulate('click');
expect(wrapper.find('Dropdown').prop('isOpen')).toEqual(true);
expect(wrapper.find('Link')).toHaveLength(2);
expect(wrapper.find('Link')).toHaveLength(dropdownItems.length);
});
test('should close when button re-clicked', () => {
const wrapper = mountWithContexts(<TemplateAddButton />);
const wrapper = mountWithContexts(
<AddDropDownButton dropdownItems={dropdownItems} />
);
wrapper.find('button').simulate('click');
expect(wrapper.find('Dropdown').prop('isOpen')).toEqual(true);
wrapper.find('button').simulate('click');

View File

@ -0,0 +1 @@
export { default } from './AddDropDownButton';

View File

@ -1,15 +1,9 @@
import React, { Component } from 'react';
import { withRouter, Link } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Card,
PageSection,
Dropdown,
DropdownItem,
DropdownPosition,
} from '@patternfly/react-core';
import { Card, PageSection } from '@patternfly/react-core';
import { InventoriesAPI } from '@api';
import AlertModal from '@components/AlertModal';
@ -17,10 +11,10 @@ import DatalistToolbar from '@components/DataListToolbar';
import ErrorDetail from '@components/ErrorDetail';
import PaginatedDataList, {
ToolbarDeleteButton,
ToolbarAddButton,
} from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs';
import { getQSConfig, parseQueryString } from '@util/qs';
import AddDropDownButton from '@components/AddDropDownButton';
import InventoryListItem from './InventoryListItem';
// The type value in const QS_CONFIG below does not have a space between job_inventory and
@ -42,7 +36,6 @@ class InventoriesList extends Component {
selected: [],
inventories: [],
itemCount: 0,
isAddOpen: false,
};
this.loadInventories = this.loadInventories.bind(this);
@ -50,7 +43,6 @@ class InventoriesList extends Component {
this.handleSelect = this.handleSelect.bind(this);
this.handleInventoryDelete = this.handleInventoryDelete.bind(this);
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
this.handleAddToggle = this.handleAddToggle.bind(this);
}
componentDidMount() {
@ -65,10 +57,6 @@ class InventoriesList extends Component {
}
}
componentWillUnmount() {
document.removeEventListener('click', this.handleAddToggle, false);
}
handleDeleteErrorClose() {
this.setState({ deletionError: null });
}
@ -88,21 +76,6 @@ class InventoriesList extends Component {
}
}
handleAddToggle(e) {
const { isAddOpen } = this.state;
document.addEventListener('click', this.handleAddToggle, false);
if (this.node && this.node.contains(e.target) && isAddOpen) {
document.removeEventListener('click', this.handleAddToggle, false);
this.setState({ isAddOpen: false });
} else if (this.node && this.node.contains(e.target) && !isAddOpen) {
this.setState({ isAddOpen: true });
} else {
this.setState({ isAddOpen: false });
document.removeEventListener('click', this.handleAddToggle, false);
}
}
async handleInventoryDelete() {
const { selected, itemCount } = this.state;
@ -168,14 +141,27 @@ class InventoriesList extends Component {
inventories,
itemCount,
selected,
isAddOpen,
actions,
} = this.state;
const { match, i18n } = this.props;
const canAdd =
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
const isAllSelected =
selected.length > 0 && selected.length === inventories.length;
const isAllSelected = selected.length === inventories.length;
const addButton = (
<AddDropDownButton
key="add"
dropdownItems={[
{
label: i18n._(t`Inventory`),
url: `${match.url}/inventory/add/`,
},
{
label: i18n._(t`Smart Inventory`),
url: `${match.url}/smart_inventory/add/`,
},
]}
/>
);
return (
<PageSection>
<Card>
@ -221,35 +207,7 @@ class InventoriesList extends Component {
itemsToDelete={selected}
pluralizedItemName="Inventories"
/>,
canAdd && (
<div
ref={node => {
this.node = node;
}}
key="add"
>
<Dropdown
isPlain
isOpen={isAddOpen}
position={DropdownPosition.right}
toggle={
<ToolbarAddButton onClick={this.handleAddToggle} />
}
dropdownItems={[
<DropdownItem key="inventory">
<Link to={`${match.url}/inventory/add/`}>
{i18n._(t`Inventory`)}
</Link>
</DropdownItem>,
<DropdownItem key="smart_inventory">
<Link to={`${match.url}/smart_inventory/add/`}>
{i18n._(t`Smart Inventory`)}
</Link>
</DropdownItem>,
]}
/>
</div>
),
canAdd && addButton,
]}
/>
)}
@ -267,35 +225,7 @@ class InventoriesList extends Component {
isSelected={selected.some(row => row.id === inventory.id)}
/>
)}
emptyStateControls={
canAdd && (
<div
ref={node => {
this.node = node;
}}
key="add"
>
<Dropdown
isPlain
isOpen={isAddOpen}
position={DropdownPosition.right}
toggle={<ToolbarAddButton onClick={this.handleAddToggle} />}
dropdownItems={[
<DropdownItem key="inventory">
<Link to={`${match.url}/inventory/add/`}>
{i18n._(t`Inventory`)}
</Link>
</DropdownItem>,
<DropdownItem key="smart_inventory">
<Link to={`${match.url}/smart_inventory/add/`}>
{i18n._(t`Smart Inventory`)}
</Link>
</DropdownItem>,
]}
/>
</div>
)
}
emptyStateControls={canAdd && addButton}
/>
</Card>
<AlertModal

View File

@ -284,7 +284,7 @@ describe('<InventoriesList />', () => {
done();
});
test('Add button shown for users without ability to POST', async done => {
test('Add button shown for users with ability to POST', async done => {
const wrapper = mountWithContexts(<InventoriesList />);
await waitForElement(
wrapper,

View File

@ -26,7 +26,7 @@ describe('<ProjectForm />', () => {
id: 100,
credential_type_id: 4,
kind: 'scm',
name: 'alpha',
name: 'Foo',
},
},
};

View File

@ -200,17 +200,22 @@ describe('<JobTemplateEdit />', () => {
data: { ...updatedTemplateData },
});
const formik = wrapper.find('Formik').instance();
const changeState = new Promise(resolve => {
const values = {
...mockJobTemplate,
...updatedTemplateData,
labels,
instanceGroups: [],
};
formik.setState({ values }, () => resolve());
});
const changeState = await act(
() =>
new Promise(resolve => {
const values = {
...mockJobTemplate,
...updatedTemplateData,
labels,
instanceGroups: [],
};
formik.setState({ values }, () => resolve());
})
);
await changeState;
wrapper.find('button[aria-label="Save"]').simulate('click');
await act(async () => {
wrapper.find('button[aria-label="Save"]').simulate('click');
});
await sleep(0);
expect(JobTemplatesAPI.update).toHaveBeenCalledWith(1, {
@ -236,7 +241,9 @@ describe('<JobTemplateEdit />', () => {
'button[aria-label="Cancel"]',
e => e.length === 1
);
cancelButton.prop('onClick')();
await act(async () => {
cancelButton.prop('onClick')();
});
expect(history.location.pathname).toEqual(
'/templates/job_template/1/details'
);

View File

@ -13,8 +13,8 @@ import PaginatedDataList, {
} from '@components/PaginatedDataList';
import { getQSConfig, parseQueryString } from '@util/qs';
import AddDropDownButton from '@components/AddDropDownButton';
import TemplateListItem from './TemplateListItem';
import TemplateAddButton from './TemplateAddButton';
// The type value in const QS_CONFIG below does not have a space between job_template and
// workflow_job_template so the params sent to the API match what the api expects.
@ -161,6 +161,21 @@ class TemplatesList extends Component {
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
const isAllSelected =
selected.length === templates.length && selected.length > 0;
const addButton = (
<AddDropDownButton
key="add"
dropdownItems={[
{
label: i18n._(t`Template`),
url: `${match.url}/template/add/`,
},
{
label: i18n._(t`Workflow Template`),
url: `${match.url}/_workflow/add/`,
},
]}
/>
);
return (
<PageSection>
<Card>
@ -206,7 +221,7 @@ class TemplatesList extends Component {
itemsToDelete={selected}
pluralizedItemName="Templates"
/>,
canAdd && <TemplateAddButton key="add" />,
canAdd && addButton,
]}
/>
)}
@ -220,7 +235,7 @@ class TemplatesList extends Component {
isSelected={selected.some(row => row.id === template.id)}
/>
)}
emptyStateControls={canAdd && <TemplateAddButton />}
emptyStateControls={canAdd && addButton}
/>
</Card>
<AlertModal

View File

@ -157,10 +157,12 @@ describe('<JobTemplateForm />', () => {
target: { value: 'new baz type', name: 'playbook' },
});
expect(form.state('values').playbook).toEqual('new baz type');
wrapper
.find('CredentialChip')
.at(0)
.prop('onClick')();
await act(async () => {
wrapper
.find('CredentialChip')
.at(0)
.prop('onClick')();
});
expect(form.state('values').credentials).toEqual([
{ id: 2, kind: 'ssh', name: 'Bar' },
]);
@ -180,7 +182,9 @@ describe('<JobTemplateForm />', () => {
});
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
expect(handleSubmit).not.toHaveBeenCalled();
wrapper.find('button[aria-label="Save"]').simulate('click');
await act(async () => {
wrapper.find('button[aria-label="Save"]').simulate('click');
});
await sleep(1);
expect(handleSubmit).toBeCalled();
});