diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.jsx
index 0854c16f99..cb2b88d168 100644
--- a/awx/ui_next/src/components/NotificationList/NotificationList.jsx
+++ b/awx/ui_next/src/components/NotificationList/NotificationList.jsx
@@ -1,15 +1,14 @@
-import React, { Component, Fragment } from 'react';
-import { number, shape, string, bool } from 'prop-types';
-import { withRouter } from 'react-router-dom';
+import React, { useEffect, useCallback, useState } from 'react';
+import { useLocation } from 'react-router-dom';
+import { number, shape, bool } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
-
import AlertModal from '../AlertModal';
import ErrorDetail from '../ErrorDetail';
import NotificationListItem from './NotificationListItem';
import PaginatedDataList from '../PaginatedDataList';
import { getQSConfig, parseQueryString } from '../../util/qs';
-
+import useRequest from '../../util/useRequest';
import { NotificationTemplatesAPI } from '../../api';
const QS_CONFIG = getQSConfig('notification', {
@@ -18,64 +17,49 @@ const QS_CONFIG = getQSConfig('notification', {
order_by: 'name',
});
-class NotificationList extends Component {
- constructor(props) {
- super(props);
- this.state = {
- contentError: null,
- hasContentLoading: true,
- toggleError: false,
- loadingToggleIds: [],
- itemCount: 0,
- notifications: [],
- startedTemplateIds: [],
- successTemplateIds: [],
- errorTemplateIds: [],
- typeLabels: null,
- };
- this.handleNotificationToggle = this.handleNotificationToggle.bind(this);
- this.handleNotificationErrorClose = this.handleNotificationErrorClose.bind(
- this
- );
- this.loadNotifications = this.loadNotifications.bind(this);
- }
+function NotificationList({ apiModel, canToggleNotifications, id, i18n }) {
+ const location = useLocation();
+ const [isToggleLoading, setIsToggleLoading] = useState(false);
+ const [toggleError, setToggleError] = useState(null);
- componentDidMount() {
- this.loadNotifications();
- }
+ const {
+ result: fetchNotificationsResult,
+ result: {
+ notifications,
+ itemCount,
+ startedTemplateIds,
+ successTemplateIds,
+ errorTemplateIds,
+ typeLabels,
+ },
+ error: contentError,
+ isLoading,
+ request: fetchNotifications,
+ setValue,
+ } = useRequest(
+ useCallback(async () => {
+ const params = parseQueryString(QS_CONFIG, location.search);
+ const [
+ {
+ data: { results: notificationsResults, count: notificationsCount },
+ },
+ {
+ data: { actions },
+ },
+ ] = await Promise.all([
+ NotificationTemplatesAPI.read(params),
+ NotificationTemplatesAPI.readOptions(),
+ ]);
- componentDidUpdate(prevProps) {
- const { location } = this.props;
- if (location !== prevProps.location) {
- this.loadNotifications();
- }
- }
+ const labels = actions.GET.notification_type.choices.reduce(
+ (map, notifType) => ({ ...map, [notifType[0]]: notifType[1] }),
+ {}
+ );
- async loadNotifications() {
- const { id, location, apiModel } = this.props;
- const { typeLabels } = this.state;
- const params = parseQueryString(QS_CONFIG, location.search);
-
- const promises = [NotificationTemplatesAPI.read(params)];
-
- if (!typeLabels) {
- promises.push(NotificationTemplatesAPI.readOptions());
- }
-
- this.setState({ contentError: null, hasContentLoading: true });
- try {
- const {
- data: { count: itemCount = 0, results: notifications = [] },
- } = await NotificationTemplatesAPI.read(params);
-
- const optionsResponse = await NotificationTemplatesAPI.readOptions();
-
- let idMatchParams;
- if (notifications.length > 0) {
- idMatchParams = { id__in: notifications.map(n => n.id).join(',') };
- } else {
- idMatchParams = {};
- }
+ const idMatchParams =
+ notificationsResults.length > 0
+ ? { id__in: notificationsResults.map(n => n.id).join(',') }
+ : {};
const [
{ data: startedTemplates },
@@ -87,69 +71,35 @@ class NotificationList extends Component {
apiModel.readNotificationTemplatesError(id, idMatchParams),
]);
- const stateToUpdate = {
- itemCount,
- notifications,
+ return {
+ notifications: notificationsResults,
+ itemCount: notificationsCount,
startedTemplateIds: startedTemplates.results.map(st => st.id),
successTemplateIds: successTemplates.results.map(su => su.id),
errorTemplateIds: errorTemplates.results.map(e => e.id),
+ typeLabels: labels,
};
-
- if (!typeLabels) {
- const {
- data: {
- actions: {
- GET: {
- notification_type: { choices },
- },
- },
- },
- } = optionsResponse;
- // The structure of choices looks like [['slack', 'Slack'], ['email', 'Email'], ...]
- stateToUpdate.typeLabels = choices.reduce(
- (map, notifType) => ({ ...map, [notifType[0]]: notifType[1] }),
- {}
- );
- }
-
- this.setState(stateToUpdate);
- } catch (err) {
- this.setState({ contentError: err });
- } finally {
- this.setState({ hasContentLoading: false });
+ }, [apiModel, id, location]),
+ {
+ notifications: [],
+ itemCount: 0,
+ startedTemplateIds: [],
+ successTemplateIds: [],
+ errorTemplateIds: [],
+ typeLabels: {},
}
- }
+ );
- async handleNotificationToggle(notificationId, isCurrentlyOn, status) {
- const { id, apiModel } = this.props;
+ useEffect(() => {
+ fetchNotifications();
+ }, [fetchNotifications]);
- let stateArrayName;
- if (status === 'success') {
- stateArrayName = 'successTemplateIds';
- } else if (status === 'error') {
- stateArrayName = 'errorTemplateIds';
- } else if (status === 'started') {
- stateArrayName = 'startedTemplateIds';
- }
-
- let stateUpdateFunction;
- if (isCurrentlyOn) {
- // when switching off, remove the toggled notification id from the array
- stateUpdateFunction = prevState => ({
- [stateArrayName]: prevState[stateArrayName].filter(
- i => i !== notificationId
- ),
- });
- } else {
- // when switching on, add the toggled notification id to the array
- stateUpdateFunction = prevState => ({
- [stateArrayName]: prevState[stateArrayName].concat(notificationId),
- });
- }
-
- this.setState(({ loadingToggleIds }) => ({
- loadingToggleIds: loadingToggleIds.concat([notificationId]),
- }));
+ const handleNotificationToggle = async (
+ notificationId,
+ isCurrentlyOn,
+ status
+ ) => {
+ setIsToggleLoading(true);
try {
if (isCurrentlyOn) {
await apiModel.disassociateNotificationTemplate(
@@ -157,128 +107,111 @@ class NotificationList extends Component {
notificationId,
status
);
+ setValue({
+ ...fetchNotificationsResult,
+ [`${status}TemplateIds`]: fetchNotificationsResult[
+ `${status}TemplateIds`
+ ].filter(i => i !== notificationId),
+ });
} else {
await apiModel.associateNotificationTemplate(
id,
notificationId,
status
);
+ setValue({
+ ...fetchNotificationsResult,
+ [`${status}TemplateIds`]: fetchNotificationsResult[
+ `${status}TemplateIds`
+ ].concat(notificationId),
+ });
}
- this.setState(stateUpdateFunction);
} catch (err) {
- this.setState({ toggleError: err });
+ setToggleError(err);
} finally {
- this.setState(({ loadingToggleIds }) => ({
- loadingToggleIds: loadingToggleIds.filter(
- item => item !== notificationId
- ),
- }));
+ setIsToggleLoading(false);
}
- }
+ };
- handleNotificationErrorClose() {
- this.setState({ toggleError: false });
- }
-
- render() {
- const { canToggleNotifications, i18n } = this.props;
- const {
- contentError,
- hasContentLoading,
- toggleError,
- loadingToggleIds,
- itemCount,
- notifications,
- startedTemplateIds,
- successTemplateIds,
- errorTemplateIds,
- typeLabels,
- } = this.state;
-
- return (
-
- (
-
- )}
- />
+ return (
+ <>
+ (
+
+ )}
+ />
+ {toggleError && (
setToggleError(null)}
>
{i18n._(t`Failed to toggle notification.`)}
-
- );
- }
+ )}
+ >
+ );
}
NotificationList.propTypes = {
+ apiModel: shape({}).isRequired,
id: number.isRequired,
canToggleNotifications: bool.isRequired,
- location: shape({
- search: string.isRequired,
- }).isRequired,
};
-export { NotificationList as _NotificationList };
-export default withI18n()(withRouter(NotificationList));
+export default withI18n()(NotificationList);
diff --git a/awx/ui_next/src/components/NotificationList/NotificationList.test.jsx b/awx/ui_next/src/components/NotificationList/NotificationList.test.jsx
index ee278ab4bf..071eb45e9f 100644
--- a/awx/ui_next/src/components/NotificationList/NotificationList.test.jsx
+++ b/awx/ui_next/src/components/NotificationList/NotificationList.test.jsx
@@ -1,15 +1,13 @@
import React from 'react';
-
+import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
-import { sleep } from '../../../testUtils/testUtils';
-
import { NotificationTemplatesAPI } from '../../api';
-
import NotificationList from './NotificationList';
jest.mock('../../api');
describe('', () => {
+ let wrapper;
const data = {
count: 2,
results: [
@@ -58,220 +56,185 @@ describe('', () => {
},
});
- beforeEach(() => {
- NotificationTemplatesAPI.read.mockReturnValue({ data });
- MockModelAPI.readNotificationTemplatesSuccess.mockReturnValue({
- data: { results: [{ id: 1 }] },
- });
- MockModelAPI.readNotificationTemplatesError.mockReturnValue({
- data: { results: [{ id: 2 }] },
- });
- MockModelAPI.readNotificationTemplatesStarted.mockReturnValue({
- data: { results: [{ id: 3 }] },
+ NotificationTemplatesAPI.read.mockReturnValue({ data });
+
+ MockModelAPI.readNotificationTemplatesSuccess.mockReturnValue({
+ data: { results: [{ id: 1 }] },
+ });
+
+ MockModelAPI.readNotificationTemplatesError.mockReturnValue({
+ data: { results: [{ id: 2 }] },
+ });
+
+ MockModelAPI.readNotificationTemplatesStarted.mockReturnValue({
+ data: { results: [{ id: 3 }] },
+ });
+
+ beforeEach(async () => {
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
});
+ wrapper.update();
});
afterEach(() => {
- jest.clearAllMocks();
+ wrapper.unmount();
});
- test('initially renders succesfully', async () => {
- const wrapper = mountWithContexts(
-
- );
- await sleep(0);
- wrapper.update();
- const dataList = wrapper.find('PaginatedDataList');
- expect(dataList).toHaveLength(1);
- expect(dataList.prop('items')).toEqual(data.results);
+ test('initially renders succesfully', () => {
+ expect(wrapper.find('PaginatedDataList')).toHaveLength(1);
});
- test('should render list fetched of items', async () => {
- const wrapper = mountWithContexts(
-
- );
- await sleep(0);
- wrapper.update();
-
+ test('should render list fetched of items', () => {
expect(NotificationTemplatesAPI.read).toHaveBeenCalled();
- expect(wrapper.find('NotificationList').state('notifications')).toEqual(
- data.results
- );
- const items = wrapper.find('NotificationListItem');
- expect(items).toHaveLength(3);
- expect(items.at(0).prop('successTurnedOn')).toEqual(true);
- expect(items.at(0).prop('errorTurnedOn')).toEqual(false);
- expect(items.at(0).prop('startedTurnedOn')).toEqual(false);
- expect(items.at(1).prop('successTurnedOn')).toEqual(false);
- expect(items.at(1).prop('errorTurnedOn')).toEqual(true);
- expect(items.at(1).prop('startedTurnedOn')).toEqual(false);
- expect(items.at(2).prop('successTurnedOn')).toEqual(false);
- expect(items.at(2).prop('errorTurnedOn')).toEqual(false);
- expect(items.at(2).prop('startedTurnedOn')).toEqual(true);
+ expect(NotificationTemplatesAPI.readOptions).toHaveBeenCalled();
+ expect(MockModelAPI.readNotificationTemplatesSuccess).toHaveBeenCalled();
+ expect(MockModelAPI.readNotificationTemplatesError).toHaveBeenCalled();
+ expect(MockModelAPI.readNotificationTemplatesStarted).toHaveBeenCalled();
+ expect(wrapper.find('NotificationListItem').length).toBe(3);
+ expect(
+ wrapper.find('input#notification-1-success-toggle').props().checked
+ ).toBe(true);
+ expect(
+ wrapper.find('input#notification-1-error-toggle').props().checked
+ ).toBe(false);
+ expect(
+ wrapper.find('input#notification-1-started-toggle').props().checked
+ ).toBe(false);
+ expect(
+ wrapper.find('input#notification-2-success-toggle').props().checked
+ ).toBe(false);
+ expect(
+ wrapper.find('input#notification-2-error-toggle').props().checked
+ ).toBe(true);
+ expect(
+ wrapper.find('input#notification-2-started-toggle').props().checked
+ ).toBe(false);
+ expect(
+ wrapper.find('input#notification-3-success-toggle').props().checked
+ ).toBe(false);
+ expect(
+ wrapper.find('input#notification-3-error-toggle').props().checked
+ ).toBe(false);
+ expect(
+ wrapper.find('input#notification-3-started-toggle').props().checked
+ ).toBe(true);
});
test('should enable success notification', async () => {
- const wrapper = mountWithContexts(
-
- );
- await sleep(0);
- wrapper.update();
-
expect(
- wrapper.find('NotificationList').state('successTemplateIds')
- ).toEqual([1]);
- const items = wrapper.find('NotificationListItem');
- items
- .at(1)
- .find('Switch[aria-label="Toggle notification success"]')
- .prop('onChange')();
+ wrapper.find('input#notification-2-success-toggle').props().checked
+ ).toBe(false);
+ await act(async () => {
+ wrapper.find('Switch#notification-2-success-toggle').prop('onChange')();
+ });
+ wrapper.update();
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
1,
2,
'success'
);
- await sleep(0);
- wrapper.update();
expect(
- wrapper.find('NotificationList').state('successTemplateIds')
- ).toEqual([1, 2]);
+ wrapper.find('input#notification-2-success-toggle').props().checked
+ ).toBe(true);
});
test('should enable error notification', async () => {
- const wrapper = mountWithContexts(
-
- );
- await sleep(0);
+ expect(
+ wrapper.find('input#notification-1-error-toggle').props().checked
+ ).toBe(false);
+ await act(async () => {
+ wrapper.find('Switch#notification-1-error-toggle').prop('onChange')();
+ });
wrapper.update();
-
- expect(wrapper.find('NotificationList').state('errorTemplateIds')).toEqual([
- 2,
- ]);
- const items = wrapper.find('NotificationListItem');
- items
- .at(0)
- .find('Switch[aria-label="Toggle notification failure"]')
- .prop('onChange')();
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
1,
1,
'error'
);
- await sleep(0);
- wrapper.update();
- expect(wrapper.find('NotificationList').state('errorTemplateIds')).toEqual([
- 2,
- 1,
- ]);
+ expect(
+ wrapper.find('input#notification-1-error-toggle').props().checked
+ ).toBe(true);
});
test('should enable start notification', async () => {
- const wrapper = mountWithContexts(
-
- );
- await sleep(0);
- wrapper.update();
-
expect(
- wrapper.find('NotificationList').state('startedTemplateIds')
- ).toEqual([3]);
- const items = wrapper.find('NotificationListItem');
- items
- .at(0)
- .find('Switch[aria-label="Toggle notification start"]')
- .prop('onChange')();
+ wrapper.find('input#notification-1-started-toggle').props().checked
+ ).toBe(false);
+ await act(async () => {
+ wrapper.find('Switch#notification-1-started-toggle').prop('onChange')();
+ });
+ wrapper.update();
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
1,
1,
'started'
);
- await sleep(0);
- wrapper.update();
expect(
- wrapper.find('NotificationList').state('startedTemplateIds')
- ).toEqual([3, 1]);
+ wrapper.find('input#notification-1-started-toggle').props().checked
+ ).toBe(true);
});
test('should disable success notification', async () => {
- const wrapper = mountWithContexts(
-
- );
- await sleep(0);
- wrapper.update();
-
expect(
- wrapper.find('NotificationList').state('successTemplateIds')
- ).toEqual([1]);
- const items = wrapper.find('NotificationListItem');
- items
- .at(0)
- .find('Switch[aria-label="Toggle notification success"]')
- .prop('onChange')();
+ wrapper.find('input#notification-1-success-toggle').props().checked
+ ).toBe(true);
+ await act(async () => {
+ wrapper.find('Switch#notification-1-success-toggle').prop('onChange')();
+ });
+ wrapper.update();
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
1,
1,
'success'
);
- await sleep(0);
- wrapper.update();
expect(
- wrapper.find('NotificationList').state('successTemplateIds')
- ).toEqual([]);
+ wrapper.find('input#notification-1-success-toggle').props().checked
+ ).toBe(false);
});
test('should disable error notification', async () => {
- const wrapper = mountWithContexts(
-
- );
- await sleep(0);
+ expect(
+ wrapper.find('input#notification-2-error-toggle').props().checked
+ ).toBe(true);
+ await act(async () => {
+ wrapper.find('Switch#notification-2-error-toggle').prop('onChange')();
+ });
wrapper.update();
-
- expect(wrapper.find('NotificationList').state('errorTemplateIds')).toEqual([
- 2,
- ]);
- const items = wrapper.find('NotificationListItem');
- items
- .at(1)
- .find('Switch[aria-label="Toggle notification failure"]')
- .prop('onChange')();
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
1,
2,
'error'
);
- await sleep(0);
- wrapper.update();
- expect(wrapper.find('NotificationList').state('errorTemplateIds')).toEqual(
- []
- );
+ expect(
+ wrapper.find('input#notification-2-error-toggle').props().checked
+ ).toBe(false);
});
test('should disable start notification', async () => {
- const wrapper = mountWithContexts(
-
- );
- await sleep(0);
- wrapper.update();
-
expect(
- wrapper.find('NotificationList').state('startedTemplateIds')
- ).toEqual([3]);
- const items = wrapper.find('NotificationListItem');
- items
- .at(2)
- .find('Switch[aria-label="Toggle notification start"]')
- .prop('onChange')();
+ wrapper.find('input#notification-3-started-toggle').props().checked
+ ).toBe(true);
+ await act(async () => {
+ wrapper.find('Switch#notification-3-started-toggle').prop('onChange')();
+ });
+ wrapper.update();
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
1,
3,
'started'
);
- await sleep(0);
- wrapper.update();
expect(
- wrapper.find('NotificationList').state('startedTemplateIds')
- ).toEqual([]);
+ wrapper.find('input#notification-3-started-toggle').props().checked
+ ).toBe(false);
});
+
test('should throw toggle error', async () => {
MockModelAPI.associateNotificationTemplate.mockRejectedValue(
new Error({
@@ -284,27 +247,16 @@ describe('', () => {
},
})
);
- const wrapper = mountWithContexts(
-
- );
- await sleep(0);
+ expect(wrapper.find('ErrorDetail').length).toBe(0);
+ await act(async () => {
+ wrapper.find('Switch#notification-1-started-toggle').prop('onChange')();
+ });
wrapper.update();
-
- expect(
- wrapper.find('NotificationList').state('startedTemplateIds')
- ).toEqual([3]);
- const items = wrapper.find('NotificationListItem');
- items
- .at(0)
- .find('Switch[aria-label="Toggle notification start"]')
- .prop('onChange')();
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
1,
1,
'started'
);
- await sleep(0);
- wrapper.update();
expect(wrapper.find('ErrorDetail').length).toBe(1);
});
});