diff --git a/src/CourseAuthoringPage.jsx b/src/CourseAuthoringPage.jsx
index ae9cfd4f3..1e0dfa62a 100644
--- a/src/CourseAuthoringPage.jsx
+++ b/src/CourseAuthoringPage.jsx
@@ -1,11 +1,15 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Footer from '@edx/frontend-component-footer';
-import { useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import Header from './studio-header/Header';
import { fetchCourseDetail } from './data/thunks';
import { useModel } from './generic/model-store';
+import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
+import { getCourseAppsApiStatus, getLoadingStatus } from './pages-and-resources/data/selectors';
+import { RequestStatus } from './data/constants';
+import Loading from './generic/Loading';
export default function CourseAuthoringPage({ courseId, children }) {
const dispatch = useDispatch();
@@ -19,17 +23,28 @@ export default function CourseAuthoringPage({ courseId, children }) {
const courseNumber = courseDetail ? courseDetail.number : null;
const courseOrg = courseDetail ? courseDetail.org : null;
const courseTitle = courseDetail ? courseDetail.name : courseId;
+ const courseAppsApiStatus = useSelector(getCourseAppsApiStatus);
+ const inProgress = useSelector(getLoadingStatus) === RequestStatus.IN_PROGRESS;
+ if (courseAppsApiStatus === RequestStatus.DENIED) {
+ return (
+
+ );
+ }
+
+ const AppHeader = () => (
+
+ );
return (
<>
-
+ {inProgress ? : AppHeader()}
{children}
-
+ {!inProgress && }
>
);
}
diff --git a/src/CourseAuthoringPage.test.jsx b/src/CourseAuthoringPage.test.jsx
new file mode 100644
index 000000000..26e844353
--- /dev/null
+++ b/src/CourseAuthoringPage.test.jsx
@@ -0,0 +1,63 @@
+import React from 'react';
+
+import { queryByTestId, render } from '@testing-library/react';
+
+import { getConfig, initializeMockApp } from '@edx/frontend-platform';
+import MockAdapter from 'axios-mock-adapter';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { AppProvider } from '@edx/frontend-platform/react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import initializeStore from './store';
+import CourseAuthoringPage from './CourseAuthoringPage';
+import PagesAndResources from './pages-and-resources/PagesAndResources';
+import executeThunk from './utils';
+import { fetchCourseApps } from './pages-and-resources/data/thunks';
+
+const courseId = 'course-v1:edX+TestX+Test_Course';
+let axiosMock;
+let store;
+let container;
+function renderComponent() {
+ const wrapper = render(
+
+
+
+
+
+
+
+ ,
+ );
+ container = wrapper.container;
+}
+
+const mockStore = async () => {
+ const apiBaseUrl = getConfig().STUDIO_BASE_URL;
+ const courseAppsApiUrl = `${apiBaseUrl}/api/course_apps/v1/apps`;
+ axiosMock.onGet(`${courseAppsApiUrl}/${courseId}`).reply(403, {
+ response: { status: 403 },
+ });
+
+ await executeThunk(fetchCourseApps(courseId), store.dispatch);
+};
+describe('DiscussionsSettings', () => {
+ beforeEach(() => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+
+ store = initializeStore();
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ });
+
+ test('renders permission error in case of 403', async () => {
+ await mockStore();
+ renderComponent();
+ expect(queryByTestId(container, 'permissionDeniedAlert')).toBeInTheDocument();
+ });
+});
diff --git a/src/pages-and-resources/PagesAndResources.jsx b/src/pages-and-resources/PagesAndResources.jsx
index 4bb8e00f7..bca2272f3 100644
--- a/src/pages-and-resources/PagesAndResources.jsx
+++ b/src/pages-and-resources/PagesAndResources.jsx
@@ -14,7 +14,9 @@ import ResourceList from './resources/ResourcesList';
import { fetchCourseApps } from './data/thunks';
import { useModels } from '../generic/model-store';
+import { getLoadingStatus } from './data/selectors';
import PagesAndResourcesProvider from './PagesAndResourcesProvider';
+import { RequestStatus } from '../data/constants';
function PagesAndResources({ courseId, intl }) {
const { path, url } = useRouteMatch();
@@ -25,9 +27,12 @@ function PagesAndResources({ courseId, intl }) {
}, [courseId]);
const courseAppIds = useSelector(state => state.pagesAndResources.courseAppIds);
+ const loadingStatus = useSelector(getLoadingStatus);
// Each page here is driven by a course app
const pages = useModels('courseApps', courseAppIds);
-
+ if (loadingStatus === RequestStatus.IN_PROGRESS) {
+ return <>>;
+ }
return (
diff --git a/src/pages-and-resources/data/selectors.js b/src/pages-and-resources/data/selectors.js
index ce02809ad..311550b9a 100644
--- a/src/pages-and-resources/data/selectors.js
+++ b/src/pages-and-resources/data/selectors.js
@@ -2,3 +2,4 @@
export const getLoadingStatus = (status) => status.pagesAndResources.loadingStatus;
export const getSavingStatus = (status) => status.pagesAndResources.savingStatus;
+export const getCourseAppsApiStatus = (status) => status.pagesAndResources.courseAppsApiStatus;
diff --git a/src/pages-and-resources/data/slice.js b/src/pages-and-resources/data/slice.js
index c84533fcb..890a0bdf1 100644
--- a/src/pages-and-resources/data/slice.js
+++ b/src/pages-and-resources/data/slice.js
@@ -8,6 +8,7 @@ const slice = createSlice({
courseAppIds: [],
loadingStatus: RequestStatus.IN_PROGRESS,
savingStatus: RequestStatus.SUCCESSFUL,
+ courseAppsApiStatus: {},
},
reducers: {
fetchCourseAppsSuccess: (state, { payload }) => {
@@ -19,6 +20,9 @@ const slice = createSlice({
updateSavingStatus: (state, { payload }) => {
state.savingStatus = payload.status;
},
+ updateCourseAppsApiStatus: (state, { payload }) => {
+ state.courseAppsApiStatus = payload.status;
+ },
},
});
@@ -26,6 +30,7 @@ export const {
fetchCourseAppsSuccess,
updateLoadingStatus,
updateSavingStatus,
+ updateCourseAppsApiStatus,
} = slice.actions;
export const {
diff --git a/src/pages-and-resources/data/thunks.js b/src/pages-and-resources/data/thunks.js
index 4f19cc14e..46471282b 100644
--- a/src/pages-and-resources/data/thunks.js
+++ b/src/pages-and-resources/data/thunks.js
@@ -8,6 +8,7 @@ import {
fetchCourseAppsSuccess,
updateLoadingStatus,
updateSavingStatus,
+ updateCourseAppsApiStatus,
} from './slice';
/* eslint-disable import/prefer-default-export */
@@ -24,9 +25,10 @@ export function fetchCourseApps(courseId) {
}));
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
} catch (error) {
- // TODO: We need generic error handling in the app for when a request just fails... in other
- // parts of the app (proctored exam settings) we show a nice message and ask the user to
- // reload/try again later.
+ if (error.response && error.response.status === 403) {
+ dispatch(updateCourseAppsApiStatus({ status: RequestStatus.DENIED }));
+ }
+
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
}
};