diff --git a/__tests__/components/DataListToolbar.test.jsx b/__tests__/components/DataListToolbar.test.jsx index 8f966ec739..50e2a6f61b 100644 --- a/__tests__/components/DataListToolbar.test.jsx +++ b/__tests__/components/DataListToolbar.test.jsx @@ -221,4 +221,36 @@ describe('', () => { 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( + + [1, 2, 3, 4]} + sortedColumnKey="name" + sortOrder="ascending" + columns={columns} + onSearch={onSearch} + onSort={onSort} + onSelectAll={onSelectAll} + onOpenDeleteModal={onOpenDeleteModal} + showDelete={showDelete} + disableTrashCanIcon={disableTrashCanIcon} + /> + + ); + + toolbar.find(openDeleteModalButton).simulate('click'); + expect(onOpenDeleteModal).toBeCalled(); + }); }); diff --git a/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx b/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx index 6b7f49646d..e874199da5 100644 --- a/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx +++ b/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx @@ -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('', () => { test('initially renders succesfully', () => { mount( @@ -17,4 +56,79 @@ describe('', () => { ); }); + + test.only('Modal closes when close button is clicked.', async (done) => { + const handleClearOrgsToDelete = jest.fn(); + const wrapper = mount( + + + + + + ); + 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( + + + + + + ); + 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(); + }); + }); }); diff --git a/src/api.js b/src/api.js index ba52325150..fcf8403fd4 100644 --- a/src/api.js +++ b/src/api.js @@ -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 }); } diff --git a/src/app.scss b/src/app.scss index 1b4a0e45b3..b8606fd856 100644 --- a/src/app.scss +++ b/src/app.scss @@ -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; } -} \ No newline at end of file +} diff --git a/src/components/DataListToolbar/DataListToolbar.jsx b/src/components/DataListToolbar/DataListToolbar.jsx index 00f3381458..d77ab68ae3 100644 --- a/src/components/DataListToolbar/DataListToolbar.jsx +++ b/src/components/DataListToolbar/DataListToolbar.jsx @@ -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 }) => ( @@ -115,10 +116,13 @@ class DataListToolbar extends React.Component { position="top" > )} diff --git a/src/components/DataListToolbar/styles.scss b/src/components/DataListToolbar/styles.scss index e8d989da3c..c91bdb727c 100644 --- a/src/components/DataListToolbar/styles.scss +++ b/src/components/DataListToolbar/styles.scss @@ -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; +} + diff --git a/src/pages/Organizations/components/OrganizationListItem.jsx b/src/pages/Organizations/components/OrganizationListItem.jsx index 19adbd98df..4a8309ba11 100644 --- a/src/pages/Organizations/components/OrganizationListItem.jsx +++ b/src/pages/Organizations/components/OrganizationListItem.jsx @@ -11,54 +11,61 @@ import { import VerticalSeparator from '../../../components/VerticalSeparator'; -export default ({ - itemId, - name, - userCount, - teamCount, - isSelected, - onSelect, - detailUrl, -}) => ( -
  • - - {({ i18n }) => ( - - )} - - -
    - - - {name} - - -
    -
    - - Users - - - {' '} - {userCount} - {' '} - - - Teams - - - {' '} - {teamCount} - {' '} - -
    -
    -
  • -); +class OrganizationListItem extends React.Component { + render () { + const { + itemId, + name, + userCount, + teamCount, + isSelected, + onSelect, + detailUrl, + } = this.props; + return ( +
  • + + {({ i18n }) => ( + + )} + + +
    + + + {name} + + +
    +
    + + Users + + + {' '} + {userCount} + {' '} + + + Teams + + + {' '} + {teamCount} + {' '} + +
    +
    +
  • + ); + } +} +export default OrganizationListItem; + diff --git a/src/pages/Organizations/screens/OrganizationsList.jsx b/src/pages/Organizations/screens/OrganizationsList.jsx index 19c553aa67..f40d572cc2 100644 --- a/src/pages/Organizations/screens/OrganizationsList.jsx +++ b/src/pages/Organizations/screens/OrganizationsList.jsx @@ -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 ( + { isModalOpen && ( + + {warningMsg} +
    + {orgsToDelete.map((org) => ( + + + {org.name} + +
    +
    + ))} +
    + +
    +
    +
    + {orgsDeleted.length + ? + : ( + + + + + )} +
    +
    + )} {noInitialResults && ( @@ -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} /> ))}