feat: [ROLES-41] Permission checks (#718)
* feat: Permission check (#718) This feature allows to fetch the User Permissions and check on every page for the right permission to allow the user to make actions or even to see the content depending on the page and the permission. Co-authored-by: hsinkoff <hsinkoff@2u.com>
This commit is contained in:
@@ -14,6 +14,8 @@ import PermissionDeniedAlert from './generic/PermissionDeniedAlert';
|
||||
import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors';
|
||||
import { RequestStatus } from './data/constants';
|
||||
import Loading from './generic/Loading';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from './generic/data/thunks';
|
||||
import { getUserPermissions } from './generic/data/selectors';
|
||||
|
||||
const AppHeader = ({
|
||||
courseNumber, courseOrg, courseTitle, courseId,
|
||||
@@ -40,9 +42,14 @@ AppHeader.defaultProps = {
|
||||
|
||||
const CourseAuthoringPage = ({ courseId, children }) => {
|
||||
const dispatch = useDispatch();
|
||||
const userPermissions = useSelector(getUserPermissions);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseDetail(courseId));
|
||||
dispatch(fetchUserPermissionsEnabledFlag());
|
||||
if (!userPermissions) {
|
||||
dispatch(fetchUserPermissionsQuery(courseId));
|
||||
}
|
||||
}, [courseId]);
|
||||
|
||||
const courseDetail = useModel('courseDetails', courseId);
|
||||
|
||||
@@ -25,6 +25,9 @@ import validateAdvancedSettingsData from './utils';
|
||||
import messages from './messages';
|
||||
import ModalError from './modal-error/ModalError';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { useUserPermissions } from '../generic/hooks';
|
||||
import { getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||
|
||||
const AdvancedSettings = ({ intl, courseId }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -41,6 +44,13 @@ const AdvancedSettings = ({ intl, courseId }) => {
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||
|
||||
const { checkPermission } = useUserPermissions();
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const viewOnly = checkPermission('view_course_settings');
|
||||
const showPermissionDeniedAlert = userPermissionsEnabled && (
|
||||
!checkPermission('manage_advanced_settings') && !checkPermission('view_course_settings')
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseAppSettings(courseId));
|
||||
dispatch(fetchProctoringExamErrors(courseId));
|
||||
@@ -83,6 +93,11 @@ const AdvancedSettings = ({ intl, courseId }) => {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
if (showPermissionDeniedAlert) {
|
||||
return (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
}
|
||||
if (loadingSettingsStatus === RequestStatus.DENIED) {
|
||||
return (
|
||||
<div className="row justify-content-center m-6">
|
||||
@@ -215,6 +230,7 @@ const AdvancedSettings = ({ intl, courseId }) => {
|
||||
handleBlur={handleSettingBlur}
|
||||
isEditableState={isEditableState}
|
||||
setIsEditableState={setIsEditableState}
|
||||
disableForm={viewOnly}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -3,7 +3,11 @@ import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
render,
|
||||
fireEvent,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import initializeStore from '../store';
|
||||
@@ -13,11 +17,15 @@ import { getCourseAdvancedSettingsApiUrl } from './data/api';
|
||||
import { updateCourseAppSetting } from './data/thunks';
|
||||
import AdvancedSettings from './AdvancedSettings';
|
||||
import messages from './messages';
|
||||
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
const userId = 3;
|
||||
const userPermissionsData = { permissions: ['view_course_settings', 'manage_advanced_settings'] };
|
||||
|
||||
// Mock the TextareaAutosize component
|
||||
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
||||
@@ -43,11 +51,23 @@ const RootWrapper = () => (
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
const permissionsMockStore = async (permissions) => {
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, permissions);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
const permissionDisabledMockStore = async () => {
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: false });
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
describe('<AdvancedSettings />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
userId,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
@@ -58,7 +78,9 @@ describe('<AdvancedSettings />', () => {
|
||||
axiosMock
|
||||
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
|
||||
.reply(200, advancedSettingsMock);
|
||||
permissionsMockStore(userPermissionsData);
|
||||
});
|
||||
|
||||
it('should render without errors', async () => {
|
||||
const { getByText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
@@ -161,4 +183,29 @@ describe('<AdvancedSettings />', () => {
|
||||
await executeThunk(updateCourseAppSetting(courseId, [3, 2, 1]), store.dispatch);
|
||||
expect(getByText('Your policy changes have been saved.')).toBeInTheDocument();
|
||||
});
|
||||
it('should shows the PermissionDeniedAlert when there are not the right user permissions', async () => {
|
||||
const permissionsData = { permissions: ['view'] };
|
||||
await permissionsMockStore(permissionsData);
|
||||
|
||||
const { queryByText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
const permissionDeniedAlert = queryByText('You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.');
|
||||
expect(permissionDeniedAlert).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should not show the PermissionDeniedAlert when the User Permissions Flag is not enabled', async () => {
|
||||
await permissionDisabledMockStore();
|
||||
|
||||
const { queryByText } = render(<RootWrapper />);
|
||||
const permissionDeniedAlert = queryByText('You are not authorized to view this page. If you feel you should have access, please reach out to your course team admin to be given access.');
|
||||
expect(permissionDeniedAlert).not.toBeInTheDocument();
|
||||
});
|
||||
it('should be view only if the permission is set for viewOnly', async () => {
|
||||
const permissions = { permissions: ['view_course_settings'] };
|
||||
await permissionsMockStore(permissions);
|
||||
const { getByLabelText } = render(<RootWrapper />);
|
||||
await waitFor(() => {
|
||||
expect(getByLabelText('Advanced Module List')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ const SettingCard = ({
|
||||
setIsEditableState,
|
||||
// injected
|
||||
intl,
|
||||
disableForm,
|
||||
}) => {
|
||||
const { deprecated, help, displayName } = settingData;
|
||||
const initialValue = JSON.stringify(settingData.value, null, 4);
|
||||
@@ -100,6 +101,7 @@ const SettingCard = ({
|
||||
onChange={handleSettingChange}
|
||||
aria-label={displayName}
|
||||
onBlur={handleCardBlur}
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Card.Section>
|
||||
@@ -135,6 +137,7 @@ SettingCard.propTypes = {
|
||||
saveSettingsPrompt: PropTypes.bool.isRequired,
|
||||
isEditableState: PropTypes.bool.isRequired,
|
||||
setIsEditableState: PropTypes.func.isRequired,
|
||||
disableForm: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SettingCard);
|
||||
|
||||
@@ -18,12 +18,14 @@ import AddUserForm from './add-user-form/AddUserForm';
|
||||
import AddTeamMember from './add-team-member/AddTeamMember';
|
||||
import CourseTeamMember from './course-team-member/CourseTeamMember';
|
||||
import InfoModal from './info-modal/InfoModal';
|
||||
import { useCourseTeam, useUserPermissions } from './hooks';
|
||||
import { useCourseTeam } from './hooks';
|
||||
import { useUserPermissions } from '../generic/hooks';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
|
||||
const CourseTeam = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
||||
|
||||
const {
|
||||
@@ -55,10 +57,10 @@ const CourseTeam = ({ courseId }) => {
|
||||
} = useCourseTeam({ intl, courseId });
|
||||
|
||||
const {
|
||||
hasPermissions,
|
||||
checkPermission,
|
||||
} = useUserPermissions();
|
||||
|
||||
const hasManageAllUsersPerm = hasPermissions('manage_all_users');
|
||||
const hasManageAllUsersPerm = checkPermission('manage_all_users');
|
||||
|
||||
if (isLoading) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
|
||||
@@ -15,6 +15,7 @@ import initializeStore from '../store';
|
||||
import { courseTeamMock, courseTeamWithOneUser, courseTeamWithoutUsers } from './__mocks__';
|
||||
import { getCourseTeamApiUrl, updateCourseTeamUserApiUrl } from './data/api';
|
||||
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||
import CourseTeam from './CourseTeam';
|
||||
import messages from './messages';
|
||||
import { USER_ROLES } from '../constants';
|
||||
@@ -180,7 +181,7 @@ describe('<CourseTeam />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('not displays "Add New Member" and AddTeamMember component when isAllowActions or hasManageAllUsersPerm is false', async () => {
|
||||
it('not displays "Add New Member" and AddTeamMember component when isAllowActions is false and hasManageAllUsersPerm is false', async () => {
|
||||
cleanup();
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
@@ -190,13 +191,37 @@ describe('<CourseTeam />', () => {
|
||||
});
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||
.reply(200, { enabled: false });
|
||||
.reply(200, { enabled: true });
|
||||
|
||||
const { queryByRole, queryByTestId } = render(<RootWrapper />);
|
||||
const { queryByRole, queryByText } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByRole('button', { name: messages.addNewMemberButton.defaultMessage })).not.toBeInTheDocument();
|
||||
expect(queryByTestId('add-team-member')).not.toBeInTheDocument();
|
||||
expect(queryByText('add-team-member')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays "Add New Member" and AddTeamMember component when hasManageAllUsersPerm is true', async () => {
|
||||
cleanup();
|
||||
axiosMock
|
||||
.onGet(getCourseTeamApiUrl(courseId))
|
||||
.reply(200, {
|
||||
...courseTeamWithOneUser,
|
||||
allowActions: false,
|
||||
});
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsEnabledFlagUrl)
|
||||
.reply(200, { enabled: true });
|
||||
axiosMock
|
||||
.onGet(getUserPermissionsUrl(courseId, userId))
|
||||
.reply(200, userPermissionsData);
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByRole('button', { name: messages.addNewMemberButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import { useToggle } from '@edx/paragon';
|
||||
import { USER_ROLES } from '../constants';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { useModel } from '../generic/model-store';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||
import { getUserPermissions, getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||
import {
|
||||
changeRoleTeamUserQuery,
|
||||
createCourseTeamQuery,
|
||||
@@ -98,8 +96,6 @@ const useCourseTeam = ({ courseId }) => {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseTeamQuery(courseId));
|
||||
dispatch(fetchUserPermissionsEnabledFlag());
|
||||
dispatch(fetchUserPermissionsQuery(courseId));
|
||||
}, [courseId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -139,20 +135,5 @@ const useCourseTeam = ({ courseId }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const useUserPermissions = () => {
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const userPermissions = useSelector(getUserPermissions);
|
||||
const hasPermissions = (checkPermissions) => {
|
||||
if (userPermissionsEnabled) {
|
||||
return userPermissions?.includes(checkPermissions);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
hasPermissions,
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { useCourseTeam, useUserPermissions };
|
||||
export { useCourseTeam };
|
||||
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import { Add as AddIcon } from '@edx/paragon/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { useModel } from '../generic/model-store';
|
||||
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
|
||||
import ProcessingNotification from '../generic/processing-notification';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
||||
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import CourseHandouts from './course-handouts/CourseHandouts';
|
||||
import CourseUpdate from './course-update/CourseUpdate';
|
||||
@@ -25,6 +25,8 @@ import { useCourseUpdates } from './hooks';
|
||||
import { getLoadingStatuses, getSavingStatuses } from './data/selectors';
|
||||
import { matchesAnyStatus } from './utils';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||
import { useUserPermissions } from '../generic/hooks';
|
||||
|
||||
const CourseUpdates = ({ courseId }) => {
|
||||
const intl = useIntl();
|
||||
@@ -60,7 +62,15 @@ const CourseUpdates = ({ courseId }) => {
|
||||
const anyStatusFailed = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.FAILED);
|
||||
const anyStatusInProgress = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.IN_PROGRESS);
|
||||
const anyStatusPending = matchesAnyStatus({ ...loadingStatuses, ...savingStatuses }, RequestStatus.PENDING);
|
||||
const { checkPermission } = useUserPermissions();
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const showPermissionDeniedAlert = userPermissionsEnabled && !checkPermission('manage_content');
|
||||
|
||||
if (showPermissionDeniedAlert) {
|
||||
return (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Container size="xl" className="px-4">
|
||||
|
||||
@@ -22,11 +22,16 @@ import { executeThunk } from '../utils';
|
||||
import { courseUpdatesMock, courseHandoutsMock } from './__mocks__';
|
||||
import CourseUpdates from './CourseUpdates';
|
||||
import messages from './messages';
|
||||
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../generic/data/api';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
const courseId = '123';
|
||||
const userId = 3;
|
||||
const userPermissionsData = { permissions: ['manage_content'] };
|
||||
const wrongUserPermissionsData = { permissions: ['wrong_permission'] };
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
@@ -61,7 +66,7 @@ const RootWrapper = () => (
|
||||
);
|
||||
|
||||
describe('<CourseUpdates />', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
@@ -79,6 +84,7 @@ describe('<CourseUpdates />', () => {
|
||||
axiosMock
|
||||
.onGet(getCourseHandoutApiUrl(courseId))
|
||||
.reply(200, courseHandoutsMock);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: false });
|
||||
});
|
||||
|
||||
it('render CourseUpdates component correctly', async () => {
|
||||
@@ -162,6 +168,26 @@ describe('<CourseUpdates />', () => {
|
||||
expect(queryByText(courseHandoutsMock.data)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should shows PermissionDeniedAlert if there are no right User Permissions', async () => {
|
||||
const { getByTestId } = render(<RootWrapper />);
|
||||
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, wrongUserPermissionsData);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
expect(getByTestId('permissionDeniedAlert')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should not show PermissionDeniedAlert if User Permissions are the correct ones', async () => {
|
||||
const { queryByTestId } = render(<RootWrapper />);
|
||||
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
expect(queryByTestId('permissionDeniedAlert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Add new update form is visible after clicking "New update" button', async () => {
|
||||
const { getByText, getByRole, getAllByTestId } = render(<RootWrapper />);
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ import {
|
||||
import { getFileSizeToClosestByte } from '../../utils';
|
||||
import FileThumbnail from './FileThumbnail';
|
||||
import FileInfoModalSidebar from './FileInfoModalSidebar';
|
||||
import { useUserPermissions } from '../../generic/hooks';
|
||||
import { getUserPermissionsEnabled } from '../../generic/data/selectors';
|
||||
import PermissionDeniedAlert from '../../generic/PermissionDeniedAlert';
|
||||
|
||||
const FilesPage = ({
|
||||
courseId,
|
||||
@@ -39,6 +42,9 @@ const FilesPage = ({
|
||||
const dispatch = useDispatch();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
||||
const { checkPermission } = useUserPermissions();
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const showPermissionDeniedAlert = userPermissionsEnabled && !checkPermission('manage_content');
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAssets(courseId));
|
||||
@@ -160,6 +166,11 @@ const FilesPage = ({
|
||||
{ ...accessColumn },
|
||||
];
|
||||
|
||||
if (showPermissionDeniedAlert) {
|
||||
return (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
}
|
||||
if (loadingStatus === RequestStatus.DENIED) {
|
||||
return (
|
||||
<div data-testid="under-construction-placeholder" className="row justify-contnt-center m-6">
|
||||
|
||||
@@ -39,10 +39,16 @@ import {
|
||||
} from './data/thunks';
|
||||
import { getAssetsUrl } from './data/api';
|
||||
import messages from '../generic/messages';
|
||||
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../../generic/data/api';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../../generic/data/thunks';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
let file;
|
||||
const userId = 3;
|
||||
const wrongUserPermissionsData = { permissions: ['wrong_permission'] };
|
||||
const userPermissionsData = { permissions: ['manage_content'] };
|
||||
|
||||
ReactDOM.createPortal = jest.fn(node => node);
|
||||
jest.mock('file-saver');
|
||||
|
||||
@@ -68,6 +74,8 @@ const mockStore = async (
|
||||
}
|
||||
renderComponent();
|
||||
await executeThunk(fetchAssets(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
const emptyMockStore = async (status) => {
|
||||
@@ -75,6 +83,27 @@ const emptyMockStore = async (status) => {
|
||||
axiosMock.onGet(fetchAssetsUrl).reply(getStatusValue(status), generateEmptyApiResponse());
|
||||
renderComponent();
|
||||
await executeThunk(fetchAssets(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
const wrongUserPermissionsMockStore = async () => {
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, wrongUserPermissionsData);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
const disabledUserPermissionsFlagMockStore = async () => {
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: false });
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
const userPermissionsMockStore = async () => {
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
describe('FilesAndUploads', () => {
|
||||
@@ -82,7 +111,7 @@ describe('FilesAndUploads', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
userId,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
@@ -100,6 +129,21 @@ describe('FilesAndUploads', () => {
|
||||
file = new File(['(⌐□_□)'], 'download.png', { type: 'image/png' });
|
||||
});
|
||||
|
||||
it('should shows PermissionDeniedAlert if there are no right User Permissions', async () => {
|
||||
renderComponent();
|
||||
await wrongUserPermissionsMockStore();
|
||||
expect(screen.getByTestId('permissionDeniedAlert')).toBeVisible();
|
||||
});
|
||||
it('should not show PermissionDeniedAlert if User Permissions Flag is not enabled', async () => {
|
||||
renderComponent();
|
||||
await disabledUserPermissionsFlagMockStore();
|
||||
expect(screen.queryByText('permissionDeniedAlert')).not.toBeInTheDocument();
|
||||
});
|
||||
it('should not show PermissionDeniedAlert if User Permissions Flag is enabled and permissions are correct', async () => {
|
||||
renderComponent();
|
||||
await userPermissionsMockStore();
|
||||
expect(screen.queryByText('permissionDeniedAlert')).not.toBeInTheDocument();
|
||||
});
|
||||
it('should return placeholder component', async () => {
|
||||
await mockStore(RequestStatus.DENIED);
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ import VideoThumbnail from './VideoThumbnail';
|
||||
import { getFormattedDuration, resampleFile } from './data/utils';
|
||||
import FILES_AND_UPLOAD_TYPE_FILTERS from '../generic/constants';
|
||||
import VideoInfoModalSidebar from './info-sidebar';
|
||||
import { useUserPermissions } from '../../generic/hooks';
|
||||
import { getUserPermissionsEnabled } from '../../generic/data/selectors';
|
||||
import PermissionDeniedAlert from '../../generic/PermissionDeniedAlert';
|
||||
|
||||
const VideosPage = ({
|
||||
courseId,
|
||||
@@ -53,6 +56,9 @@ const VideosPage = ({
|
||||
const [isTranscriptSettingsOpen, openTranscriptSettings, closeTranscriptSettings] = useToggle(false);
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.heading));
|
||||
const { checkPermission } = useUserPermissions();
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const showPermissionDeniedAlert = userPermissionsEnabled && !checkPermission('manage_content');
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchVideos(courseId));
|
||||
@@ -179,6 +185,11 @@ const VideosPage = ({
|
||||
{ ...processingStatusColumn },
|
||||
];
|
||||
|
||||
if (showPermissionDeniedAlert) {
|
||||
return (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
}
|
||||
if (loadingStatus === RequestStatus.DENIED) {
|
||||
return (
|
||||
<div data-testid="under-construction-placeholder" className="row justify-contnt-center m-6">
|
||||
|
||||
@@ -39,10 +39,15 @@ import {
|
||||
import { getVideosUrl, getCourseVideosApiUrl, getApiBaseUrl } from './data/api';
|
||||
import videoMessages from './messages';
|
||||
import messages from '../generic/messages';
|
||||
import { getUserPermissionsUrl, getUserPermissionsEnabledFlagUrl } from '../../generic/data/api';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../../generic/data/thunks';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
let file;
|
||||
const userId = 3;
|
||||
const wrongUserPermissionsData = { permissions: ['wrong_permission'] };
|
||||
const userPermissionsData = { permissions: ['manage_content'] };
|
||||
jest.mock('file-saver');
|
||||
|
||||
const renderComponent = () => {
|
||||
@@ -55,9 +60,7 @@ const renderComponent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const mockStore = async (
|
||||
status,
|
||||
) => {
|
||||
const mockStore = async (status) => {
|
||||
const fetchVideosUrl = getVideosUrl(courseId);
|
||||
const videosData = generateFetchVideosApiResponse();
|
||||
axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), videosData);
|
||||
@@ -68,6 +71,8 @@ const mockStore = async (
|
||||
|
||||
renderComponent();
|
||||
await executeThunk(fetchVideos(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
const emptyMockStore = async (status) => {
|
||||
@@ -75,6 +80,27 @@ const emptyMockStore = async (status) => {
|
||||
axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), generateEmptyApiResponse());
|
||||
renderComponent();
|
||||
await executeThunk(fetchVideos(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
const wrongUserPermissionsMockStore = async () => {
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, wrongUserPermissionsData);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
const disabledUserPermissionsFlagMockStore = async () => {
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: false });
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
const userPermissionsMockStore = async () => {
|
||||
axiosMock.onGet(getUserPermissionsUrl(courseId, userId)).reply(200, userPermissionsData);
|
||||
axiosMock.onGet(getUserPermissionsEnabledFlagUrl).reply(200, { enabled: true });
|
||||
await executeThunk(fetchUserPermissionsQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchUserPermissionsEnabledFlag(), store.dispatch);
|
||||
};
|
||||
|
||||
describe('Videos page', () => {
|
||||
@@ -82,7 +108,7 @@ describe('Videos page', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
userId,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
@@ -146,13 +172,31 @@ describe('Videos page', () => {
|
||||
|
||||
expect(screen.getByTestId('files-data-table')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should shows PermissionDeniedAlert if there are no right User Permissions', async () => {
|
||||
renderComponent();
|
||||
await wrongUserPermissionsMockStore();
|
||||
expect(screen.getByTestId('permissionDeniedAlert')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should not show PermissionDeniedAlert if User Permissions Flag is not enabled', async () => {
|
||||
renderComponent();
|
||||
await disabledUserPermissionsFlagMockStore();
|
||||
expect(screen.queryByText('permissionDeniedAlert')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show PermissionDeniedAlert if User Permissions Flag is enabled and permission is correct', async () => {
|
||||
renderComponent();
|
||||
await userPermissionsMockStore();
|
||||
expect(screen.queryByText('permissionDeniedAlert')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid videos', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
userId,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
|
||||
@@ -16,6 +16,8 @@ export const initialState = {
|
||||
},
|
||||
organizations: ['krisEdx', 'krisEd', 'DeveloperInc', 'importMit', 'testX', 'edX', 'developerInb'],
|
||||
savingStatus: '',
|
||||
userPermissions: [],
|
||||
userPermissionsEnabled: false,
|
||||
},
|
||||
studioHome: {
|
||||
loadingStatuses: {
|
||||
|
||||
@@ -48,7 +48,6 @@ export async function createOrRerunCourse(courseData) {
|
||||
/**
|
||||
* Get user course roles permissions.
|
||||
* @param {string} courseId
|
||||
* @param {string} userId
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function getUserPermissions(courseId) {
|
||||
|
||||
@@ -27,8 +27,6 @@ describe('generic api calls', () => {
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
userPermissions: [],
|
||||
userPermissionsEnabled: false,
|
||||
});
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
21
src/generic/hooks.jsx
Normal file
21
src/generic/hooks.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getUserPermissions, getUserPermissionsEnabled } from './data/selectors';
|
||||
|
||||
const useUserPermissions = () => {
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const userPermissions = useSelector(getUserPermissions);
|
||||
|
||||
const checkPermission = (permission) => {
|
||||
if (!userPermissionsEnabled || !Array.isArray(userPermissions)) {
|
||||
return false;
|
||||
}
|
||||
return userPermissions.includes(permission);
|
||||
};
|
||||
|
||||
return {
|
||||
checkPermission,
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { useUserPermissions };
|
||||
@@ -1,10 +1,14 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { StudioHeader } from '@edx/frontend-component-header';
|
||||
import { getContentMenuItems, getSettingMenuItems, getToolsMenuItems } from './utils';
|
||||
import { useUserPermissions } from '../generic/hooks';
|
||||
import { getUserPermissions, getUserPermissionsEnabled } from '../generic/data/selectors';
|
||||
import { fetchUserPermissionsQuery, fetchUserPermissionsEnabledFlag } from '../generic/data/thunks';
|
||||
import messages from './messages';
|
||||
|
||||
const Header = ({
|
||||
@@ -16,24 +20,63 @@ const Header = ({
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { checkPermission } = useUserPermissions();
|
||||
const userPermissions = useSelector(getUserPermissions);
|
||||
const userPermissionsEnabled = useSelector(getUserPermissionsEnabled);
|
||||
const hasContentPermissions = !userPermissionsEnabled || (userPermissionsEnabled && checkPermission('manage_content'));
|
||||
const hasSettingsPermissions = !userPermissionsEnabled
|
||||
|| (userPermissionsEnabled && (checkPermission('manage_advanced_settings') || checkPermission('view_course_settings')));
|
||||
const hasToolsPermissions = !userPermissionsEnabled
|
||||
|| (userPermissionsEnabled && (checkPermission('manage_course_settings') || checkPermission('view_course_settings')));
|
||||
const studioBaseUrl = getConfig().STUDIO_BASE_URL;
|
||||
const mainMenuDropdowns = [
|
||||
{
|
||||
id: `${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`,
|
||||
buttonTitle: intl.formatMessage(messages['header.links.content']),
|
||||
items: getContentMenuItems({ studioBaseUrl, courseId, intl }),
|
||||
},
|
||||
const contentMenu = getContentMenuItems({
|
||||
studioBaseUrl,
|
||||
courseId,
|
||||
intl,
|
||||
hasContentPermissions,
|
||||
});
|
||||
const mainMenuDropdowns = [];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUserPermissionsEnabledFlag());
|
||||
if (!userPermissions) {
|
||||
dispatch(fetchUserPermissionsQuery(courseId));
|
||||
}
|
||||
}, [courseId]);
|
||||
|
||||
if (contentMenu.length > 0) {
|
||||
mainMenuDropdowns.push(
|
||||
{
|
||||
id: `${intl.formatMessage(messages['header.links.content'])}-dropdown-menu`,
|
||||
buttonTitle: intl.formatMessage(messages['header.links.content']),
|
||||
items: contentMenu,
|
||||
},
|
||||
);
|
||||
}
|
||||
mainMenuDropdowns.push(
|
||||
{
|
||||
id: `${intl.formatMessage(messages['header.links.settings'])}-dropdown-menu`,
|
||||
buttonTitle: intl.formatMessage(messages['header.links.settings']),
|
||||
items: getSettingMenuItems({ studioBaseUrl, courseId, intl }),
|
||||
items: getSettingMenuItems({
|
||||
studioBaseUrl,
|
||||
courseId,
|
||||
intl,
|
||||
hasSettingsPermissions,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: `${intl.formatMessage(messages['header.links.tools'])}-dropdown-menu`,
|
||||
buttonTitle: intl.formatMessage(messages['header.links.tools']),
|
||||
items: getToolsMenuItems({ studioBaseUrl, courseId, intl }),
|
||||
items: getToolsMenuItems({
|
||||
studioBaseUrl,
|
||||
courseId,
|
||||
intl,
|
||||
hasToolsPermissions,
|
||||
}),
|
||||
},
|
||||
];
|
||||
);
|
||||
|
||||
const outlineLink = `${studioBaseUrl}/course/${courseId}`;
|
||||
return (
|
||||
<StudioHeader
|
||||
|
||||
@@ -2,72 +2,112 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { getPagePath } from '../utils';
|
||||
import messages from './messages';
|
||||
|
||||
export const getContentMenuItems = ({ studioBaseUrl, courseId, intl }) => {
|
||||
const items = [
|
||||
{
|
||||
href: `${studioBaseUrl}/course/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.outline']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/course_info/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.updates']),
|
||||
},
|
||||
{
|
||||
href: getPagePath(courseId, 'true', 'tabs'),
|
||||
title: intl.formatMessage(messages['header.links.pages']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/assets/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.filesAndUploads']),
|
||||
},
|
||||
];
|
||||
export const getContentMenuItems = ({
|
||||
studioBaseUrl,
|
||||
courseId,
|
||||
intl,
|
||||
hasContentPermissions,
|
||||
}) => {
|
||||
const items = [];
|
||||
|
||||
if (hasContentPermissions) {
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/course/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.outline']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/course_info/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.updates']),
|
||||
},
|
||||
{
|
||||
href: getPagePath(courseId, 'true', 'tabs'),
|
||||
title: intl.formatMessage(messages['header.links.pages']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/assets/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.filesAndUploads']),
|
||||
},
|
||||
);
|
||||
}
|
||||
if (getConfig().ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN === 'true') {
|
||||
items.push({
|
||||
href: `${studioBaseUrl}/videos/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.videoUploads']),
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export const getSettingMenuItems = ({ studioBaseUrl, courseId, intl }) => ([
|
||||
{
|
||||
href: `${studioBaseUrl}/settings/details/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.scheduleAndDetails']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/settings/grading/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.grading']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/course_team/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.courseTeam']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/group_configurations/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.groupConfigurations']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/settings/advanced/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.advancedSettings']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/certificates/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.certificates']),
|
||||
},
|
||||
]);
|
||||
export const getSettingMenuItems = ({
|
||||
studioBaseUrl,
|
||||
courseId,
|
||||
intl,
|
||||
hasSettingsPermissions,
|
||||
}) => {
|
||||
const items = [];
|
||||
|
||||
export const getToolsMenuItems = ({ studioBaseUrl, courseId, intl }) => ([
|
||||
{
|
||||
href: `${studioBaseUrl}/import/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.import']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/export/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.export']),
|
||||
}, {
|
||||
href: `${studioBaseUrl}/checklists/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.checklists']),
|
||||
},
|
||||
]);
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/settings/details/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.scheduleAndDetails']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/settings/grading/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.grading']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/course_team/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.courseTeam']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/group_configurations/course-v1:${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.groupConfigurations']),
|
||||
},
|
||||
);
|
||||
if (hasSettingsPermissions) {
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/settings/advanced/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.advancedSettings']),
|
||||
},
|
||||
);
|
||||
}
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/certificates/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.certificates']),
|
||||
},
|
||||
);
|
||||
return items;
|
||||
};
|
||||
|
||||
export const getToolsMenuItems = ({
|
||||
studioBaseUrl,
|
||||
courseId,
|
||||
intl,
|
||||
hasToolsPermissions,
|
||||
}) => {
|
||||
const items = [];
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/import/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.import']),
|
||||
},
|
||||
{
|
||||
href: `${studioBaseUrl}/export/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.export']),
|
||||
},
|
||||
);
|
||||
if (hasToolsPermissions) {
|
||||
items.push(
|
||||
{
|
||||
href: `${studioBaseUrl}/checklists/${courseId}`,
|
||||
title: intl.formatMessage(messages['header.links.checklists']),
|
||||
},
|
||||
);
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||
import { getContentMenuItems } from './utils';
|
||||
import { getContentMenuItems, getSettingMenuItems, getToolsMenuItems } from './utils';
|
||||
|
||||
const props = {
|
||||
const baseProps = {
|
||||
studioBaseUrl: 'UrLSTuiO',
|
||||
courseId: '123',
|
||||
intl: {
|
||||
formatMessage: jest.fn(),
|
||||
},
|
||||
};
|
||||
const contentProps = { ...baseProps, hasContentPermissions: true };
|
||||
const settingProps = { ...baseProps, hasSettingsPermissions: true };
|
||||
const toolsProps = { ...baseProps, hasToolsPermissions: true };
|
||||
|
||||
describe('header utils', () => {
|
||||
describe('getContentMenuItems', () => {
|
||||
@@ -27,5 +30,30 @@ describe('header utils', () => {
|
||||
const actualItems = getContentMenuItems(props);
|
||||
expect(actualItems).toHaveLength(4);
|
||||
});
|
||||
it('should include only Video Uploads option', () => {
|
||||
process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = 'true';
|
||||
const actualItems = getContentMenuItems({ ...baseProps, hasContentPermissions: false });
|
||||
expect(actualItems).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
describe('getSettingMenuItems', async () => {
|
||||
it('should include all options', () => {
|
||||
const actualItems = getSettingMenuItems(settingProps);
|
||||
expect(actualItems).toHaveLength(6);
|
||||
});
|
||||
it('should not include Advanced Settings option', () => {
|
||||
const actualItems = getSettingMenuItems({ ...baseProps, hasSettingsPermissions: false });
|
||||
expect(actualItems).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
describe('getToolsMenuItems', async () => {
|
||||
it('should include all options', () => {
|
||||
const actualItems = getToolsMenuItems(toolsProps);
|
||||
expect(actualItems).toHaveLength(3);
|
||||
});
|
||||
it('should not include Checklist option', () => {
|
||||
const actualItems = getToolsMenuItems({ ...baseProps, hasToolsPermissions: false });
|
||||
expect(actualItems).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import { RequestStatus } from '../data/constants';
|
||||
import SettingsComponent from './SettingsComponent';
|
||||
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
|
||||
import getPageHeadTitle from '../generic/utils';
|
||||
import { useUserPermissions } from '../generic/hooks';
|
||||
|
||||
const PagesAndResources = ({ courseId, intl }) => {
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
@@ -73,12 +74,14 @@ const PagesAndResources = ({ courseId, intl }) => {
|
||||
contentPermissionsPages.push(page);
|
||||
}
|
||||
|
||||
const { checkPermission } = useUserPermissions();
|
||||
|
||||
if (loadingStatus === RequestStatus.IN_PROGRESS) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (courseAppsApiStatus === RequestStatus.DENIED) {
|
||||
if (courseAppsApiStatus === RequestStatus.DENIED || !checkPermission('manage_content')) {
|
||||
return (
|
||||
<PermissionDeniedAlert />
|
||||
);
|
||||
|
||||
@@ -159,6 +159,7 @@ const StudioHome = ({ intl }) => {
|
||||
<SubHeader
|
||||
title={intl.formatMessage(messages.headingTitle, { studioShortName: studioShortName || 'Studio' })}
|
||||
headerActions={headerButtons}
|
||||
key={studioShortName}
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
Reference in New Issue
Block a user