From f3a07753e6e64751f2ed212f6f1578aaabb709c9 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Mon, 25 Mar 2019 11:30:40 -0400 Subject: [PATCH 1/5] Add alert for org. delete. --- __tests__/components/DataListToolbar.test.jsx | 28 +++++ src/api.js | 5 + src/app.scss | 6 +- .../DataListToolbar/DataListToolbar.jsx | 15 ++- src/components/DataListToolbar/styles.scss | 13 ++ .../components/OrganizationListItem.jsx | 113 ++++++++++-------- .../screens/OrganizationsList.jsx | 89 +++++++++++++- 7 files changed, 209 insertions(+), 60 deletions(-) diff --git a/__tests__/components/DataListToolbar.test.jsx b/__tests__/components/DataListToolbar.test.jsx index 8f966ec739..b07c4fb5e8 100644 --- a/__tests__/components/DataListToolbar.test.jsx +++ b/__tests__/components/DataListToolbar.test.jsx @@ -221,4 +221,32 @@ 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 deleteModal = 'button.pf-c-button.pf-m-plain.awx-ToolBarBtn'; + const onSearch = jest.fn(); + const onSort = jest.fn(); + const onSelectAll = jest.fn(); + + toolbar = mount( + + ([1, 2, 3, 4])} + sortedColumnKey="name" + sortOrder="ascending" + columns={columns} + onSearch={onSearch} + onSort={onSort} + onSelectAll={onSelectAll} + onOpenDeleteModal={() => {}} + /> + + ); + + toolbar.find(deleteModal).simulate('click'); + expect(onOpenDeleteModal).toBeCalled(); + }); }); diff --git a/src/api.js b/src/api.js index f87c51703e..d448fe3425 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..6a6ef32afe 100644 --- a/src/app.scss +++ b/src/app.scss @@ -279,6 +279,10 @@ margin-bottom: 10px; } +.orgListAlert-actionBtn{ + margin:0 10px; +} + .awx-c-form-action-group { float: right; display: block; @@ -287,4 +291,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..7912c74ba3 100644 --- a/src/components/DataListToolbar/DataListToolbar.jsx +++ b/src/components/DataListToolbar/DataListToolbar.jsx @@ -24,29 +24,31 @@ import ExpandCollapse from '../ExpandCollapse'; import Search from '../Search'; import Sort from '../Sort'; import VerticalSeparator from '../VerticalSeparator'; +// import SelectedList from '../SelectedList'; 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 +117,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..55eea410d9 100644 --- a/src/pages/Organizations/components/OrganizationListItem.jsx +++ b/src/pages/Organizations/components/OrganizationListItem.jsx @@ -4,61 +4,72 @@ import { Trans, t } from '@lingui/macro'; import { Badge, Checkbox, + Button, } from '@patternfly/react-core'; +import { + TrashAltIcon, +} from '@patternfly/react-icons'; import { Link } from 'react-router-dom'; 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..9da3b1f85e 100644 --- a/src/pages/Organizations/screens/OrganizationsList.jsx +++ b/src/pages/Organizations/screens/OrganizationsList.jsx @@ -12,9 +12,11 @@ import { EmptyState, EmptyStateIcon, EmptyStateBody, + Modal, PageSection, PageSectionVariants, - Title + Title, + Button } from '@patternfly/react-core'; import { CubesIcon } from '@patternfly/react-icons'; import DataListToolbar from '../../../components/DataListToolbar'; @@ -54,6 +56,8 @@ class OrganizationsList extends Component { loading: true, results: [], selected: [], + isModalOpen: false, + orgsToDelete: [] }; this.onSearch = this.onSearch.bind(this); @@ -64,6 +68,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 +136,50 @@ class OrganizationsList extends Component { 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) { const { history, location } = this.props; const pathname = '/organizations'; @@ -196,18 +247,47 @@ class OrganizationsList extends Component { error, loading, noInitialResults, + orgsToDelete, 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} +
    +
    + ))} +
    + + + + +
    + )} {noInitialResults && ( @@ -229,6 +309,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 +326,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} /> ))} From cc0fd6beb6dff9494f8a1f7a801cc2f1aa96f725 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 28 Mar 2019 10:42:17 -0400 Subject: [PATCH 2/5] fix openDeleteModal with button test --- __tests__/components/DataListToolbar.test.jsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/__tests__/components/DataListToolbar.test.jsx b/__tests__/components/DataListToolbar.test.jsx index b07c4fb5e8..6567e21a05 100644 --- a/__tests__/components/DataListToolbar.test.jsx +++ b/__tests__/components/DataListToolbar.test.jsx @@ -225,10 +225,12 @@ describe('', () => { 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 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( @@ -241,12 +243,14 @@ describe('', () => { onSearch={onSearch} onSort={onSort} onSelectAll={onSelectAll} - onOpenDeleteModal={() => {}} + onOpenDeleteModal={onOpenDeleteModal} + showDelete={showDelete} + disableTrashCanIcon={disableTrashCanIcon} /> ); - toolbar.find(deleteModal).simulate('click'); + toolbar.find(openDeleteModalButton).simulate('click'); expect(onOpenDeleteModal).toBeCalled(); }); }); From de55ec168895090778e42aa6dd1b8b2e585df7eb Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Mon, 25 Mar 2019 11:30:40 -0400 Subject: [PATCH 3/5] Add alert for org. delete. --- __tests__/components/DataListToolbar.test.jsx | 2 +- .../screens/OrganizationsList.test.jsx | 116 ++++++++++++++++++ src/app.scss | 7 ++ .../components/OrganizationListItem.jsx | 4 - .../screens/OrganizationsList.jsx | 77 ++++++++---- 5 files changed, 178 insertions(+), 28 deletions(-) diff --git a/__tests__/components/DataListToolbar.test.jsx b/__tests__/components/DataListToolbar.test.jsx index 6567e21a05..50e2a6f61b 100644 --- a/__tests__/components/DataListToolbar.test.jsx +++ b/__tests__/components/DataListToolbar.test.jsx @@ -236,7 +236,7 @@ describe('', () => { ([1, 2, 3, 4])} + selected={() => [1, 2, 3, 4]} sortedColumnKey="name" sortOrder="ascending" columns={columns} diff --git a/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx b/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx index 6b7f49646d..6b62d9343f 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,81 @@ 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('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.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(); + expect(list.state().results).toHaveLength(0); + done(); + }); + }); }); diff --git a/src/app.scss b/src/app.scss index 6a6ef32afe..b8606fd856 100644 --- a/src/app.scss +++ b/src/app.scss @@ -282,6 +282,13 @@ .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; diff --git a/src/pages/Organizations/components/OrganizationListItem.jsx b/src/pages/Organizations/components/OrganizationListItem.jsx index 55eea410d9..4a8309ba11 100644 --- a/src/pages/Organizations/components/OrganizationListItem.jsx +++ b/src/pages/Organizations/components/OrganizationListItem.jsx @@ -4,11 +4,7 @@ import { Trans, t } from '@lingui/macro'; import { Badge, Checkbox, - Button, } from '@patternfly/react-core'; -import { - TrashAltIcon, -} from '@patternfly/react-icons'; import { Link } from 'react-router-dom'; diff --git a/src/pages/Organizations/screens/OrganizationsList.jsx b/src/pages/Organizations/screens/OrganizationsList.jsx index 9da3b1f85e..0ed9ce0d3d 100644 --- a/src/pages/Organizations/screens/OrganizationsList.jsx +++ b/src/pages/Organizations/screens/OrganizationsList.jsx @@ -16,8 +16,11 @@ import { PageSection, PageSectionVariants, Title, - Button + Button, + Progress, + ProgressVariant } from '@patternfly/react-core'; + import { CubesIcon } from '@patternfly/react-icons'; import DataListToolbar from '../../../components/DataListToolbar'; import OrganizationListItem from '../components/OrganizationListItem'; @@ -57,7 +60,10 @@ class OrganizationsList extends Component { results: [], selected: [], isModalOpen: false, - orgsToDelete: [] + orgsToDelete: [], + orgsDeleted: [], + deleteSuccess: false, + }; this.onSearch = this.onSearch.bind(this); @@ -137,7 +143,12 @@ class OrganizationsList extends Component { } handleClearOrgsToDelete () { - this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen, orgsToDelete: [] })); + this.setState({ + isModalOpen: false, + orgsDeleted: [], + deleteSuccess: false, + orgsToDelete: [] + }); this.onSelectAll(); } @@ -163,20 +174,24 @@ class OrganizationsList extends Component { } async handleOrgDelete (event) { - const { orgsToDelete } = this.state; + const { orgsToDelete, orgsDeleted } = this.state; const { api } = this.props; - try { - const deleteOrgsApiCalls = []; + this.setState({ deleteStarted: true }); - orgsToDelete.forEach((org) => { - deleteOrgsApiCalls.push(api.destroyOrganization(org.id)); - }); - await Promise.all(deleteOrgsApiCalls); - } finally { - this.handleClearOrgsToDelete(); - const queryParams = this.getQueryParams(); - this.fetchOrganizations(queryParams); - } + 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(); } @@ -245,9 +260,12 @@ class OrganizationsList extends Component { const { count, error, + deleteSuccess, + deleteStarted, loading, noInitialResults, orgsToDelete, + orgsDeleted, page, pageCount, page_size, @@ -266,7 +284,6 @@ class OrganizationsList extends Component { { isModalOpen && ( {orgsToDelete.map((org) => ( - - {org.name} + + + {org.name} +
    -
    + ))} +
    + +

    - - - - +
    + {orgsDeleted.length + ? + : ( + + + + + )} +
    )} {noInitialResults && ( From a42a1bfa17bf6dda18918f3d8dd96c422351e2d1 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Fri, 5 Apr 2019 11:29:48 -0400 Subject: [PATCH 4/5] Addresses PR feedback --- .../pages/Organizations/screens/OrganizationsList.test.jsx | 2 -- src/components/DataListToolbar/DataListToolbar.jsx | 1 - 2 files changed, 3 deletions(-) diff --git a/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx b/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx index 6b62d9343f..e874199da5 100644 --- a/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx +++ b/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx @@ -72,7 +72,6 @@ describe('', () => { ); 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); @@ -129,7 +128,6 @@ describe('', () => { wrapper.find('button[aria-label="confirm-delete"]').simulate('click'); expect(list.state().orgsToDelete.length).toEqual(list.state().orgsDeleted.length); expect(fetchOrganizations).toHaveBeenCalled(); - expect(list.state().results).toHaveLength(0); done(); }); }); diff --git a/src/components/DataListToolbar/DataListToolbar.jsx b/src/components/DataListToolbar/DataListToolbar.jsx index 7912c74ba3..d77ab68ae3 100644 --- a/src/components/DataListToolbar/DataListToolbar.jsx +++ b/src/components/DataListToolbar/DataListToolbar.jsx @@ -24,7 +24,6 @@ import ExpandCollapse from '../ExpandCollapse'; import Search from '../Search'; import Sort from '../Sort'; import VerticalSeparator from '../VerticalSeparator'; -// import SelectedList from '../SelectedList'; class DataListToolbar extends React.Component { render () { From c0b882d6fb97a25575c2fcb50336f3e9b8873419 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Fri, 5 Apr 2019 12:01:04 -0400 Subject: [PATCH 5/5] fixes progress bar --- src/pages/Organizations/screens/OrganizationsList.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/Organizations/screens/OrganizationsList.jsx b/src/pages/Organizations/screens/OrganizationsList.jsx index 0ed9ce0d3d..f40d572cc2 100644 --- a/src/pages/Organizations/screens/OrganizationsList.jsx +++ b/src/pages/Organizations/screens/OrganizationsList.jsx @@ -147,7 +147,8 @@ class OrganizationsList extends Component { isModalOpen: false, orgsDeleted: [], deleteSuccess: false, - orgsToDelete: [] + orgsToDelete: [], + deleteStarted: false }); this.onSelectAll(); }