diff --git a/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx b/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx index cb6a5cc518..709549eb9e 100644 --- a/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx +++ b/awx/ui_next/src/components/DisassociateButton/DisassociateButton.jsx @@ -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 ( <> - -
- -
-
+ {isKebabified ? ( + setIsOpen(true)} + > + {i18n._(t`Delete`)} + + ) : ( + +
+ +
+
+ )} {isOpen && ( {modalNote && {modalNote}} -
- {i18n._( - t`This action will disassociate the following and any of their descendents:` - )} -
+
{i18n._(t`This action will disassociate the following:`)}
{itemsToDisassociate.map(item => ( diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx index f9fcdb9fa7..4b53938373 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroup/InventoryGroup.jsx @@ -134,7 +134,7 @@ function InventoryGroup({ i18n, setBreadcrumb, inventory }) { key="relatedGroups" path="/inventories/inventory/:id/groups/:groupId/nested_groups" > - + , ]} diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx index 228a1fb9cf..15b5db3475 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroupHosts/InventoryGroupHostList.jsx @@ -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 = ; return ( <> ', () => { 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('', () => { 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('', () => { 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('', () => { }); 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' ); diff --git a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx index 0ea82d41c2..9ce63a3108 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryRelatedGroups/InventoryRelatedGroupList.jsx @@ -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 = ; return ( <> @@ -136,18 +156,7 @@ function InventoryRelatedGroupList({ i18n, inventoryGroup }) { } qsConfig={QS_CONFIG} additionalControls={[ - ...(canAdd - ? [ - setIsModalOpen(true)} - onAddNew={() => history.push(addFormUrl)} - newTitle={i18n._(t`Add new group`)} - existingTitle={i18n._(t`Add existing group`)} - label={i18n._(t`group`)} - />, - ] - : []), + ...(canAdd ? [addButton] : []), {({ isKebabified }) => isKebabified ? ( diff --git a/awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx b/awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx index 86a2704954..f85c619e3d 100644 --- a/awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/AddDropdown.jsx @@ -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 = [ - - {newTitle} - , - - {existingTitle} - , - ]; + 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 ( + + {dropdownItems.map(item => ( + + {item.title} + + ))} + + ); + } return ( - setIsOpen(prevState => !prevState)} - > - {i18n._(t`Add`)} - - } - dropdownItems={dropdownItems} - /> +
+ setIsOpen(prevState => !prevState)} + > + {i18n._(t`Add`)} + + } + dropdownItems={dropdownItems.map(item => ( + + {item.title} + + ))} + /> +
); } 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); diff --git a/awx/ui_next/src/screens/Inventory/shared/AddDropdown.test.jsx b/awx/ui_next/src/screens/Inventory/shared/AddDropdown.test.jsx index 68a8ab7ef9..39b291bc8f 100644 --- a/awx/ui_next/src/screens/Inventory/shared/AddDropdown.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/AddDropdown.test.jsx @@ -5,13 +5,23 @@ import AddDropdown from './AddDropdown'; describe('', () => { 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( - - ); + wrapper = mountWithContexts(); dropdownToggle = wrapper.find('DropdownToggle button'); });