refactor: Migrate advancedSettings from the redux store to React Query (#2893)
This commit is contained in:
@@ -1,192 +0,0 @@
|
|||||||
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
|
||||||
import { useUserPermissions } from '@src/authz/data/apiHooks';
|
|
||||||
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
|
|
||||||
import {
|
|
||||||
render as baseRender,
|
|
||||||
fireEvent,
|
|
||||||
initializeMocks,
|
|
||||||
waitFor,
|
|
||||||
} from '../testUtils';
|
|
||||||
import { executeThunk } from '../utils';
|
|
||||||
import { advancedSettingsMock } from './__mocks__';
|
|
||||||
import { getCourseAdvancedSettingsApiUrl } from './data/api';
|
|
||||||
import { updateCourseAppSetting } from './data/thunks';
|
|
||||||
import AdvancedSettings from './AdvancedSettings';
|
|
||||||
import messages from './messages';
|
|
||||||
|
|
||||||
let axiosMock;
|
|
||||||
let store;
|
|
||||||
const mockPathname = '/foo-bar';
|
|
||||||
const courseId = '123';
|
|
||||||
|
|
||||||
// Mock the TextareaAutosize component
|
|
||||||
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
|
||||||
<textarea
|
|
||||||
{...props}
|
|
||||||
onFocus={() => { }}
|
|
||||||
onBlur={() => { }}
|
|
||||||
/>
|
|
||||||
)));
|
|
||||||
|
|
||||||
jest.mock('@src/authz/data/apiHooks', () => ({
|
|
||||||
useUserPermissions: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const render = () => baseRender(
|
|
||||||
<CourseAuthoringProvider courseId={courseId}>
|
|
||||||
<AdvancedSettings />
|
|
||||||
</CourseAuthoringProvider>,
|
|
||||||
{ path: mockPathname },
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('<AdvancedSettings />', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const mocks = initializeMocks();
|
|
||||||
store = mocks.reduxStore;
|
|
||||||
axiosMock = mocks.axiosMock;
|
|
||||||
axiosMock
|
|
||||||
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
|
|
||||||
.reply(200, advancedSettingsMock);
|
|
||||||
|
|
||||||
jest.mocked(useUserPermissions).mockReturnValue({
|
|
||||||
isLoading: false,
|
|
||||||
data: { canManageAdvancedSettings: true },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should render without errors', async () => {
|
|
||||||
const { getByText } = render();
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
|
||||||
const advancedSettingsElement = getByText(messages.headingTitle.defaultMessage, {
|
|
||||||
selector: 'h2.sub-header-title',
|
|
||||||
});
|
|
||||||
expect(advancedSettingsElement).toBeInTheDocument();
|
|
||||||
expect(getByText(messages.policy.defaultMessage)).toBeInTheDocument();
|
|
||||||
expect(getByText(/Do not modify these policies unless you are familiar with their purpose./i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should render setting element', async () => {
|
|
||||||
const { getByText, queryByText } = render();
|
|
||||||
await waitFor(() => {
|
|
||||||
const advancedModuleListTitle = getByText(/Advanced Module List/i);
|
|
||||||
expect(advancedModuleListTitle).toBeInTheDocument();
|
|
||||||
expect(queryByText('Certificate web/html view enabled')).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should change to onСhange', async () => {
|
|
||||||
const { getByLabelText } = render();
|
|
||||||
await waitFor(() => {
|
|
||||||
const textarea = getByLabelText(/Advanced Module List/i);
|
|
||||||
expect(textarea).toBeInTheDocument();
|
|
||||||
fireEvent.change(textarea, { target: { value: '[1, 2, 3]' } });
|
|
||||||
expect(textarea.value).toBe('[1, 2, 3]');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should display a warning alert', async () => {
|
|
||||||
const { getByLabelText, getByText } = render();
|
|
||||||
await waitFor(() => {
|
|
||||||
const textarea = getByLabelText(/Advanced Module List/i);
|
|
||||||
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
|
|
||||||
expect(getByText(messages.buttonCancelText.defaultMessage)).toBeInTheDocument();
|
|
||||||
expect(getByText(messages.buttonSaveText.defaultMessage)).toBeInTheDocument();
|
|
||||||
expect(getByText(messages.alertWarning.defaultMessage)).toBeInTheDocument();
|
|
||||||
expect(getByText(messages.alertWarningDescriptions.defaultMessage)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should display a tooltip on clicking on the icon', async () => {
|
|
||||||
const { getByLabelText, getByText } = render();
|
|
||||||
await waitFor(() => {
|
|
||||||
const button = getByLabelText(/Show help text/i);
|
|
||||||
fireEvent.click(button);
|
|
||||||
expect(getByText(/Enter the names of the advanced modules to use in your course./i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should change deprecated button text ', async () => {
|
|
||||||
const { getByText } = render();
|
|
||||||
await waitFor(() => {
|
|
||||||
const showDeprecatedItemsBtn = getByText(/Show Deprecated Settings/i);
|
|
||||||
expect(showDeprecatedItemsBtn).toBeInTheDocument();
|
|
||||||
fireEvent.click(showDeprecatedItemsBtn);
|
|
||||||
expect(getByText(/Hide Deprecated Settings/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
expect(getByText('Certificate web/html view enabled')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
it('should reset to default value on click on Cancel button', async () => {
|
|
||||||
const { getByLabelText, getByText } = render();
|
|
||||||
let textarea;
|
|
||||||
await waitFor(() => {
|
|
||||||
textarea = getByLabelText(/Advanced Module List/i);
|
|
||||||
});
|
|
||||||
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
|
|
||||||
expect(textarea.value).toBe('[3, 2, 1]');
|
|
||||||
fireEvent.click(getByText(messages.buttonCancelText.defaultMessage));
|
|
||||||
expect(textarea.value).toBe('[]');
|
|
||||||
});
|
|
||||||
it('should update the textarea value and display the updated value after clicking "Change manually"', async () => {
|
|
||||||
const { getByLabelText, getByText } = render();
|
|
||||||
let textarea;
|
|
||||||
await waitFor(() => {
|
|
||||||
textarea = getByLabelText(/Advanced Module List/i);
|
|
||||||
});
|
|
||||||
fireEvent.change(textarea, { target: { value: '[3, 2, 1,' } });
|
|
||||||
expect(textarea.value).toBe('[3, 2, 1,');
|
|
||||||
fireEvent.click(getByText('Save changes'));
|
|
||||||
fireEvent.click(getByText('Change manually'));
|
|
||||||
expect(textarea.value).toBe('[3, 2, 1,');
|
|
||||||
});
|
|
||||||
it('should show success alert after save', async () => {
|
|
||||||
const { getByLabelText, getByText } = render();
|
|
||||||
let textarea;
|
|
||||||
await waitFor(() => {
|
|
||||||
textarea = getByLabelText(/Advanced Module List/i);
|
|
||||||
});
|
|
||||||
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
|
|
||||||
expect(textarea.value).toBe('[3, 2, 1]');
|
|
||||||
axiosMock
|
|
||||||
.onPatch(`${getCourseAdvancedSettingsApiUrl(courseId)}`)
|
|
||||||
.reply(200, {
|
|
||||||
...advancedSettingsMock,
|
|
||||||
advancedModules: {
|
|
||||||
...advancedSettingsMock.advancedModules,
|
|
||||||
value: [3, 2, 1],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
fireEvent.click(getByText('Save changes'));
|
|
||||||
await executeThunk(updateCourseAppSetting(courseId, [3, 2, 1]), store.dispatch);
|
|
||||||
expect(getByText('Your policy changes have been saved.')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render without errors when authz.enable_course_authoring flag is enabled and the user is authorized', async () => {
|
|
||||||
// Mock feature flag
|
|
||||||
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
|
||||||
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
|
|
||||||
jest.mocked(useUserPermissions).mockReturnValue({
|
|
||||||
isLoading: false,
|
|
||||||
data: { canManageAdvancedSettings: true },
|
|
||||||
});
|
|
||||||
const { getByText } = render();
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
|
||||||
const advancedSettingsElement = getByText(messages.headingTitle.defaultMessage, {
|
|
||||||
selector: 'h2.sub-header-title',
|
|
||||||
});
|
|
||||||
expect(advancedSettingsElement).toBeInTheDocument();
|
|
||||||
expect(getByText(messages.policy.defaultMessage)).toBeInTheDocument();
|
|
||||||
expect(getByText(/Do not modify these policies unless you are familiar with their purpose./i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should show permission alert when authz.enable_course_authoring flag is enabled and the user is not authorized', async () => {
|
|
||||||
// Mock feature flag
|
|
||||||
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
|
||||||
// Mock the useUserPermissions hook to return true for the authz.enable_course_authoring permission
|
|
||||||
jest.mocked(useUserPermissions).mockReturnValue({
|
|
||||||
isLoading: false,
|
|
||||||
data: { canManageAdvancedSettings: false },
|
|
||||||
});
|
|
||||||
const { getByTestId } = render();
|
|
||||||
await waitFor(() => {
|
|
||||||
const permissionAlert = getByTestId('permissionDeniedAlert');
|
|
||||||
expect(permissionAlert).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
190
src/advanced-settings/AdvancedSettings.test.tsx
Normal file
190
src/advanced-settings/AdvancedSettings.test.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { CourseAuthoringProvider } from '@src/CourseAuthoringContext';
|
||||||
|
import { useUserPermissions } from '@src/authz/data/apiHooks';
|
||||||
|
import { mockWaffleFlags } from '@src/data/apiHooks.mock';
|
||||||
|
import {
|
||||||
|
render as baseRender,
|
||||||
|
fireEvent,
|
||||||
|
initializeMocks,
|
||||||
|
screen,
|
||||||
|
} from '@src/testUtils';
|
||||||
|
import { advancedSettingsMock } from './__mocks__';
|
||||||
|
import { getCourseAdvancedSettingsApiUrl } from './data/api';
|
||||||
|
import AdvancedSettings from './AdvancedSettings';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
let axiosMock;
|
||||||
|
const mockPathname = '/foo-bar';
|
||||||
|
const courseId = '123';
|
||||||
|
|
||||||
|
// Mock the TextareaAutosize component
|
||||||
|
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
||||||
|
<textarea
|
||||||
|
{...props}
|
||||||
|
onFocus={() => { }}
|
||||||
|
/>
|
||||||
|
)));
|
||||||
|
|
||||||
|
jest.mock('@src/authz/data/apiHooks', () => ({
|
||||||
|
useUserPermissions: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const render = () => baseRender(
|
||||||
|
<CourseAuthoringProvider courseId={courseId}>
|
||||||
|
<AdvancedSettings />
|
||||||
|
</CourseAuthoringProvider>,
|
||||||
|
{ path: mockPathname },
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('<AdvancedSettings />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const mocks = initializeMocks();
|
||||||
|
axiosMock = mocks.axiosMock;
|
||||||
|
axiosMock
|
||||||
|
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
|
||||||
|
.reply(200, advancedSettingsMock);
|
||||||
|
|
||||||
|
jest.mocked(useUserPermissions).mockReturnValue({
|
||||||
|
isLoading: false,
|
||||||
|
data: { canManageAdvancedSettings: true },
|
||||||
|
} as unknown as ReturnType<typeof useUserPermissions>);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render placeholder when settings fetch returns 403', async () => {
|
||||||
|
axiosMock
|
||||||
|
.onGet(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`)
|
||||||
|
.reply(403);
|
||||||
|
render();
|
||||||
|
expect(await screen.findByText(/Under Construction/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render without errors', async () => {
|
||||||
|
render();
|
||||||
|
expect(await screen.findByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.headingTitle.defaultMessage, {
|
||||||
|
selector: 'h2.sub-header-title',
|
||||||
|
})).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.policy.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Do not modify these policies unless you are familiar with their purpose./i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render setting element', async () => {
|
||||||
|
render();
|
||||||
|
expect(await screen.findByText(/Advanced Module List/i)).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Certificate web/html view enabled')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change to onСhange', async () => {
|
||||||
|
render();
|
||||||
|
const textarea = await screen.findByLabelText(/Advanced Module List/i);
|
||||||
|
expect(textarea).toBeInTheDocument();
|
||||||
|
fireEvent.change(textarea, { target: { value: '[1, 2, 3]' } });
|
||||||
|
expect(textarea).toHaveValue('[1, 2, 3]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a warning alert', async () => {
|
||||||
|
render();
|
||||||
|
const textarea = await screen.findByLabelText(/Advanced Module List/i);
|
||||||
|
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
|
||||||
|
expect(screen.getByText(messages.buttonCancelText.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.buttonSaveText.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.alertWarning.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.alertWarningDescriptions.defaultMessage)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display a tooltip on clicking on the icon', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render();
|
||||||
|
const button = await screen.findByLabelText(/Show help text/i);
|
||||||
|
await user.click(button);
|
||||||
|
expect(screen.getByText(/Enter the names of the advanced modules to use in your course./i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change deprecated button text', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render();
|
||||||
|
const showDeprecatedItemsBtn = await screen.findByText(/Show Deprecated Settings/i);
|
||||||
|
expect(showDeprecatedItemsBtn).toBeInTheDocument();
|
||||||
|
await user.click(showDeprecatedItemsBtn);
|
||||||
|
expect(screen.getByText(/Hide Deprecated Settings/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Certificate web/html view enabled')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset to default value on click on Cancel button', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render();
|
||||||
|
const textarea = await screen.findByLabelText(/Advanced Module List/i);
|
||||||
|
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
|
||||||
|
expect(textarea).toHaveValue('[3, 2, 1]');
|
||||||
|
await user.click(screen.getByText(messages.buttonCancelText.defaultMessage));
|
||||||
|
expect(textarea).toHaveValue('[]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the textarea value and display the updated value after clicking "Change manually"', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render();
|
||||||
|
const textarea = await screen.findByLabelText(/Advanced Module List/i);
|
||||||
|
fireEvent.change(textarea, { target: { value: '[3, 2, 1,' } });
|
||||||
|
fireEvent.blur(textarea);
|
||||||
|
expect(textarea).toHaveValue('[3, 2, 1,');
|
||||||
|
await user.click(screen.getByText('Save changes'));
|
||||||
|
await user.click(await screen.findByText('Change manually'));
|
||||||
|
expect(textarea).toHaveValue('[3, 2, 1,');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show success alert after save', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render();
|
||||||
|
const textarea = await screen.findByLabelText(/Advanced Module List/i);
|
||||||
|
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
|
||||||
|
expect(textarea).toHaveValue('[3, 2, 1]');
|
||||||
|
axiosMock
|
||||||
|
.onPatch(`${getCourseAdvancedSettingsApiUrl(courseId)}`)
|
||||||
|
.reply(200, {
|
||||||
|
...advancedSettingsMock,
|
||||||
|
advancedModules: {
|
||||||
|
...advancedSettingsMock.advancedModules,
|
||||||
|
value: [3, 2, 1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await user.click(screen.getByText('Save changes'));
|
||||||
|
expect(screen.getByText('Your policy changes have been saved.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error modal on save failure', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render();
|
||||||
|
const textarea = await screen.findByLabelText(/Advanced Module List/i);
|
||||||
|
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
|
||||||
|
axiosMock
|
||||||
|
.onPatch(`${getCourseAdvancedSettingsApiUrl(courseId)}`)
|
||||||
|
.reply(500);
|
||||||
|
await user.click(screen.getByText('Save changes'));
|
||||||
|
expect(await screen.findByText('Validation error while saving')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render without errors when authz.enable_course_authoring flag is enabled and the user is authorized', async () => {
|
||||||
|
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
||||||
|
jest.mocked(useUserPermissions).mockReturnValue({
|
||||||
|
isLoading: false,
|
||||||
|
data: { canManageAdvancedSettings: true },
|
||||||
|
} as unknown as ReturnType<typeof useUserPermissions>);
|
||||||
|
render();
|
||||||
|
expect(await screen.findByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.headingTitle.defaultMessage, {
|
||||||
|
selector: 'h2.sub-header-title',
|
||||||
|
})).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(messages.policy.defaultMessage)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Do not modify these policies unless you are familiar with their purpose./i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show permission alert when authz.enable_course_authoring flag is enabled and the user is not authorized', async () => {
|
||||||
|
mockWaffleFlags({ enableAuthzCourseAuthoring: true });
|
||||||
|
jest.mocked(useUserPermissions).mockReturnValue({
|
||||||
|
isLoading: false,
|
||||||
|
data: { canManageAdvancedSettings: false },
|
||||||
|
} as unknown as ReturnType<typeof useUserPermissions>);
|
||||||
|
render();
|
||||||
|
expect(await screen.findByTestId('permissionDeniedAlert')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { Helmet } from 'react-helmet';
|
||||||
import {
|
import {
|
||||||
Container, Button, Layout, StatefulButton, TransitionReplace,
|
Container, Button, Layout, StatefulButton, TransitionReplace,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
@@ -10,40 +10,35 @@ import { useWaffleFlags } from '@src/data/apiHooks';
|
|||||||
import { useUserPermissions } from '@src/authz/data/apiHooks';
|
import { useUserPermissions } from '@src/authz/data/apiHooks';
|
||||||
import { COURSE_PERMISSIONS } from '@src/authz/constants';
|
import { COURSE_PERMISSIONS } from '@src/authz/constants';
|
||||||
import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
|
import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert';
|
||||||
import Placeholder from '../editors/Placeholder';
|
import AlertProctoringError from '@src/generic/AlertProctoringError';
|
||||||
|
import { LoadingSpinner } from '@src/generic/Loading';
|
||||||
|
import InternetConnectionAlert from '@src/generic/internet-connection-alert';
|
||||||
|
import { parseArrayOrObjectValues } from '@src/utils';
|
||||||
|
import { RequestStatus } from '@src/data/constants';
|
||||||
|
import SubHeader from '@src/generic/sub-header/SubHeader';
|
||||||
|
import AlertMessage from '@src/generic/alert-message';
|
||||||
|
import getPageHeadTitle from '@src/generic/utils';
|
||||||
|
import Placeholder from '@src/editors/Placeholder';
|
||||||
|
|
||||||
import AlertProctoringError from '../generic/AlertProctoringError';
|
|
||||||
import InternetConnectionAlert from '../generic/internet-connection-alert';
|
|
||||||
import { parseArrayOrObjectValues } from '../utils';
|
|
||||||
import { RequestStatus } from '../data/constants';
|
|
||||||
import SubHeader from '../generic/sub-header/SubHeader';
|
|
||||||
import AlertMessage from '../generic/alert-message';
|
|
||||||
import { fetchCourseAppSettings, updateCourseAppSetting, fetchProctoringExamErrors } from './data/thunks';
|
|
||||||
import {
|
|
||||||
getCourseAppSettings, getSavingStatus, getProctoringExamErrors, getSendRequestErrors, getLoadingStatus,
|
|
||||||
} from './data/selectors';
|
|
||||||
import SettingCard from './setting-card/SettingCard';
|
import SettingCard from './setting-card/SettingCard';
|
||||||
import SettingsSidebar from './settings-sidebar/SettingsSidebar';
|
import SettingsSidebar from './settings-sidebar/SettingsSidebar';
|
||||||
import validateAdvancedSettingsData from './utils';
|
import validateAdvancedSettingsData from './utils';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import ModalError from './modal-error/ModalError';
|
import ModalError from './modal-error/ModalError';
|
||||||
import getPageHeadTitle from '../generic/utils';
|
import { useCourseAdvancedSettings, useProctoringExamErrors, useUpdateCourseAdvancedSettings } from './data/apiHooks';
|
||||||
|
|
||||||
const AdvancedSettings = () => {
|
const AdvancedSettings = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [saveSettingsPrompt, showSaveSettingsPrompt] = useState(false);
|
const [saveSettingsPrompt, showSaveSettingsPrompt] = useState(false);
|
||||||
const [showDeprecated, setShowDeprecated] = useState(false);
|
const [showDeprecated, setShowDeprecated] = useState(false);
|
||||||
const [errorModal, showErrorModal] = useState(false);
|
const [errorModal, showErrorModal] = useState(false);
|
||||||
const [editedSettings, setEditedSettings] = useState({});
|
const [editedSettings, setEditedSettings] = useState({});
|
||||||
const [errorFields, setErrorFields] = useState([]);
|
const [errorFields, setErrorFields] = useState([]);
|
||||||
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
|
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
|
||||||
const [isQueryPending, setIsQueryPending] = useState(false);
|
|
||||||
const [isEditableState, setIsEditableState] = useState(false);
|
const [isEditableState, setIsEditableState] = useState(false);
|
||||||
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);
|
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);
|
||||||
|
|
||||||
const { courseId, courseDetails } = useCourseAuthoringContext();
|
const { courseId, courseDetails } = useCourseAuthoringContext();
|
||||||
document.title = getPageHeadTitle(courseDetails?.name, intl.formatMessage(messages.headingTitle));
|
|
||||||
|
|
||||||
const waffleFlags = useWaffleFlags(courseId);
|
const waffleFlags = useWaffleFlags(courseId);
|
||||||
const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
|
const isAuthzEnabled = waffleFlags.enableAuthzCourseAuthoring;
|
||||||
@@ -54,18 +49,25 @@ const AdvancedSettings = () => {
|
|||||||
},
|
},
|
||||||
}, isAuthzEnabled);
|
}, isAuthzEnabled);
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
dispatch(fetchCourseAppSettings(courseId));
|
data: advancedSettingsData = {},
|
||||||
dispatch(fetchProctoringExamErrors(courseId));
|
isPending: isPendingSettingsStatus,
|
||||||
}, [courseId]);
|
failureReason: settingsStatusError,
|
||||||
|
} = useCourseAdvancedSettings(courseId);
|
||||||
|
|
||||||
const advancedSettingsData = useSelector(getCourseAppSettings);
|
const {
|
||||||
const savingStatus = useSelector(getSavingStatus);
|
data: proctoringExamErrors = {},
|
||||||
const proctoringExamErrors = useSelector(getProctoringExamErrors);
|
} = useProctoringExamErrors(courseId);
|
||||||
const settingsWithSendErrors = useSelector(getSendRequestErrors) || {};
|
|
||||||
const loadingSettingsStatus = useSelector(getLoadingStatus);
|
|
||||||
|
|
||||||
const isLoading = loadingSettingsStatus === RequestStatus.IN_PROGRESS || (isAuthzEnabled && isLoadingUserPermissions);
|
const updateMutation = useUpdateCourseAdvancedSettings(courseId);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isPending: isQueryPending,
|
||||||
|
isSuccess: isQuerySuccess,
|
||||||
|
error: queryError,
|
||||||
|
} = updateMutation;
|
||||||
|
|
||||||
|
const isLoading = isPendingSettingsStatus || (isAuthzEnabled && isLoadingUserPermissions);
|
||||||
const updateSettingsButtonState = {
|
const updateSettingsButtonState = {
|
||||||
labels: {
|
labels: {
|
||||||
default: intl.formatMessage(messages.buttonSaveText),
|
default: intl.formatMessage(messages.buttonSaveText),
|
||||||
@@ -73,30 +75,34 @@ const AdvancedSettings = () => {
|
|||||||
},
|
},
|
||||||
disabledStates: ['pending'],
|
disabledStates: ['pending'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
proctoringErrors,
|
proctoringErrors,
|
||||||
mfeProctoredExamSettingsUrl,
|
mfeProctoredExamSettingsUrl,
|
||||||
} = proctoringExamErrors;
|
} = proctoringExamErrors;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (savingStatus === RequestStatus.SUCCESSFUL) {
|
if (isQuerySuccess) {
|
||||||
setIsQueryPending(false);
|
|
||||||
setShowSuccessAlert(true);
|
setShowSuccessAlert(true);
|
||||||
setIsEditableState(false);
|
setIsEditableState(false);
|
||||||
setTimeout(() => setShowSuccessAlert(false), 15000);
|
setTimeout(() => setShowSuccessAlert(false), 15000);
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
showSaveSettingsPrompt(false);
|
showSaveSettingsPrompt(false);
|
||||||
} else if (savingStatus === RequestStatus.FAILED && !hasInternetConnectionError) {
|
} else if (queryError && !hasInternetConnectionError) {
|
||||||
setErrorFields(settingsWithSendErrors);
|
// @ts-ignore
|
||||||
|
setErrorFields(queryError?.response?.data ?? []);
|
||||||
showErrorModal(true);
|
showErrorModal(true);
|
||||||
}
|
}
|
||||||
}, [savingStatus]);
|
}, [isQuerySuccess, queryError]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
return (
|
||||||
return <></>;
|
<div className="row justify-content-center m-6">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (loadingSettingsStatus === RequestStatus.DENIED) {
|
if (settingsStatusError?.response?.status === 403) {
|
||||||
return (
|
return (
|
||||||
<div className="row justify-content-center m-6">
|
<div className="row justify-content-center m-6">
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
@@ -118,24 +124,21 @@ const AdvancedSettings = () => {
|
|||||||
const handleUpdateAdvancedSettingsData = () => {
|
const handleUpdateAdvancedSettingsData = () => {
|
||||||
const isValid = validateAdvancedSettingsData(editedSettings, setErrorFields, setEditedSettings);
|
const isValid = validateAdvancedSettingsData(editedSettings, setErrorFields, setEditedSettings);
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
setIsQueryPending(true);
|
setShowSuccessAlert(false);
|
||||||
|
updateMutation.mutate(parseArrayOrObjectValues(editedSettings));
|
||||||
} else {
|
} else {
|
||||||
showSaveSettingsPrompt(false);
|
showSaveSettingsPrompt(false);
|
||||||
showErrorModal(!errorModal);
|
showErrorModal(!errorModal);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
const handleInternetConnectionFailed = () => {
|
const handleInternetConnectionFailed = () => {
|
||||||
setInternetConnectionError(true);
|
setInternetConnectionError(true);
|
||||||
showSaveSettingsPrompt(false);
|
showSaveSettingsPrompt(false);
|
||||||
setShowSuccessAlert(false);
|
setShowSuccessAlert(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQueryProcessing = () => {
|
|
||||||
setShowSuccessAlert(false);
|
|
||||||
dispatch(updateCourseAppSetting(courseId, parseArrayOrObjectValues(editedSettings)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleManuallyChangeClick = (setToState) => {
|
const handleManuallyChangeClick = (setToState) => {
|
||||||
showErrorModal(setToState);
|
showErrorModal(setToState);
|
||||||
showSaveSettingsPrompt(true);
|
showSaveSettingsPrompt(true);
|
||||||
@@ -152,6 +155,11 @@ const AdvancedSettings = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>
|
||||||
|
{getPageHeadTitle(courseDetails?.name ?? '', intl.formatMessage(messages.headingTitle))}
|
||||||
|
</title>
|
||||||
|
</Helmet>
|
||||||
<Container size="xl" className="advanced-settings px-4">
|
<Container size="xl" className="advanced-settings px-4">
|
||||||
<div className="setting-header mt-5">
|
<div className="setting-header mt-5">
|
||||||
{(proctoringErrors?.length > 0) && (
|
{(proctoringErrors?.length > 0) && (
|
||||||
@@ -161,7 +169,11 @@ const AdvancedSettings = () => {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
aria-labelledby={intl.formatMessage(messages.alertProctoringAriaLabelledby)}
|
aria-labelledby={intl.formatMessage(messages.alertProctoringAriaLabelledby)}
|
||||||
aria-describedby={intl.formatMessage(messages.alertProctoringDescribedby)}
|
aria-describedby={intl.formatMessage(messages.alertProctoringDescribedby)}
|
||||||
/>
|
>
|
||||||
|
{/* Empty children to satisfy the type checker */}
|
||||||
|
{/* eslint-disable-next-line react/jsx-no-useless-fragment */}
|
||||||
|
<></>
|
||||||
|
</AlertProctoringError>
|
||||||
)}
|
)}
|
||||||
<TransitionReplace>
|
<TransitionReplace>
|
||||||
{showSuccessAlert ? (
|
{showSuccessAlert ? (
|
||||||
@@ -257,9 +269,8 @@ const AdvancedSettings = () => {
|
|||||||
<div className="alert-toast">
|
<div className="alert-toast">
|
||||||
{isQueryPending && (
|
{isQueryPending && (
|
||||||
<InternetConnectionAlert
|
<InternetConnectionAlert
|
||||||
isFailed={savingStatus === RequestStatus.FAILED}
|
isFailed={Boolean(queryError)}
|
||||||
isQueryPending={isQueryPending}
|
isQueryPending={isQueryPending}
|
||||||
onQueryProcessing={handleQueryProcessing}
|
|
||||||
onInternetConnectionFailed={handleInternetConnectionFailed}
|
onInternetConnectionFailed={handleInternetConnectionFailed}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -270,18 +281,18 @@ const AdvancedSettings = () => {
|
|||||||
aria-describedby={intl.formatMessage(messages.alertWarningAriaDescribedby)}
|
aria-describedby={intl.formatMessage(messages.alertWarningAriaDescribedby)}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
actions={[
|
actions={[
|
||||||
!isQueryPending && (
|
!isQueryPending ? (
|
||||||
<Button variant="tertiary" onClick={handleResetSettingsValues}>
|
<Button variant="tertiary" onClick={handleResetSettingsValues}>
|
||||||
{intl.formatMessage(messages.buttonCancelText)}
|
{intl.formatMessage(messages.buttonCancelText)}
|
||||||
</Button>
|
</Button>
|
||||||
),
|
) : /* istanbul ignore next */ null,
|
||||||
<StatefulButton
|
<StatefulButton
|
||||||
key="statefulBtn"
|
key="statefulBtn"
|
||||||
onClick={handleUpdateAdvancedSettingsData}
|
onClick={handleUpdateAdvancedSettingsData}
|
||||||
state={isQueryPending ? RequestStatus.PENDING : 'default'}
|
state={isQueryPending ? RequestStatus.PENDING : 'default'}
|
||||||
{...updateSettingsButtonState}
|
{...updateSettingsButtonState}
|
||||||
/>,
|
/>,
|
||||||
].filter(Boolean)}
|
].filter((action): action is JSX.Element => action !== null)}
|
||||||
variant="warning"
|
variant="warning"
|
||||||
icon={Warning}
|
icon={Warning}
|
||||||
title={intl.formatMessage(messages.alertWarning)}
|
title={intl.formatMessage(messages.alertWarning)}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
import {
|
import {
|
||||||
camelCaseObject,
|
camelCaseObject,
|
||||||
getConfig,
|
getConfig,
|
||||||
} from '@edx/frontend-platform';
|
} from '@edx/frontend-platform';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||||
import { camelCase } from 'lodash';
|
import { camelCase } from 'lodash';
|
||||||
import { convertObjectToSnakeCase } from '../../utils';
|
import { convertObjectToSnakeCase } from '@src/utils';
|
||||||
|
|
||||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||||
export const getCourseAdvancedSettingsApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v0/advanced_settings/${courseId}`;
|
export const getCourseAdvancedSettingsApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v0/advanced_settings/${courseId}`;
|
||||||
@@ -13,10 +12,8 @@ const getProctoringErrorsApiUrl = () => `${getApiBaseUrl()}/api/contentstore/v1/
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get's advanced setting for a course.
|
* Get's advanced setting for a course.
|
||||||
* @param {string} courseId
|
|
||||||
* @returns {Promise<Object>}
|
|
||||||
*/
|
*/
|
||||||
export async function getCourseAdvancedSettings(courseId) {
|
export async function getCourseAdvancedSettings(courseId: string): Promise<Record<string, any>> {
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
|
.get(`${getCourseAdvancedSettingsApiUrl(courseId)}?fetch_all=0`);
|
||||||
const keepValues = {};
|
const keepValues = {};
|
||||||
@@ -36,11 +33,11 @@ export async function getCourseAdvancedSettings(courseId) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates advanced setting for a course.
|
* Updates advanced setting for a course.
|
||||||
* @param {string} courseId
|
|
||||||
* @param {object} settings
|
|
||||||
* @returns {Promise<Object>}
|
|
||||||
*/
|
*/
|
||||||
export async function updateCourseAdvancedSettings(courseId, settings) {
|
export async function updateCourseAdvancedSettings(
|
||||||
|
courseId: string,
|
||||||
|
settings: Record<string, any>,
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
|
.patch(`${getCourseAdvancedSettingsApiUrl(courseId)}`, convertObjectToSnakeCase(settings));
|
||||||
const keepValues = {};
|
const keepValues = {};
|
||||||
@@ -60,10 +57,8 @@ export async function updateCourseAdvancedSettings(courseId, settings) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets proctoring exam errors.
|
* Gets proctoring exam errors.
|
||||||
* @param {string} courseId
|
|
||||||
* @returns {Promise<Object>}
|
|
||||||
*/
|
*/
|
||||||
export async function getProctoringExamErrors(courseId) {
|
export async function getProctoringExamErrors(courseId: string): Promise<Record<string, any>> {
|
||||||
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
|
const { data } = await getAuthenticatedHttpClient().get(`${getProctoringErrorsApiUrl()}${courseId}`);
|
||||||
const keepValues = {};
|
const keepValues = {};
|
||||||
Object.keys(data).forEach((key) => {
|
Object.keys(data).forEach((key) => {
|
||||||
@@ -77,5 +72,6 @@ export async function getProctoringExamErrors(courseId) {
|
|||||||
value: keepValues[key]?.value,
|
value: keepValues[key]?.value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return formattedData;
|
return formattedData;
|
||||||
}
|
}
|
||||||
56
src/advanced-settings/data/apiHooks.ts
Normal file
56
src/advanced-settings/data/apiHooks.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import {
|
||||||
|
getCourseAdvancedSettings,
|
||||||
|
getProctoringExamErrors,
|
||||||
|
updateCourseAdvancedSettings,
|
||||||
|
} from './api';
|
||||||
|
|
||||||
|
export const advancedSettingsQueryKeys = {
|
||||||
|
all: ['advancedSettings'],
|
||||||
|
/** Base key for advanced settings specific to a courseId */
|
||||||
|
courseAdvancedSettings: (courseId: string) => [...advancedSettingsQueryKeys.all, courseId],
|
||||||
|
/** Key for proctoring exam errors specific to a courseId */
|
||||||
|
proctoringExamErrors: (courseId: string) => [...advancedSettingsQueryKeys.all, courseId, 'proctoringErrors'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortSettingsByDisplayName = (settings: Record<string, any>): Record<string, any> => (
|
||||||
|
Object.fromEntries(Object.entries(settings).sort(
|
||||||
|
([, v1], [, v2]) => v1.displayName.localeCompare(v2.displayName),
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the advanced settings for a course, sorted alphabetically by display name.
|
||||||
|
*/
|
||||||
|
export const useCourseAdvancedSettings = (courseId: string) => (
|
||||||
|
useQuery<Record<string, any>, AxiosError>({
|
||||||
|
queryKey: advancedSettingsQueryKeys.courseAdvancedSettings(courseId),
|
||||||
|
queryFn: () => getCourseAdvancedSettings(courseId),
|
||||||
|
select: sortSettingsByDisplayName,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the proctoring exam errors for a course.
|
||||||
|
*/
|
||||||
|
export const useProctoringExamErrors = (courseId: string) => (
|
||||||
|
useQuery({
|
||||||
|
queryKey: advancedSettingsQueryKeys.proctoringExamErrors(courseId),
|
||||||
|
queryFn: () => getProctoringExamErrors(courseId),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a mutation to update the advanced settings for a course.
|
||||||
|
*/
|
||||||
|
export const useUpdateCourseAdvancedSettings = (courseId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<Record<string, any>, AxiosError, Record<string, any>>({
|
||||||
|
mutationFn: (settings: Record<string, any>) => updateCourseAdvancedSettings(courseId, settings),
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: advancedSettingsQueryKeys.courseAdvancedSettings(courseId) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export const getLoadingStatus = (state) => state.advancedSettings.loadingStatus;
|
|
||||||
export const getCourseAppSettings = state => state.advancedSettings.courseAppSettings;
|
|
||||||
export const getSavingStatus = (state) => state.advancedSettings.savingStatus;
|
|
||||||
export const getProctoringExamErrors = (state) => state.advancedSettings.proctoringErrors;
|
|
||||||
export const getSendRequestErrors = (state) => state.advancedSettings.sendRequestErrors.developer_message;
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
import { RequestStatus } from '../../data/constants';
|
|
||||||
|
|
||||||
const slice = createSlice({
|
|
||||||
name: 'advancedSettings',
|
|
||||||
initialState: {
|
|
||||||
loadingStatus: RequestStatus.IN_PROGRESS,
|
|
||||||
savingStatus: '',
|
|
||||||
courseAppSettings: {},
|
|
||||||
proctoringErrors: {},
|
|
||||||
sendRequestErrors: {},
|
|
||||||
},
|
|
||||||
reducers: {
|
|
||||||
updateLoadingStatus: (state, { payload }) => {
|
|
||||||
state.loadingStatus = payload.status;
|
|
||||||
},
|
|
||||||
updateSavingStatus: (state, { payload }) => {
|
|
||||||
state.savingStatus = payload.status;
|
|
||||||
},
|
|
||||||
fetchCourseAppsSettingsSuccess: (state, { payload }) => {
|
|
||||||
Object.assign(state.courseAppSettings, payload);
|
|
||||||
},
|
|
||||||
updateCourseAppsSettingsSuccess: (state, { payload }) => {
|
|
||||||
Object.assign(state.courseAppSettings, payload);
|
|
||||||
},
|
|
||||||
getDataSendErrors: (state, { payload }) => {
|
|
||||||
Object.assign(state.sendRequestErrors, payload);
|
|
||||||
},
|
|
||||||
fetchProctoringExamErrorsSuccess: (state, { payload }) => {
|
|
||||||
Object.assign(state.proctoringErrors, payload);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const {
|
|
||||||
updateLoadingStatus,
|
|
||||||
updateSavingStatus,
|
|
||||||
getDataSendErrors,
|
|
||||||
fetchCourseAppsSettingsSuccess,
|
|
||||||
updateCourseAppsSettingsSuccess,
|
|
||||||
fetchProctoringExamErrorsSuccess,
|
|
||||||
} = slice.actions;
|
|
||||||
|
|
||||||
export const {
|
|
||||||
reducer,
|
|
||||||
} = slice;
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { RequestStatus } from '../../data/constants';
|
|
||||||
import {
|
|
||||||
getCourseAdvancedSettings,
|
|
||||||
updateCourseAdvancedSettings,
|
|
||||||
getProctoringExamErrors,
|
|
||||||
} from './api';
|
|
||||||
import {
|
|
||||||
fetchCourseAppsSettingsSuccess,
|
|
||||||
updateCourseAppsSettingsSuccess,
|
|
||||||
updateLoadingStatus,
|
|
||||||
updateSavingStatus,
|
|
||||||
fetchProctoringExamErrorsSuccess,
|
|
||||||
getDataSendErrors,
|
|
||||||
} from './slice';
|
|
||||||
|
|
||||||
export function fetchCourseAppSettings(courseId) {
|
|
||||||
return async (dispatch) => {
|
|
||||||
dispatch(updateLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const settingValues = await getCourseAdvancedSettings(courseId);
|
|
||||||
const sortedDisplayName = [];
|
|
||||||
Object.values(settingValues).forEach(value => {
|
|
||||||
const { displayName } = value;
|
|
||||||
sortedDisplayName.push(displayName);
|
|
||||||
});
|
|
||||||
const sortedSettingValues = {};
|
|
||||||
sortedDisplayName.sort((a, b) => a.localeCompare(b)).forEach((displayName => {
|
|
||||||
Object.entries(settingValues).forEach(([key, value]) => {
|
|
||||||
if (value.displayName === displayName) {
|
|
||||||
sortedSettingValues[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
dispatch(fetchCourseAppsSettingsSuccess(sortedSettingValues));
|
|
||||||
dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response && error.response.status === 403) {
|
|
||||||
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.DENIED }));
|
|
||||||
} else {
|
|
||||||
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateCourseAppSetting(courseId, settings) {
|
|
||||||
return async (dispatch) => {
|
|
||||||
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const settingValues = await updateCourseAdvancedSettings(courseId, settings);
|
|
||||||
dispatch(updateCourseAppsSettingsSuccess(settingValues));
|
|
||||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
let errorData;
|
|
||||||
try {
|
|
||||||
const { customAttributes: { httpErrorResponseData } } = error;
|
|
||||||
errorData = JSON.parse(httpErrorResponseData);
|
|
||||||
} catch {
|
|
||||||
errorData = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(getDataSendErrors(errorData));
|
|
||||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchProctoringExamErrors(courseId) {
|
|
||||||
return async (dispatch) => {
|
|
||||||
dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const settingValues = await getProctoringExamErrors(courseId);
|
|
||||||
dispatch(fetchProctoringExamErrorsSuccess(settingValues));
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import { reducer as modelsReducer } from './generic/model-store';
|
|||||||
import { reducer as discussionsReducer } from './pages-and-resources/discussions/data/slice';
|
import { reducer as discussionsReducer } from './pages-and-resources/discussions/data/slice';
|
||||||
import { reducer as pagesAndResourcesReducer } from './pages-and-resources/data/slice';
|
import { reducer as pagesAndResourcesReducer } from './pages-and-resources/data/slice';
|
||||||
import { reducer as customPagesReducer } from './custom-pages/data/slice';
|
import { reducer as customPagesReducer } from './custom-pages/data/slice';
|
||||||
import { reducer as advancedSettingsReducer } from './advanced-settings/data/slice';
|
|
||||||
import { reducer as studioHomeReducer } from './studio-home/data/slice';
|
import { reducer as studioHomeReducer } from './studio-home/data/slice';
|
||||||
import { reducer as scheduleAndDetailsReducer } from './schedule-and-details/data/slice';
|
import { reducer as scheduleAndDetailsReducer } from './schedule-and-details/data/slice';
|
||||||
import { reducer as filesReducer } from './files-and-videos/files-page/data/slice';
|
import { reducer as filesReducer } from './files-and-videos/files-page/data/slice';
|
||||||
@@ -39,7 +38,6 @@ export interface DeprecatedReduxState {
|
|||||||
assets: Record<string, any>;
|
assets: Record<string, any>;
|
||||||
pagesAndResources: Record<string, any>;
|
pagesAndResources: Record<string, any>;
|
||||||
scheduleAndDetails: Record<string, any>;
|
scheduleAndDetails: Record<string, any>;
|
||||||
advancedSettings: Record<string, any>;
|
|
||||||
studioHome: InferState<typeof studioHomeReducer>;
|
studioHome: InferState<typeof studioHomeReducer>;
|
||||||
models: Record<string, any>;
|
models: Record<string, any>;
|
||||||
live: Record<string, any>;
|
live: Record<string, any>;
|
||||||
@@ -71,7 +69,6 @@ export default function initializeStore(preloadedState: Partial<DeprecatedReduxS
|
|||||||
assets: filesReducer,
|
assets: filesReducer,
|
||||||
pagesAndResources: pagesAndResourcesReducer,
|
pagesAndResources: pagesAndResourcesReducer,
|
||||||
scheduleAndDetails: scheduleAndDetailsReducer,
|
scheduleAndDetails: scheduleAndDetailsReducer,
|
||||||
advancedSettings: advancedSettingsReducer,
|
|
||||||
studioHome: studioHomeReducer,
|
studioHome: studioHomeReducer,
|
||||||
models: modelsReducer,
|
models: modelsReducer,
|
||||||
live: liveReducer,
|
live: liveReducer,
|
||||||
|
|||||||
Reference in New Issue
Block a user