Refactors to show add button properly in Advanced Search mode

This commit is contained in:
Alex Corey 2020-09-29 11:41:54 -04:00
parent 9620da287c
commit f604065246
7 changed files with 168 additions and 98 deletions

View File

@ -1,9 +1,11 @@
import React, { useState } from 'react';
import React, { useState, useEffect, useContext } from 'react';
import { arrayOf, func, object, string } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Button, Tooltip } from '@patternfly/react-core';
import { Button, Tooltip, DropdownItem } from '@patternfly/react-core';
import styled from 'styled-components';
import { KebabifiedContext } from '../../contexts/Kebabified';
import AlertModal from '../AlertModal';
const ModalNote = styled.div`
@ -19,12 +21,19 @@ function DisassociateButton({
verifyCannotDisassociate = true,
}) {
const [isOpen, setIsOpen] = useState(false);
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
function handleDisassociate() {
onDisassociate();
setIsOpen(false);
}
useEffect(() => {
if (isKebabified) {
onKebabModalChange(isOpen);
}
}, [isKebabified, isOpen, onKebabModalChange]);
function cannotDisassociate(item) {
return !item.summary_fields?.user_capabilities?.delete;
}
@ -67,18 +76,29 @@ function DisassociateButton({
// See: https://github.com/patternfly/patternfly-react/issues/1894
return (
<>
<Tooltip content={renderTooltip()} position="top">
<div>
<Button
variant="secondary"
aria-label={i18n._(t`Disassociate`)}
onClick={() => setIsOpen(true)}
isDisabled={isDisabled}
>
{i18n._(t`Disassociate`)}
</Button>
</div>
</Tooltip>
{isKebabified ? (
<DropdownItem
key="add"
isDisabled={isDisabled}
component="button"
onClick={() => setIsOpen(true)}
>
{i18n._(t`Delete`)}
</DropdownItem>
) : (
<Tooltip content={renderTooltip()} position="top">
<div>
<Button
variant="secondary"
aria-label={i18n._(t`Disassociate`)}
onClick={() => setIsOpen(true)}
isDisabled={isDisabled}
>
{i18n._(t`Disassociate`)}
</Button>
</div>
</Tooltip>
)}
{isOpen && (
<AlertModal
@ -107,11 +127,7 @@ function DisassociateButton({
>
{modalNote && <ModalNote>{modalNote}</ModalNote>}
<div>
{i18n._(
t`This action will disassociate the following and any of their descendents:`
)}
</div>
<div>{i18n._(t`This action will disassociate the following:`)}</div>
{itemsToDisassociate.map(item => (
<span key={item.id}>

View File

@ -134,7 +134,7 @@ function InventoryGroup({ i18n, setBreadcrumb, inventory }) {
key="relatedGroups"
path="/inventories/inventory/:id/groups/:groupId/nested_groups"
>
<InventoryGroupsRelatedGroup inventoryGroup={inventoryGroup} />
<InventoryGroupsRelatedGroup />
</Route>,
]}
<Route key="not-found" path="*">

View File

@ -166,7 +166,26 @@ function InventoryGroupHostList({ i18n }) {
const canAdd =
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
const addFormUrl = `/inventories/inventory/${inventoryId}/groups/${groupId}/nested_hosts/add`;
const addButtonOptions = [];
if (canAdd) {
addButtonOptions.push(
{
onAdd: () => setIsModalOpen(true),
title: i18n._(t`Add existing host`),
label: i18n._(t`host`),
key: 'existing',
},
{
onAdd: () => history.push(addFormUrl),
title: i18n._(t`Add new host`),
label: i18n._(t`host`),
key: 'new',
}
);
}
// const addButton = <AddDropdown key="add" dropdownItems={addButtonOptions} />;
return (
<>
<PaginatedDataList

View File

@ -190,11 +190,11 @@ describe('<InventoryGroupHostList />', () => {
test('should show associate host modal when adding an existing host', () => {
const dropdownToggle = wrapper.find(
'DropdownToggle button[aria-label="add host"]'
'DropdownToggle button[aria-label="add"]'
);
dropdownToggle.simulate('click');
wrapper
.find('DropdownItem[aria-label="add existing host"]')
.find('DropdownItem[aria-label="Add existing host"]')
.simulate('click');
expect(wrapper.find('AssociateModal').length).toBe(1);
wrapper.find('ModalBoxCloseButton').simulate('click');
@ -209,12 +209,10 @@ describe('<InventoryGroupHostList />', () => {
results: [{ id: 123, name: 'foo', url: '/api/v2/hosts/123/' }],
},
});
wrapper
.find('DropdownToggle button[aria-label="add host"]')
.simulate('click');
wrapper.find('DropdownToggle button[aria-label="add"]').simulate('click');
await act(async () => {
wrapper
.find('DropdownItem[aria-label="add existing host"]')
.find('DropdownItem[aria-label="Add existing host"]')
.simulate('click');
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
@ -241,12 +239,10 @@ describe('<InventoryGroupHostList />', () => {
results: [{ id: 123, name: 'foo', url: '/api/v2/hosts/123/' }],
},
});
wrapper
.find('DropdownToggle button[aria-label="add host"]')
.simulate('click');
wrapper.find('DropdownToggle button[aria-label="add"]').simulate('click');
await act(async () => {
wrapper
.find('DropdownItem[aria-label="add existing host"]')
.find('DropdownItem[aria-label="Add existing host"]')
.simulate('click');
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
@ -288,10 +284,10 @@ describe('<InventoryGroupHostList />', () => {
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
const dropdownToggle = wrapper.find(
'DropdownToggle button[aria-label="add host"]'
'DropdownToggle button[aria-label="add"]'
);
dropdownToggle.simulate('click');
wrapper.find('DropdownItem[aria-label="add new host"]').simulate('click');
wrapper.find('DropdownItem[aria-label="Add new host"]').simulate('click');
expect(history.location.pathname).toEqual(
'/inventories/inventory/1/groups/2/nested_hosts/add'
);

View File

@ -28,7 +28,7 @@ const QS_CONFIG = getQSConfig('group', {
page_size: 20,
order_by: 'name',
});
function InventoryRelatedGroupList({ i18n, inventoryGroup }) {
function InventoryRelatedGroupList({ i18n }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const { id: inventoryId, groupId } = useParams();
const location = useLocation();
@ -92,6 +92,26 @@ function InventoryRelatedGroupList({ i18n, inventoryGroup }) {
);
const addFormUrl = `/home`;
const addButtonOptions = [];
if (canAdd) {
addButtonOptions.push(
{
onAdd: () => setIsModalOpen(true),
title: i18n._(t`Add existing group`),
label: i18n._(t`group`),
key: 'existing',
},
{
onAdd: () => history.push(addFormUrl),
title: i18n._(t`Add new group`),
label: i18n._(t`group`),
key: 'new',
}
);
}
const addButton = <AddDropdown key="add" dropdownItems={addButtonOptions} />;
return (
<>
@ -136,18 +156,7 @@ function InventoryRelatedGroupList({ i18n, inventoryGroup }) {
}
qsConfig={QS_CONFIG}
additionalControls={[
...(canAdd
? [
<AddDropdown
key="associate"
onAddExisting={() => setIsModalOpen(true)}
onAddNew={() => history.push(addFormUrl)}
newTitle={i18n._(t`Add new group`)}
existingTitle={i18n._(t`Add existing group`)}
label={i18n._(t`group`)}
/>,
]
: []),
...(canAdd ? [addButton] : []),
<Kebabified>
{({ isKebabified }) =>
isKebabified ? (

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { func, string } from 'prop-types';
import React, { useState, useRef, useEffect, Fragment } from 'react';
import { func, string, arrayOf, shape } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
@ -8,61 +8,81 @@ import {
DropdownPosition,
DropdownToggle,
} from '@patternfly/react-core';
import { useKebabifiedMenu } from '../../../contexts/Kebabified';
function AddDropdown({
i18n,
onAddNew,
onAddExisting,
newTitle,
existingTitle,
label,
}) {
function AddDropdown({ dropdownItems, i18n }) {
const { isKebabified } = useKebabifiedMenu();
const [isOpen, setIsOpen] = useState(false);
const element = useRef(null);
const dropdownItems = [
<DropdownItem
key="add-new"
aria-label={`add new ${label}`}
component="button"
onClick={onAddNew}
>
{newTitle}
</DropdownItem>,
<DropdownItem
key="add-existing"
aria-label={`add existing ${label}`}
component="button"
onClick={onAddExisting}
>
{existingTitle}
</DropdownItem>,
];
useEffect(() => {
const toggle = e => {
if (!isKebabified && (!element || !element.current.contains(e.target))) {
setIsOpen(false);
}
};
document.addEventListener('click', toggle, false);
return () => {
document.removeEventListener('click', toggle);
};
}, [isKebabified]);
if (isKebabified) {
return (
<Fragment>
{dropdownItems.map(item => (
<DropdownItem
key={item.key}
aria-label={item.title}
onClick={item.onAdd}
>
{item.title}
</DropdownItem>
))}
</Fragment>
);
}
return (
<Dropdown
isOpen={isOpen}
position={DropdownPosition.right}
toggle={
<DropdownToggle
id={`add-${label}-dropdown`}
aria-label={`add ${label}`}
isPrimary
onToggle={() => setIsOpen(prevState => !prevState)}
>
{i18n._(t`Add`)}
</DropdownToggle>
}
dropdownItems={dropdownItems}
/>
<div ref={element} key="add">
<Dropdown
isOpen={isOpen}
position={DropdownPosition.right}
toggle={
<DropdownToggle
id="add"
aria-label="add"
isPrimary
onToggle={() => setIsOpen(prevState => !prevState)}
>
{i18n._(t`Add`)}
</DropdownToggle>
}
dropdownItems={dropdownItems.map(item => (
<DropdownItem
className="pf-c-dropdown__menu-item"
key={item.key}
aria-label={item.title}
onClick={item.onAdd}
>
{item.title}
</DropdownItem>
))}
/>
</div>
);
}
AddDropdown.propTypes = {
onAddNew: func.isRequired,
onAddExisting: func.isRequired,
newTitle: string.isRequired,
existingTitle: string.isRequired,
label: string.isRequired,
dropdownItems: arrayOf(
shape({
label: string.isRequired,
onAdd: func.isRequired,
key: string.isRequired,
})
).isRequired,
};
export { AddDropdown as _AddDropdown };
export default withI18n()(AddDropdown);

View File

@ -5,13 +5,23 @@ import AddDropdown from './AddDropdown';
describe('<AddDropdown />', () => {
let wrapper;
let dropdownToggle;
const onAddNew = jest.fn();
const onAddExisting = jest.fn();
const dropdownItems = [
{
onAdd: () => {},
title: 'Add existing group',
label: 'group',
key: 'existing',
},
{
onAdd: () => {},
title: 'Add new group',
label: 'group',
key: 'new',
},
];
beforeEach(() => {
wrapper = mountWithContexts(
<AddDropdown onAddNew={onAddNew} onAddExisting={onAddExisting} />
);
wrapper = mountWithContexts(<AddDropdown dropdownItems={dropdownItems} />);
dropdownToggle = wrapper.find('DropdownToggle button');
});