From 4c89568d7186276f2ffef6dd59c56346cae057f4 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Thu, 5 Dec 2019 11:34:18 -0500 Subject: [PATCH] Apply radio selection to ALL selected groups in modal * Use semantic html to describe modal list * Move nested try/catch block * Remove deprecated type fields * If delete fails, keep selected list checked --- .../InventoryGroups/InventoryGroups.jsx | 59 ++++---- .../InventoryGroups/InventoryGroups.test.jsx | 26 ++-- .../shared/InventoryGroupsDeleteModal.jsx | 133 ++++++------------ awx/ui_next/src/types.js | 8 -- 4 files changed, 86 insertions(+), 140 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx index 53f1f75774..49f55cd4d8 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.jsx @@ -131,46 +131,37 @@ function InventoryGroups({ i18n, location, match }) { return i18n._(t`Select a row to delete`); }; - const promoteGroups = list => { - const promotePromises = Object.keys(list) - .filter(groupId => list[groupId] === 'promote') - .map(groupId => InventoriesAPI.promoteGroup(inventoryId, +groupId)); - - return Promise.all(promotePromises); - }; - - const deleteGroups = list => { - const deletePromises = Object.keys(list) - .filter(groupId => list[groupId] === 'delete') - .map(groupId => GroupsAPI.destroy(+groupId)); - - return Promise.all(deletePromises); - }; - - const handleDelete = async list => { + const handleDelete = async option => { setIsLoading(true); try { - await Promise.all([promoteGroups(list), deleteGroups(list)]); + /* eslint-disable no-await-in-loop, no-restricted-syntax */ + /* Delete groups sequentially to avoid api integrity errors */ + for (const group of selected) { + if (option === 'delete') { + await GroupsAPI.destroy(+group.id); + } else if (option === 'promote') { + await InventoriesAPI.promoteGroup(inventoryId, +group.id); + } + } + /* eslint-enable no-await-in-loop, no-restricted-syntax */ } catch (error) { setDeletionError(error); - } finally { - toggleModal(); - setSelected([]); - - try { - const { - data: { count, results }, - } = await fetchGroups(inventoryId, location.search); - - setGroups(results); - setGroupCount(count); - } catch (error) { - setContentError(error); - } finally { - setIsLoading(false); - } } + + toggleModal(); + + try { + const { + data: { count, results }, + } = await fetchGroups(inventoryId, location.search); + setGroups(results); + setGroupCount(count); + } catch (error) { + setContentError(error); + } + + setIsLoading(false); }; const canAdd = diff --git a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx index abf12234fe..2b5a7340c0 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryGroups/InventoryGroups.test.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; -import { MemoryRouter, Route } from 'react-router-dom'; +import { Route } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { InventoriesAPI, GroupsAPI } from '@api'; import InventoryGroups from './InventoryGroups'; @@ -67,15 +68,20 @@ describe('', () => { }, }, }); - + const history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/3/groups'], + }); await act(async () => { wrapper = mountWithContexts( - - } - /> - + } + />, + { + context: { + router: { history, route: { location: history.location } }, + }, + } ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); @@ -194,6 +200,10 @@ describe('', () => { 'InventoryGroupsDeleteModal', el => el.props().isModalOpen === true ); + await act(async () => { + wrapper.find('Radio[id="radio-delete"]').invoke('onChange')(); + }); + wrapper.update(); await act(async () => { wrapper .find('ModalBoxFooter Button[aria-label="Delete"]') diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryGroupsDeleteModal.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupsDeleteModal.jsx index d0ac798872..ca86d722b5 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventoryGroupsDeleteModal.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupsDeleteModal.jsx @@ -1,38 +1,18 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import ReactDOM from 'react-dom'; +import { func, bool, arrayOf, object } from 'prop-types'; import AlertModal from '@components/AlertModal'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Button, Radio } from '@patternfly/react-core'; import styled from 'styled-components'; -const ListItem = styled.div` - padding: 24px 1px; - - dl { - display: flex; - font-weight: 600; - } - dt { - color: var(--pf-global--danger-color--100); - margin-right: 10px; - } - .pf-c-radio { - margin-top: 10px; - } +const ListItem = styled.li` + display: flex; + font-weight: 600; + color: var(--pf-global--danger-color--100); `; -const ContentWrapper = styled.div` - ${ListItem} + ${ListItem} { - border-top-width: 1px; - border-top-style: solid; - border-top-color: #d7d7d7; - } - ${ListItem}:last-child { - padding-bottom: 0; - } - `; - const InventoryGroupsDeleteModal = ({ onClose, onDelete, @@ -40,65 +20,7 @@ const InventoryGroupsDeleteModal = ({ groups, i18n, }) => { - const [deleteList, setDeleteList] = useState([]); - - useEffect(() => { - const groupIds = groups.reduce((obj, group) => { - if (group.total_groups > 0 || group.total_hosts > 0) { - return { ...obj, [group.id]: null }; - } - return { ...obj, [group.id]: 'delete' }; - }, {}); - - setDeleteList(groupIds); - }, [groups]); - - const handleChange = (groupId, radioOption) => { - setDeleteList({ ...deleteList, [groupId]: radioOption }); - }; - - const content = groups - .map(group => { - if (group.total_groups > 0 || group.total_hosts > 0) { - return ( - -
-
{group.name}
-
- {i18n._( - t`(${group.total_groups} Groups and ${group.total_hosts} Hosts)` - )} -
-
- handleChange(group.id, 'delete')} - /> - handleChange(group.id, 'promote')} - /> -
- ); - } - return ( - -
-
{group.name}
-
{i18n._(t`(No Child Groups or Hosts)`)}
-
-
- ); - }) - .reduce((array, el) => { - return array.concat(el); - }, []); + const [radioOption, setRadioOption] = useState(null); return ReactDOM.createPortal( onDelete(deleteList)} + onClick={() => onDelete(radioOption)} variant="danger" key="delete" - isDisabled={Object.keys(deleteList).some( - group => deleteList[group] === null - )} + isDisabled={radioOption === null} > {i18n._(t`Delete`)} , @@ -135,10 +55,43 @@ const InventoryGroupsDeleteModal = ({ groups.length > 1 ? i18n._(t`groups`) : i18n._(t`group`) } below?` )} - {content} +
+ {groups.map(group => { + return {group.name}; + })} +
+
+ setRadioOption('delete')} + /> + setRadioOption('promote')} + /> +
, document.body ); }; +InventoryGroupsDeleteModal.propTypes = { + onClose: func.isRequired, + onDelete: func.isRequired, + isModalOpen: bool, + groups: arrayOf(object), +}; + +InventoryGroupsDeleteModal.defaultProps = { + isModalOpen: false, + groups: [], +}; + export default withI18n()(InventoryGroupsDeleteModal); diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index 4713e0dcfd..5fee265305 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -199,8 +199,6 @@ export const Host = shape({ enabled: bool, instance_id: string, variables: string, - has_active_failures: bool, - has_inventory_sources: bool, last_job: number, last_job_host_summary: number, }); @@ -242,10 +240,4 @@ export const Group = shape({ description: string, inventory: number, variables: string, - has_active_failures: bool, - total_hosts: number, - hosts_with_active_failures: number, - total_groups: number, - groups_with_active_failures: number, - has_inventory_sources: bool, });