feat: Added permission alert in pages (#153)
* feat: Added permission alert on pages
This commit is contained in:
@@ -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 (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
}
|
||||
|
||||
const AppHeader = () => (
|
||||
<Header
|
||||
courseNumber={courseNumber}
|
||||
courseOrg={courseOrg}
|
||||
courseTitle={courseTitle}
|
||||
courseId={courseId}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
courseNumber={courseNumber}
|
||||
courseOrg={courseOrg}
|
||||
courseTitle={courseTitle}
|
||||
courseId={courseId}
|
||||
/>
|
||||
{inProgress ? <Loading /> : AppHeader()}
|
||||
{children}
|
||||
<Footer />
|
||||
{!inProgress && <Footer />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
63
src/CourseAuthoringPage.test.jsx
Normal file
63
src/CourseAuthoringPage.test.jsx
Normal file
@@ -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(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<CourseAuthoringPage courseId={courseId}>
|
||||
<PagesAndResources courseId={courseId} />
|
||||
</CourseAuthoringPage>
|
||||
</IntlProvider>
|
||||
</AppProvider>
|
||||
,
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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 (
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<main className="container container-mw-md">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user