diff --git a/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx b/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx
index cf02be5fac..261bfb2641 100644
--- a/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx
+++ b/awx/ui_next/src/screens/Team/TeamList/TeamList.jsx
@@ -1,10 +1,11 @@
-import React, { Component, Fragment } from 'react';
-import { withRouter } from 'react-router-dom';
+import React, { Fragment, useState, useEffect, useCallback } from 'react';
+import { useLocation, useRouteMatch } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Card, PageSection } from '@patternfly/react-core';
import { TeamsAPI } from '@api';
+import useRequest, { useDeleteItems } from '@util/useRequest';
import AlertModal from '@components/AlertModal';
import DataListToolbar from '@components/DataListToolbar';
import ErrorDetail from '@components/ErrorDetail';
@@ -22,218 +23,167 @@ const QS_CONFIG = getQSConfig('team', {
order_by: 'name',
});
-class TeamsList extends Component {
- constructor(props) {
- super(props);
+function TeamList({ i18n }) {
+ const location = useLocation();
+ const match = useRouteMatch();
+ const [selected, setSelected] = useState([]);
- this.state = {
- hasContentLoading: true,
- contentError: null,
- deletionError: null,
+ const {
+ result: { teams, itemCount, actions },
+ error: contentError,
+ isLoading,
+ request: fetchTeams,
+ } = useRequest(
+ useCallback(async () => {
+ const params = parseQueryString(QS_CONFIG, location.search);
+ const [response, actionsResponse] = await Promise.all([
+ TeamsAPI.read(params),
+ TeamsAPI.readOptions(),
+ ]);
+ return {
+ teams: response.data.results,
+ itemCount: response.data.count,
+ actions: actionsResponse.data.actions,
+ };
+ }, [location]),
+ {
teams: [],
- selected: [],
itemCount: 0,
- actions: null,
- };
-
- this.handleSelectAll = this.handleSelectAll.bind(this);
- this.handleSelect = this.handleSelect.bind(this);
- this.handleTeamDelete = this.handleTeamDelete.bind(this);
- this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
- this.loadTeams = this.loadTeams.bind(this);
- }
-
- componentDidMount() {
- this.loadTeams();
- }
-
- componentDidUpdate(prevProps) {
- const { location } = this.props;
- if (location !== prevProps.location) {
- this.loadTeams();
+ actions: {},
}
- }
+ );
- handleSelectAll(isSelected) {
- const { teams } = this.state;
+ useEffect(() => {
+ fetchTeams();
+ }, [fetchTeams]);
- const selected = isSelected ? [...teams] : [];
- this.setState({ selected });
- }
+ const isAllSelected = selected.length === teams.length && selected.length > 0;
+ const {
+ isLoading: isDeleteLoading,
+ deleteItems: deleteTeams,
+ deletionError,
+ clearDeletionError,
+ } = useDeleteItems(
+ useCallback(async () => {
+ return Promise.all(selected.map(team => TeamsAPI.destroy(team.id)));
+ }, [selected]),
+ {
+ qsConfig: QS_CONFIG,
+ allItemsSelected: isAllSelected,
+ fetchItems: fetchTeams,
+ }
+ );
- handleSelect(row) {
- const { selected } = this.state;
+ const handleTeamDelete = async () => {
+ await deleteTeams();
+ setSelected([]);
+ };
+ const hasContentLoading = isDeleteLoading || isLoading;
+ const canAdd = actions && actions.POST;
+
+ const handleSelectAll = isSelected => {
+ setSelected(isSelected ? [...teams] : []);
+ };
+
+ const handleSelect = row => {
if (selected.some(s => s.id === row.id)) {
- this.setState({ selected: selected.filter(s => s.id !== row.id) });
+ setSelected(selected.filter(s => s.id !== row.id));
} else {
- this.setState({ selected: selected.concat(row) });
+ setSelected(selected.concat(row));
}
- }
+ };
- handleDeleteErrorClose() {
- this.setState({ deletionError: null });
- }
-
- async handleTeamDelete() {
- const { selected } = this.state;
-
- this.setState({ hasContentLoading: true });
- try {
- await Promise.all(selected.map(team => TeamsAPI.destroy(team.id)));
- } catch (err) {
- this.setState({ deletionError: err });
- } finally {
- await this.loadTeams();
- }
- }
-
- async loadTeams() {
- const { location } = this.props;
- const { actions: cachedActions } = this.state;
- const params = parseQueryString(QS_CONFIG, location.search);
-
- let optionsPromise;
- if (cachedActions) {
- optionsPromise = Promise.resolve({ data: { actions: cachedActions } });
- } else {
- optionsPromise = TeamsAPI.readOptions();
- }
-
- const promises = Promise.all([TeamsAPI.read(params), optionsPromise]);
-
- this.setState({ contentError: null, hasContentLoading: true });
- try {
- const [
- {
- data: { count, results },
- },
- {
- data: { actions },
- },
- ] = await promises;
- this.setState({
- actions,
- itemCount: count,
- teams: results,
- selected: [],
- });
- } catch (err) {
- this.setState({ contentError: err });
- } finally {
- this.setState({ hasContentLoading: false });
- }
- }
-
- render() {
- const {
- actions,
- itemCount,
- contentError,
- hasContentLoading,
- deletionError,
- selected,
- teams,
- } = this.state;
- const { match, i18n } = this.props;
-
- const canAdd =
- actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
- const isAllSelected =
- selected.length > 0 && selected.length === teams.length;
-
- return (
-
-
-
- (
- ,
- ]
- : []),
- ,
- ]}
- />
- )}
- renderItem={o => (
- row.id === o.id)}
- onSelect={() => this.handleSelect(o)}
- />
- )}
- emptyStateControls={
- canAdd ? (
-
- ) : null
- }
- />
-
-
-
- {i18n._(t`Failed to delete one or more teams.`)}
-
-
-
- );
- }
+ return (
+
+
+
+ (
+ ,
+ ]
+ : []),
+ ,
+ ]}
+ />
+ )}
+ renderItem={o => (
+ row.id === o.id)}
+ onSelect={() => handleSelect(o)}
+ />
+ )}
+ emptyStateControls={
+ canAdd ? (
+
+ ) : null
+ }
+ />
+
+
+
+ {i18n._(t`Failed to delete one or more teams.`)}
+
+
+
+ );
}
-export { TeamsList as _TeamsList };
-export default withI18n()(withRouter(TeamsList));
+export default withI18n()(TeamList);
diff --git a/awx/ui_next/src/screens/Team/TeamList/TeamList.test.jsx b/awx/ui_next/src/screens/Team/TeamList/TeamList.test.jsx
index 063b958a70..da6520581b 100644
--- a/awx/ui_next/src/screens/Team/TeamList/TeamList.test.jsx
+++ b/awx/ui_next/src/screens/Team/TeamList/TeamList.test.jsx
@@ -1,12 +1,13 @@
import React from 'react';
+import { act } from 'react-dom/test-utils';
import { TeamsAPI } from '@api';
-import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
-import TeamsList, { _TeamsList } from './TeamList';
+import TeamList from './TeamList';
jest.mock('@api');
-const mockAPITeamsList = {
+const mockAPITeamList = {
data: {
count: 3,
results: [
@@ -50,15 +51,14 @@ const mockAPITeamsList = {
warningMsg: 'message',
};
-describe('', () => {
- let wrapper;
-
+describe('', () => {
beforeEach(() => {
- TeamsAPI.read = () =>
+ TeamsAPI.read = jest.fn(() =>
Promise.resolve({
- data: mockAPITeamsList.data,
- });
- TeamsAPI.readOptions = () =>
+ data: mockAPITeamList.data,
+ })
+ );
+ TeamsAPI.readOptions = jest.fn(() =>
Promise.resolve({
data: {
actions: {
@@ -66,105 +66,119 @@ describe('', () => {
POST: {},
},
},
- });
- });
-
- test('initially renders succesfully', () => {
- mountWithContexts();
- });
-
- test('Selects one team when row is checked', async () => {
- wrapper = mountWithContexts();
- await waitForElement(
- wrapper,
- 'TeamsList',
- el => el.state('hasContentLoading') === false
+ })
);
- expect(
- wrapper
- .find('input[type="checkbox"]')
- .findWhere(n => n.prop('checked') === true).length
- ).toBe(0);
- wrapper
- .find('TeamListItem')
- .at(0)
- .find('DataListCheck')
- .props()
- .onChange(true);
- wrapper.update();
- expect(
- wrapper
- .find('input[type="checkbox"]')
- .findWhere(n => n.prop('checked') === true).length
- ).toBe(1);
});
- test('Select all checkbox selects and unselects all rows', async () => {
- wrapper = mountWithContexts();
- await waitForElement(
- wrapper,
- 'TeamsList',
- el => el.state('hasContentLoading') === false
- );
- expect(
- wrapper
- .find('input[type="checkbox"]')
- .findWhere(n => n.prop('checked') === true).length
- ).toBe(0);
- wrapper
- .find('Checkbox#select-all')
- .props()
- .onChange(true);
- wrapper.update();
- expect(
- wrapper
- .find('input[type="checkbox"]')
- .findWhere(n => n.prop('checked') === true).length
- ).toBe(4);
- wrapper
- .find('Checkbox#select-all')
- .props()
- .onChange(false);
- wrapper.update();
- expect(
- wrapper
- .find('input[type="checkbox"]')
- .findWhere(n => n.prop('checked') === true).length
- ).toBe(0);
- });
-
- test('api is called to delete Teams for each team in selected.', () => {
- wrapper = mountWithContexts();
- const component = wrapper.find('TeamsList');
- wrapper.find('TeamsList').setState({
- teams: mockAPITeamsList.data.results,
- itemCount: 3,
- isInitialized: true,
- isModalOpen: mockAPITeamsList.isModalOpen,
- selected: mockAPITeamsList.data.results,
+ test('should load and render teams', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts();
});
- wrapper.find('ToolbarDeleteButton').prop('onDelete')();
- expect(TeamsAPI.destroy).toHaveBeenCalledTimes(
- component.state('selected').length
- );
+ wrapper.update();
+
+ expect(wrapper.find('TeamListItem')).toHaveLength(3);
});
- test('call loadTeams after team(s) have been deleted', () => {
- const fetchTeams = jest.spyOn(_TeamsList.prototype, 'loadTeams');
- const event = { preventDefault: () => {} };
- wrapper = mountWithContexts();
- wrapper.find('TeamsList').setState({
- teams: mockAPITeamsList.data.results,
- itemCount: 3,
- isInitialized: true,
- selected: mockAPITeamsList.data.results.slice(0, 1),
+ test('should select team when checked', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts();
});
- const component = wrapper.find('TeamsList');
- component.instance().handleTeamDelete(event);
- expect(fetchTeams).toBeCalled();
+ wrapper.update();
+
+ await act(async () => {
+ wrapper
+ .find('TeamListItem')
+ .first()
+ .invoke('onSelect')();
+ });
+ wrapper.update();
+
+ expect(
+ wrapper
+ .find('TeamListItem')
+ .first()
+ .prop('isSelected')
+ ).toEqual(true);
});
- test('error is shown when team not successfully deleted from api', async done => {
+ test('should select all', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+
+ await act(async () => {
+ wrapper.find('DataListToolbar').invoke('onSelectAll')(true);
+ });
+ wrapper.update();
+
+ const items = wrapper.find('TeamListItem');
+ expect(items).toHaveLength(3);
+ items.forEach(item => {
+ expect(item.prop('isSelected')).toEqual(true);
+ });
+
+ expect(
+ wrapper
+ .find('TeamListItem')
+ .first()
+ .prop('isSelected')
+ ).toEqual(true);
+ });
+
+ test('should call delete api', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+
+ await act(async () => {
+ wrapper
+ .find('TeamListItem')
+ .at(0)
+ .invoke('onSelect')();
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper
+ .find('TeamListItem')
+ .at(1)
+ .invoke('onSelect')();
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
+ });
+
+ expect(TeamsAPI.destroy).toHaveBeenCalledTimes(2);
+ });
+
+ test('should re-fetch teams after team(s) have been deleted', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+ expect(TeamsAPI.read).toHaveBeenCalledTimes(1);
+ await act(async () => {
+ wrapper
+ .find('TeamListItem')
+ .at(0)
+ .invoke('onSelect')();
+ });
+ wrapper.update();
+ await act(async () => {
+ wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
+ });
+
+ expect(TeamsAPI.read).toHaveBeenCalledTimes(2);
+ });
+
+ test('should show deletion error', async () => {
TeamsAPI.destroy.mockRejectedValue(
new Error({
response: {
@@ -176,40 +190,41 @@ describe('', () => {
},
})
);
-
- wrapper = mountWithContexts();
- wrapper.find('TeamsList').setState({
- teams: mockAPITeamsList.data.results,
- itemCount: 3,
- isInitialized: true,
- selected: mockAPITeamsList.data.results.slice(0, 1),
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts();
});
- wrapper.find('ToolbarDeleteButton').prop('onDelete')();
- await waitForElement(
- wrapper,
- 'Modal',
- el => el.props().isOpen === true && el.props().title === 'Error!'
- );
- done();
+ wrapper.update();
+ expect(TeamsAPI.read).toHaveBeenCalledTimes(1);
+ await act(async () => {
+ wrapper
+ .find('TeamListItem')
+ .at(0)
+ .invoke('onSelect')();
+ });
+ wrapper.update();
+
+ await act(async () => {
+ wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
+ });
+ wrapper.update();
+
+ const modal = wrapper.find('Modal');
+ expect(modal).toHaveLength(1);
+ expect(modal.prop('title')).toEqual('Error!');
});
- test('Add button shown for users without ability to POST', async done => {
- wrapper = mountWithContexts();
- await waitForElement(
- wrapper,
- 'TeamsList',
- el => el.state('hasContentLoading') === true
- );
- await waitForElement(
- wrapper,
- 'TeamsList',
- el => el.state('hasContentLoading') === false
- );
+ test('Add button shown for users without ability to POST', async () => {
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+
expect(wrapper.find('ToolbarAddButton').length).toBe(1);
- done();
});
- test('Add button hidden for users without ability to POST', async done => {
+ test('Add button hidden for users without ability to POST', async () => {
TeamsAPI.readOptions = () =>
Promise.resolve({
data: {
@@ -218,18 +233,12 @@ describe('', () => {
},
},
});
- wrapper = mountWithContexts();
- await waitForElement(
- wrapper,
- 'TeamsList',
- el => el.state('hasContentLoading') === true
- );
- await waitForElement(
- wrapper,
- 'TeamsList',
- el => el.state('hasContentLoading') === false
- );
+ let wrapper;
+ await act(async () => {
+ wrapper = mountWithContexts();
+ });
+ wrapper.update();
+
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
- done();
});
});