Merge pull request #141 from AlexSCorey/48-deleteOrgs

Add alert for org. delete.
This commit is contained in:
Alex Corey 2019-04-05 12:39:38 -04:00 committed by GitHub
commit 09fd8e8106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 361 additions and 60 deletions

View File

@ -221,4 +221,36 @@ describe('<DataListToolbar />', () => {
const upAlphaIcon = toolbar.find(upAlphaIconSelector);
expect(upAlphaIcon.length).toBe(1);
});
test('trash can button triggers correct function', () => {
const columns = [{ name: 'Name', key: 'name', isSortable: true }];
const onOpenDeleteModal = jest.fn();
const openDeleteModalButton = 'button[aria-label="Delete"]';
const onSearch = jest.fn();
const onSort = jest.fn();
const onSelectAll = jest.fn();
const showDelete = true;
const disableTrashCanIcon = false;
toolbar = mount(
<I18nProvider>
<DataListToolbar
isAllSelected={false}
selected={() => [1, 2, 3, 4]}
sortedColumnKey="name"
sortOrder="ascending"
columns={columns}
onSearch={onSearch}
onSort={onSort}
onSelectAll={onSelectAll}
onOpenDeleteModal={onOpenDeleteModal}
showDelete={showDelete}
disableTrashCanIcon={disableTrashCanIcon}
/>
</I18nProvider>
);
toolbar.find(openDeleteModalButton).simulate('click');
expect(onOpenDeleteModal).toBeCalled();
});
});

View File

@ -4,6 +4,45 @@ import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import OrganizationsList from '../../../../src/pages/Organizations/screens/OrganizationsList';
const mockAPIOrgsList = {
data: {
results: [{
name: 'Organization 0',
id: 1,
summary_fields: {
related_field_counts: {
teams: 3,
users: 4
}
},
},
{
name: 'Organization 1',
id: 1,
summary_fields: {
related_field_counts: {
teams: 2,
users: 5
}
},
},
{
name: 'Organization 2',
id: 2,
summary_fields: {
related_field_counts: {
teams: 5,
users: 6
}
},
}]
},
isModalOpen: false,
warningTitle: 'title',
warningMsg: 'message'
};
describe('<OrganizationsList />', () => {
test('initially renders succesfully', () => {
mount(
@ -17,4 +56,79 @@ describe('<OrganizationsList />', () => {
</MemoryRouter>
);
});
test.only('Modal closes when close button is clicked.', async (done) => {
const handleClearOrgsToDelete = jest.fn();
const wrapper = mount(
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<I18nProvider>
<OrganizationsList
match={{ path: '/organizations', url: '/organizations' }}
location={{ search: '', pathname: '/organizations' }}
getItems={({ data: { orgsToDelete: [{ name: 'Organization 1', id: 1 }] } })}
handleClearOrgsToDelete={handleClearOrgsToDelete()}
/>
</I18nProvider>
</MemoryRouter>
);
wrapper.find({ type: 'checkbox' }).simulate('click');
wrapper.find('DataListToolbar').prop('onOpenDeleteModal')();
expect(wrapper.find('OrganizationsList').state().isModalOpen).toEqual(true);
setImmediate(() => {
wrapper.update();
wrapper.setState({
selected: mockAPIOrgsList.data.results.map((result) => result.id),
orgsToDelete: mockAPIOrgsList.data.results.map((result) => result),
isModalOpen: true,
});
wrapper.find('button[aria-label="Close"]').simulate('click');
expect(handleClearOrgsToDelete).toBeCalled();
const list = wrapper.find('OrganizationsList');
expect(list.state().isModalOpen).toBe(false);
done();
});
});
test.only('Orgs to delete length is 0 when all orgs are selected and Delete button is called.', async (done) => {
const handleClearOrgsToDelete = jest.fn();
const handleOrgDelete = jest.fn();
const fetchOrganizations = jest.fn();
const wrapper = mount(
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<I18nProvider>
<OrganizationsList
match={{ path: '/organizations', url: '/organizations' }}
location={{ search: '', pathname: '/organizations' }}
getItems={({ data: { orgsToDelete: [{ name: 'Organization 1', id: 1 }] } })}
handleClearOrgsToDelete={handleClearOrgsToDelete()}
handleOrgDelete={handleOrgDelete()}
fetchOrganizations={fetchOrganizations()}
/>
</I18nProvider>
</MemoryRouter>
);
wrapper.find({ type: 'checkbox' }).simulate('click');
wrapper.find('button[aria-label="Delete"]').simulate('click');
wrapper.find('DataListToolbar').prop('onOpenDeleteModal')();
expect(wrapper.find('OrganizationsList').state().isModalOpen).toEqual(true);
setImmediate(() => {
wrapper.update();
wrapper.setState({
selected: mockAPIOrgsList.data.results.map((result) => result.id),
orgsToDelete: mockAPIOrgsList.data.results.map((result) => result),
isModalOpen: true,
});
wrapper.update();
const list = wrapper.find('OrganizationsList');
wrapper.find('button[aria-label="confirm-delete"]').simulate('click');
expect(list.state().orgsToDelete.length).toEqual(list.state().orgsDeleted.length);
expect(fetchOrganizations).toHaveBeenCalled();
done();
});
});
});

View File

@ -56,6 +56,11 @@ class APIClient {
return this.http.get(API_CONFIG);
}
destroyOrganization (id) {
const endpoint = `${API_ORGANIZATIONS}${id}/`;
return (this.http.delete(endpoint));
}
getOrganizations (params = {}) {
return this.http.get(API_ORGANIZATIONS, { params });
}

View File

@ -279,6 +279,17 @@
margin-bottom: 10px;
}
.orgListAlert-actionBtn{
margin:0 10px;
}
.orgListDetete-progressBar{
padding-right: 32px;
}
.orgListDelete-progressBar-noShow{
display: none;
padding-right: 32px;
}
.awx-c-form-action-group {
float: right;
display: block;
@ -287,4 +298,4 @@
margin-top: 20px;
margin-right: 20px;
}
}
}

