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
This commit is contained in:
Marliana Lara 2019-12-05 11:34:18 -05:00
parent 5d1f322cd1
commit 4c89568d71
No known key found for this signature in database
GPG Key ID: 38C73B40DFA809EE
4 changed files with 86 additions and 140 deletions

View File

@ -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 =

View File

@ -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('<InventoryGroups />', () => {
},
},
});
const history = createMemoryHistory({
initialEntries: ['/inventories/inventory/3/groups'],
});
await act(async () => {
wrapper = mountWithContexts(
<MemoryRouter initialEntries={['/inventories/inventory/3/groups']}>
<Route
path="/inventories/inventory/:id/groups"
component={() => <InventoryGroups />}
/>
</MemoryRouter>
<Route
path="/inventories/inventory/:id/groups"
component={() => <InventoryGroups />}
/>,
{
context: {
router: { history, route: { location: history.location } },
},
}
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
@ -194,6 +200,10 @@ describe('<InventoryGroups />', () => {
'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"]')

View File

@ -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 (
<ListItem key={group.id}>
<dl>
<dt>{group.name}</dt>
<dd>
{i18n._(
t`(${group.total_groups} Groups and ${group.total_hosts} Hosts)`
)}
</dd>
</dl>
<Radio
key="radio-delete"
label={i18n._(t`Delete All Groups and Hosts`)}
id={`radio-delete-${group.id}`}
name={`radio-${group.id}`}
onChange={() => handleChange(group.id, 'delete')}
/>
<Radio
key="radio-promote"
label={i18n._(t`Promote Child Groups and Hosts`)}
id={`radio-promote-${group.id}`}
name={`radio-${group.id}`}
onChange={() => handleChange(group.id, 'promote')}
/>
</ListItem>
);
}
return (
<ListItem key={group.id}>
<dl>
<dt>{group.name}</dt>
<dd>{i18n._(t`(No Child Groups or Hosts)`)}</dd>
</dl>
</ListItem>
);
})
.reduce((array, el) => {
return array.concat(el);
}, []);
const [radioOption, setRadioOption] = useState(null);
return ReactDOM.createPortal(
<AlertModal
@ -111,12 +33,10 @@ const InventoryGroupsDeleteModal = ({
actions={[
<Button
aria-label={i18n._(t`Delete`)}
onClick={() => onDelete(deleteList)}
onClick={() => onDelete(radioOption)}
variant="danger"
key="delete"
isDisabled={Object.keys(deleteList).some(
group => deleteList[group] === null
)}
isDisabled={radioOption === null}
>
{i18n._(t`Delete`)}
</Button>,
@ -135,10 +55,43 @@ const InventoryGroupsDeleteModal = ({
groups.length > 1 ? i18n._(t`groups`) : i18n._(t`group`)
} below?`
)}
<ContentWrapper>{content}</ContentWrapper>
<div css="padding: 24px 0;">
{groups.map(group => {
return <ListItem key={group.id}>{group.name}</ListItem>;
})}
</div>
<div css="padding-left: 1px;">
<Radio
id="radio-delete"
key="radio-delete"
label={i18n._(t`Delete All Groups and Hosts`)}
name="option"
onChange={() => setRadioOption('delete')}
/>
<Radio
css="margin-top: 5px;"
id="radio-promote"
key="radio-promote"
label={i18n._(t`Promote Child Groups and Hosts`)}
name="option"
onChange={() => setRadioOption('promote')}
/>
</div>
</AlertModal>,
document.body
);
};
InventoryGroupsDeleteModal.propTypes = {
onClose: func.isRequired,
onDelete: func.isRequired,
isModalOpen: bool,
groups: arrayOf(object),
};
InventoryGroupsDeleteModal.defaultProps = {
isModalOpen: false,
groups: [],
};
export default withI18n()(InventoryGroupsDeleteModal);

View File

@ -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,
});