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

View File

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

View File

@@ -166,7 +166,26 @@ function InventoryGroupHostList({ i18n }) {
const canAdd = const canAdd =
actions && Object.prototype.hasOwnProperty.call(actions, 'POST'); actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
const addFormUrl = `/inventories/inventory/${inventoryId}/groups/${groupId}/nested_hosts/add`; 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 ( return (
<> <>
<PaginatedDataList <PaginatedDataList

View File

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

View File

@@ -28,7 +28,7 @@ const QS_CONFIG = getQSConfig('group', {
page_size: 20, page_size: 20,
order_by: 'name', order_by: 'name',
}); });
function InventoryRelatedGroupList({ i18n, inventoryGroup }) { function InventoryRelatedGroupList({ i18n }) {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { id: inventoryId, groupId } = useParams(); const { id: inventoryId, groupId } = useParams();
const location = useLocation(); const location = useLocation();
@@ -92,6 +92,26 @@ function InventoryRelatedGroupList({ i18n, inventoryGroup }) {
); );
const addFormUrl = `/home`; 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 ( return (
<> <>
@@ -136,18 +156,7 @@ function InventoryRelatedGroupList({ i18n, inventoryGroup }) {
} }
qsConfig={QS_CONFIG} qsConfig={QS_CONFIG}
additionalControls={[ additionalControls={[
...(canAdd ...(canAdd ? [addButton] : []),
? [
<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`)}
/>,
]
: []),
<Kebabified> <Kebabified>
{({ isKebabified }) => {({ isKebabified }) =>
isKebabified ? ( isKebabified ? (

View File

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

View File

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