View File

@ -28,25 +28,26 @@ import VerticalSeparator from '../VerticalSeparator';
class DataListToolbar extends React.Component {
render () {
const {
addUrl,
columns,
isAllSelected,
disableTrashCanIcon,
onSelectAll,
sortedColumnKey,
sortOrder,
addUrl,
showDelete,
showSelectAll,
isAllSelected,
isLookup,
isCompact,
onSort,
onSearch,
onCompact,
onExpand,
add
add,
onOpenDeleteModal
} = this.props;
const showExpandCollapse = (onCompact && onExpand);
return (
<I18n>
{({ i18n }) => (
@ -115,10 +116,13 @@ class DataListToolbar extends React.Component {
position="top"
>
<Button
className="awx-ToolBarBtn"
variant="plain"
aria-label={i18n._(t`Delete`)}
onClick={onOpenDeleteModal}
isDisabled={disableTrashCanIcon}
>
<TrashAltIcon />
<TrashAltIcon className="awx-ToolBarTrashCanIcon" />
</Button>
</Tooltip>
)}

View File

@ -80,3 +80,16 @@
.awx-toolbar .pf-l-toolbar__item .pf-c-button.pf-m-plain {
font-size: 18px;
}
.pf-c-button--disabled--BackgroundColor{
background-color: #b7b7b7;
}
.awx-ToolBarBtn{
width: 30px;
}
.awx-ToolBarBtn:hover{
.awx-ToolBarTrashCanIcon {
color:white;
}
background-color:#d9534f;
}

View File

@ -11,54 +11,61 @@ import {
import VerticalSeparator from '../../../components/VerticalSeparator';
export default ({
itemId,
name,
userCount,
teamCount,
isSelected,
onSelect,
detailUrl,
}) => (
<li key={itemId} className="pf-c-data-list__item" aria-labelledby="check-action-item1">
<I18n>
{({ i18n }) => (
<Checkbox
checked={isSelected}
onChange={onSelect}
aria-label={i18n._(t`select organization ${itemId}`)}
id={`select-organization-${itemId}`}
/>
)}
</I18n>
<VerticalSeparator />
<div className="pf-c-data-list__cell">
<span id="check-action-item1">
<Link
to={`${detailUrl}`}
>
<b>{name}</b>
</Link>
</span>
</div>
<div className="pf-c-data-list__cell">
<Link to={`${detailUrl}/access`}>
<Trans>Users</Trans>
</Link>
<Badge isRead>
{' '}
{userCount}
{' '}
</Badge>
<Link to={`${detailUrl}/teams`}>
<Trans>Teams</Trans>
</Link>
<Badge isRead>
{' '}
{teamCount}
{' '}
</Badge>
</div>
<div className="pf-c-data-list__cell" />
</li>
);
class OrganizationListItem extends React.Component {
render () {
const {
itemId,
name,
userCount,
teamCount,
isSelected,
onSelect,
detailUrl,
} = this.props;
return (
<li key={itemId} className="pf-c-data-list__item" aria-labelledby="check-action-item1">
<I18n>
{({ i18n }) => (
<Checkbox
checked={isSelected}
onChange={onSelect}
aria-label={i18n._(t`select organization ${itemId}`)}
id={`select-organization-${itemId}`}
/>
)}
</I18n>
<VerticalSeparator />
<div className="pf-c-data-list__cell">
<span id="check-action-item1">
<Link
to={`${detailUrl}`}
>
<b>{name}</b>
</Link>
</span>
</div>
<div className="pf-c-data-list__cell">
<Link to={`${detailUrl}/access`}>
<Trans>Users</Trans>
</Link>
<Badge isRead>
{' '}
{userCount}
{' '}
</Badge>
<Link to={`${detailUrl}/teams`}>
<Trans>Teams</Trans>
</Link>
<Badge isRead>
{' '}
{teamCount}
{' '}
</Badge>
</div>
<div className="pf-c-data-list__cell" />
</li>
);
}
}
export default OrganizationListItem;

View File

@ -12,10 +12,15 @@ import {
EmptyState,
EmptyStateIcon,
EmptyStateBody,
Modal,
PageSection,
PageSectionVariants,
Title
Title,
Button,
Progress,
ProgressVariant
} from '@patternfly/react-core';
import { CubesIcon } from '@patternfly/react-icons';
import DataListToolbar from '../../../components/DataListToolbar';
import OrganizationListItem from '../components/OrganizationListItem';
@ -54,6 +59,11 @@ class OrganizationsList extends Component {
loading: true,
results: [],
selected: [],
isModalOpen: false,
orgsToDelete: [],
orgsDeleted: [],
deleteSuccess: false,
};
this.onSearch = this.onSearch.bind(this);
@ -64,6 +74,9 @@ class OrganizationsList extends Component {
this.onSelect = this.onSelect.bind(this);
this.updateUrl = this.updateUrl.bind(this);
this.fetchOrganizations = this.fetchOrganizations.bind(this);
this.handleOrgDelete = this.handleOrgDelete.bind(this);
this.handleOpenOrgDeleteModal = this.handleOpenOrgDeleteModal.bind(this);
this.handleClearOrgsToDelete = this.handleClearOrgsToDelete.bind(this);
}
componentDidMount () {
@ -129,6 +142,60 @@ class OrganizationsList extends Component {
return Object.assign({}, this.defaultParams, searchParams, overrides);
}
handleClearOrgsToDelete () {
this.setState({
isModalOpen: false,
orgsDeleted: [],
deleteSuccess: false,
orgsToDelete: [],
deleteStarted: false
});
this.onSelectAll();
}
handleOpenOrgDeleteModal () {
const { results, selected } = this.state;
const warningTitle = i18nMark('Delete Organization');
const warningMsg = i18nMark('Are you sure you want to delete:');
const orgsToDelete = [];
results.forEach((result) => {
selected.forEach((selectedOrg) => {
if (result.id === selectedOrg) {
orgsToDelete.push({ name: result.name, id: selectedOrg });
}
});
});
this.setState({
orgsToDelete,
isModalOpen: true,
warningTitle,
warningMsg,
loading: false });
}
async handleOrgDelete (event) {
const { orgsToDelete, orgsDeleted } = this.state;
const { api } = this.props;
this.setState({ deleteStarted: true });
orgsToDelete.forEach(async (org) => {
try {
const res = await api.destroyOrganization(org.id);
this.setState({
orgsDeleted: orgsDeleted.concat(res)
});
} catch {
this.setState({ deleteSuccess: false });
} finally {
this.setState({ deleteSuccess: true });
const queryParams = this.getQueryParams();
this.fetchOrganizations(queryParams);
}
});
event.preventDefault();
}
updateUrl (queryParams) {
const { history, location } = this.props;
const pathname = '/organizations';
@ -194,20 +261,65 @@ class OrganizationsList extends Component {
const {
count,
error,
deleteSuccess,
deleteStarted,
loading,
noInitialResults,
orgsToDelete,
orgsDeleted,
page,
pageCount,
page_size,
selected,
sortedColumnKey,
sortOrder,
results,
selected,
isModalOpen,
warningTitle,
warningMsg,
} = this.state;
const { match } = this.props;
return (
<PageSection variant={medium}>
<Card>
{ isModalOpen && (
<Modal
className="orgListAlert"
title={warningTitle}
isOpen={isModalOpen}
style={{ width: '1000px' }}
variant="danger"
onClose={this.handleClearOrgsToDelete}
>
{warningMsg}
<br />
{orgsToDelete.map((org) => (
<span key={org.id}>
<strong>
{org.name}
</strong>
<br />
</span>
))}
<div className={deleteStarted ? 'orgListDetete-progressBar' : 'orgListDelete-progressBar-noShow'}>
<Progress
value={deleteSuccess ? 100 : 67}
variant={deleteStarted ? ProgressVariant.success : ProgressVariant.danger}
/>
</div>
<br />
<div className="awx-c-form-action-group">
{orgsDeleted.length
? <Button className="orgListAlert-actionBtn" keys="cancel" variant="primary" aria-label="close-delete" onClick={this.handleClearOrgsToDelete}>Close</Button>
: (
<span>
<Button className="orgListAlert-actionBtn" keys="cancel" variant="secondary" aria-label="cancel-delete" onClick={this.handleClearOrgsToDelete}>Cancel</Button>
<Button className="orgListAlert-actionBtn" keys="cancel" variant="danger" aria-label="confirm-delete" onClick={this.handleOrgDelete}>Delete</Button>
</span>
)}
</div>
</Modal>
)}
{noInitialResults && (
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
@ -229,6 +341,8 @@ class OrganizationsList extends Component {
onSearch={this.onSearch}
onSort={this.onSort}
onSelectAll={this.onSelectAll}
onOpenDeleteModal={this.handleOpenOrgDeleteModal}
disableTrashCanIcon={selected.length === 0}
showDelete
showSelectAll
/>
@ -244,7 +358,8 @@ class OrganizationsList extends Component {
userCount={o.summary_fields.related_field_counts.users}
teamCount={o.summary_fields.related_field_counts.teams}
isSelected={selected.includes(o.id)}
onSelect={() => this.onSelect(o.id)}
onSelect={() => this.onSelect(o.id, o.name)}
onOpenOrgDeleteModal={this.handleOpenOrgDeleteModal}
/>
))}
</ul>