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');
});