diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx index ac94f980af..8ae62d85e1 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.jsx @@ -5,11 +5,14 @@ import { t } from '@lingui/macro'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import PaginatedDataList, { ToolbarAddButton, + ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; import useSelected from '../../../util/useSelected'; -import useRequest from '../../../util/useRequest'; -import { UsersAPI } from '../../../api'; +import useRequest, { useDeleteItems } from '../../../util/useRequest'; +import { UsersAPI, TokensAPI } from '../../../api'; import DataListToolbar from '../../../components/DataListToolbar'; +import AlertModal from '../../../components/AlertModal'; +import ErrorDetail from '../../../components/ErrorDetail'; import UserTokensListItem from './UserTokenListItem'; const QS_CONFIG = getQSConfig('user', { @@ -54,84 +57,127 @@ function UserTokenList({ i18n }) { tokens ); + const { + isLoading: isDeleteLoading, + deleteItems: deleteTokens, + deletionError, + clearDeletionError, + } = useDeleteItems( + useCallback(async () => { + return Promise.all( + selected.map(({ id: tokenId }) => TokensAPI.destroy(tokenId)) + ); + }, [selected]), + { + qsConfig: QS_CONFIG, + allItemsSelected: isAllSelected, + fetchItems: fetchTokens, + } + ); + const handleDelete = async () => { + await deleteTokens(); + setSelected([]); + }; + const canAdd = true; return ( - ( - setSelected(isSelected ? [...tokens] : [])} - additionalControls={[ - ...(canAdd - ? [ - , - ] - : []), - ]} - /> + <> + ( + + setSelected(isSelected ? [...tokens] : []) + } + additionalControls={[ + ...(canAdd + ? [ + , + ] + : []), + , + ]} + /> + )} + renderItem={token => ( + { + handleSelect(token); + }} + isSelected={selected.some(row => row.id === token.id)} + /> + )} + emptyStateControls={ + canAdd ? ( + + ) : null + } + /> + {deletionError && ( + + {i18n._(t`Failed to delete one or more user tokens.`)} + + )} - renderItem={token => ( - { - handleSelect(token); - }} - isSelected={selected.some(row => row.id === token.id)} - /> - )} - emptyStateControls={ - canAdd ? ( - - ) : null - } - /> + ); } diff --git a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.test.jsx b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.test.jsx index 83549798bb..07a7d66b66 100644 --- a/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.test.jsx +++ b/awx/ui_next/src/screens/User/UserTokenList/UserTokenList.test.jsx @@ -4,10 +4,11 @@ import { mountWithContexts, waitForElement, } from '../../../../testUtils/enzymeHelpers'; -import { UsersAPI } from '../../../api'; +import { UsersAPI, TokensAPI } from '../../../api'; import UserTokenList from './UserTokenList'; jest.mock('../../../api/models/Users'); +jest.mock('../../../api/models/Tokens'); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -162,4 +163,96 @@ describe('', () => { wrapper.find('DataListCheck[id="select-token-3"]').props().checked ).toBe(true); }); + test('delete button should be disabled', async () => { + UsersAPI.readTokens.mockResolvedValue(tokens); + await act(async () => { + wrapper = mountWithContexts(); + }); + waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); + expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( + true + ); + }); + test('should select and then delete item properly', async () => { + UsersAPI.readTokens.mockResolvedValue(tokens); + await act(async () => { + wrapper = mountWithContexts(); + }); + waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); + expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( + true + ); + await act(async () => { + wrapper + .find('DataListCheck[aria-labelledby="check-action-3"]') + .prop('onChange')(tokens.data.results[0]); + }); + wrapper.update(); + expect( + wrapper + .find('DataListCheck[aria-labelledby="check-action-3"]') + .prop('checked') + ).toBe(true); + expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( + false + ); + await act(async () => + wrapper.find('Button[aria-label="Delete"]').prop('onClick')() + ); + wrapper.update(); + await act(async () => expect(wrapper.find('AlertModal').length).toBe(1)); + await act(async () => + wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')() + ); + wrapper.update(); + expect(TokensAPI.destroy).toHaveBeenCalledWith(3); + }); + test('should select and then delete item properly', async () => { + UsersAPI.readTokens.mockResolvedValue(tokens); + TokensAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/tokens', + }, + data: 'An error occurred', + status: 403, + }, + }) + ); + await act(async () => { + wrapper = mountWithContexts(); + }); + waitForElement(wrapper, 'ContentEmpty', el => el.length === 0); + expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( + true + ); + await act(async () => { + wrapper + .find('DataListCheck[aria-labelledby="check-action-3"]') + .prop('onChange')(tokens.data.results[0]); + }); + wrapper.update(); + expect( + wrapper + .find('DataListCheck[aria-labelledby="check-action-3"]') + .prop('checked') + ).toBe(true); + expect(wrapper.find('Button[aria-label="Delete"]').prop('isDisabled')).toBe( + false + ); + await act(async () => + wrapper.find('Button[aria-label="Delete"]').prop('onClick')() + ); + wrapper.update(); + await act(async () => expect(wrapper.find('AlertModal').length).toBe(1)); + await act(async () => + wrapper.find('Button[aria-label="confirm delete"]').prop('onClick')() + ); + wrapper.update(); + expect(TokensAPI.destroy).toHaveBeenCalledWith(3); + wrapper.update(); + expect(wrapper.find('ErrorDetail').length).toBe(1); + }); });