Add alert for org. delete.

This commit is contained in:
Alex Corey
2019-03-25 11:30:40 -04:00
parent 1cb2a95a47
commit f3a07753e6
7 changed files with 209 additions and 60 deletions

View File

@@ -221,4 +221,32 @@ describe('<DataListToolbar />', () => {
const upAlphaIcon = toolbar.find(upAlphaIconSelector); const upAlphaIcon = toolbar.find(upAlphaIconSelector);
expect(upAlphaIcon.length).toBe(1); 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 deleteModal = 'button.pf-c-button.pf-m-plain.awx-ToolBarBtn';
const onSearch = jest.fn();
const onSort = jest.fn();
const onSelectAll = jest.fn();
toolbar = mount(
<I18nProvider>
<DataListToolbar
isAllSelected={false}
selected={() => ([1, 2, 3, 4])}
sortedColumnKey="name"
sortOrder="ascending"
columns={columns}
onSearch={onSearch}
onSort={onSort}
onSelectAll={onSelectAll}
onOpenDeleteModal={() => {}}
/>
</I18nProvider>
);
toolbar.find(deleteModal).simulate('click');
expect(onOpenDeleteModal).toBeCalled();
});
}); });

View File

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

View File

@@ -279,6 +279,10 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
.orgListAlert-actionBtn{
margin:0 10px;
}
.awx-c-form-action-group { .awx-c-form-action-group {
float: right; float: right;
display: block; display: block;

View File

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

View File

@@ -80,3 +80,16 @@
.awx-toolbar .pf-l-toolbar__item .pf-c-button.pf-m-plain { .awx-toolbar .pf-l-toolbar__item .pf-c-button.pf-m-plain {
font-size: 18px; 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

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

View File

@@ -12,9 +12,11 @@ import {
EmptyState, EmptyState,
EmptyStateIcon, EmptyStateIcon,
EmptyStateBody, EmptyStateBody,
Modal,
PageSection, PageSection,
PageSectionVariants, PageSectionVariants,
Title Title,
Button
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { CubesIcon } from '@patternfly/react-icons'; import { CubesIcon } from '@patternfly/react-icons';
import DataListToolbar from '../../../components/DataListToolbar'; import DataListToolbar from '../../../components/DataListToolbar';
@@ -54,6 +56,8 @@ class OrganizationsList extends Component {
loading: true, loading: true,
results: [], results: [],
selected: [], selected: [],
isModalOpen: false,
orgsToDelete: []
}; };
this.onSearch = this.onSearch.bind(this); this.onSearch = this.onSearch.bind(this);
@@ -64,6 +68,9 @@ class OrganizationsList extends Component {
this.onSelect = this.onSelect.bind(this); this.onSelect = this.onSelect.bind(this);
this.updateUrl = this.updateUrl.bind(this); this.updateUrl = this.updateUrl.bind(this);
this.fetchOrganizations = this.fetchOrganizations.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 () { componentDidMount () {
@@ -129,6 +136,50 @@ class OrganizationsList extends Component {
return Object.assign({}, this.defaultParams, searchParams, overrides); return Object.assign({}, this.defaultParams, searchParams, overrides);
} }
handleClearOrgsToDelete () {
this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen, orgsToDelete: [] }));
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 } = this.state;
const { api } = this.props;
try {
const deleteOrgsApiCalls = [];
orgsToDelete.forEach((org) => {
deleteOrgsApiCalls.push(api.destroyOrganization(org.id));
});
await Promise.all(deleteOrgsApiCalls);
} finally {
this.handleClearOrgsToDelete();
const queryParams = this.getQueryParams();
this.fetchOrganizations(queryParams);
}
event.preventDefault();
}
updateUrl (queryParams) { updateUrl (queryParams) {
const { history, location } = this.props; const { history, location } = this.props;
const pathname = '/organizations'; const pathname = '/organizations';
@@ -196,18 +247,47 @@ class OrganizationsList extends Component {
error, error,
loading, loading,
noInitialResults, noInitialResults,
orgsToDelete,
page, page,
pageCount, pageCount,
page_size, page_size,
selected,
sortedColumnKey, sortedColumnKey,
sortOrder, sortOrder,
results, results,
selected, isModalOpen,
warningTitle,
warningMsg,
} = this.state; } = this.state;
const { match } = this.props; const { match } = this.props;
return ( return (
<PageSection variant={medium}> <PageSection variant={medium}>
<Card> <Card>
{ isModalOpen && (
<Modal
className="orgListAlert"
width="50%"
title={warningTitle}
isOpen={isModalOpen}
style={{ width: '1000px' }}
variant="danger"
onClose={this.handleClearOrgsToDelete}
>
{warningMsg}
<br />
{orgsToDelete.map((org) => (
<strong key={org.id}>
{org.name}
<br />
</strong>
))}
<br />
<span className="awx-c-form-action-group">
<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>
</Modal>
)}
{noInitialResults && ( {noInitialResults && (
<EmptyState> <EmptyState>
<EmptyStateIcon icon={CubesIcon} /> <EmptyStateIcon icon={CubesIcon} />
@@ -229,6 +309,8 @@ class OrganizationsList extends Component {
onSearch={this.onSearch} onSearch={this.onSearch}
onSort={this.onSort} onSort={this.onSort}
onSelectAll={this.onSelectAll} onSelectAll={this.onSelectAll}
onOpenDeleteModal={this.handleOpenOrgDeleteModal}
disableTrashCanIcon={selected.length === 0}
showDelete showDelete
showSelectAll showSelectAll
/> />
@@ -244,7 +326,8 @@ class OrganizationsList extends Component {
userCount={o.summary_fields.related_field_counts.users} userCount={o.summary_fields.related_field_counts.users}
teamCount={o.summary_fields.related_field_counts.teams} teamCount={o.summary_fields.related_field_counts.teams}
isSelected={selected.includes(o.id)} isSelected={selected.includes(o.id)}
onSelect={() => this.onSelect(o.id)} onSelect={() => this.onSelect(o.id, o.name)}
onOpenOrgDeleteModal={this.handleOpenOrgDeleteModal}
/> />
))} ))}
</ul> </ul>