- {!isEditableState && (
+ {isQueryPending && (
', () => {
});
});
it('should render setting element', async () => {
- const { getByText } = render(
);
+ 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 () => {
@@ -112,24 +115,50 @@ describe('
', () => {
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(() => {
- const textarea = getByLabelText(/Advanced Module List/i);
- fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
- fireEvent.click(getByText(messages.buttonCancelText.defaultMessage));
- expect(textarea.value).toBe('[]');
+ 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(() => {
- const textarea = getByLabelText(/Advanced Module List/i);
- fireEvent.change(textarea, { target: { value: '[3, 2, 1' } });
- fireEvent.click(getByText(messages.buttonSaveText.defaultMessage));
- fireEvent.click(getByText(/Change manually/i));
- expect(textarea.value).toBe('[3, 2, 1');
+ 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();
});
});
diff --git a/src/advanced-settings/__mocks__/advancedSettings.js b/src/advanced-settings/__mocks__/advancedSettings.js
index 52d5bed6b0..ead6c3528b 100644
--- a/src/advanced-settings/__mocks__/advancedSettings.js
+++ b/src/advanced-settings/__mocks__/advancedSettings.js
@@ -6,4 +6,11 @@ module.exports = {
hideOnEnabledPublisher: false,
value: [],
},
+ certHtmlViewEnabled: {
+ deprecated: true,
+ display_name: 'Certificate web/html view enabled',
+ help: 'If true, certificate Web/HTML views are enabled for the course.',
+ hide_on_enabled_publisher: false,
+ value: true,
+ },
};
diff --git a/src/advanced-settings/data/thunks.js b/src/advanced-settings/data/thunks.js
index 8686582093..db9030fa39 100644
--- a/src/advanced-settings/data/thunks.js
+++ b/src/advanced-settings/data/thunks.js
@@ -19,10 +19,27 @@ export function fetchCourseAppSettings(courseId) {
try {
const settingValues = await getCourseAdvancedSettings(courseId);
- dispatch(fetchCourseAppsSettingsSuccess(settingValues));
+ const sortedDisplayName = [];
+ Object.values(settingValues).forEach(value => {
+ const { displayName } = value;
+ sortedDisplayName.push(displayName);
+ });
+ const sortedSettingValues = {};
+ sortedDisplayName.sort().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) {
- dispatch(updateLoadingStatus({ status: RequestStatus.FAILED }));
+ if (error.response && error.response.status === 403) {
+ dispatch(updateLoadingStatus({ courseId, status: RequestStatus.DENIED }));
+ } else {
+ dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
+ }
}
};
}
diff --git a/src/advanced-settings/scss/AdvancedSettings.scss b/src/advanced-settings/scss/AdvancedSettings.scss
index 3922addf1a..676c6dc81c 100644
--- a/src/advanced-settings/scss/AdvancedSettings.scss
+++ b/src/advanced-settings/scss/AdvancedSettings.scss
@@ -8,8 +8,8 @@
.instructions,
strong {
- font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
color: $text-color-base;
+ font-weight: 400;
}
}
@@ -18,11 +18,6 @@
.pgn__card-header .pgn__card-header-title-md {
font-size: 1.125rem;
- padding-top: .625rem;
- }
-
- .pgn__icon {
- color: $headings-color;
}
}
@@ -44,7 +39,6 @@
}
.form-control {
- resize: none;
min-height: 2.75rem;
width: $setting-form-control-width;
}
@@ -54,7 +48,8 @@
}
.pgn__card-header {
- padding: 0 1.5rem;
+ padding: 0 0 0 1.5rem;
+ flex-grow: 1;
}
.pgn__card-status {
@@ -66,12 +61,39 @@
margin-bottom: 1.438rem;
display: flex;
flex-direction: revert;
+ }
+}
+
+.setting-sidebar-supplementary {
+ .setting-sidebar-supplementary-about {
+ .setting-sidebar-supplementary-about-title {
+ font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base;
+ color: $headings-color;
+ margin-bottom: 1.25rem;
+ }
+
+ .setting-sidebar-supplementary-about-descriptions {
+ font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
+ color: $text-color-base;
+ }
+ }
+
+ .setting-sidebar-supplementary-other-links ul {
+ list-style: none;
- .pgn__card-header-subtitle-md {
- margin-left: .375rem;
- margin-top: .125rem;
+ .setting-sidebar-supplementary-other-link {
+ font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
+ line-height: 1.5rem;
+ color: $info-500;
+ margin-bottom: .5rem;
}
}
+
+ .setting-sidebar-supplementary-other-title {
+ font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base;
+ color: $headings-color;
+ margin-bottom: 1.25rem;
+ }
}
.modal-error-item {
@@ -89,3 +111,15 @@
align-items: center;
}
}
+
+.modal-popup-content {
+ max-width: 200px;
+ color: $white;
+ background-color: $black;
+ filter: drop-shadow(0 2px 4px rgba(0 0 0 / .15));
+ font-weight: 400;
+}
+
+.pgn__modal-popup__arrow::after {
+ border-top-color: $black;
+}
diff --git a/src/advanced-settings/setting-card/SettingCard.jsx b/src/advanced-settings/setting-card/SettingCard.jsx
index b4ff22aad8..15cef8ad9f 100644
--- a/src/advanced-settings/setting-card/SettingCard.jsx
+++ b/src/advanced-settings/setting-card/SettingCard.jsx
@@ -1,10 +1,15 @@
-import React from 'react';
+import React, { useState } from 'react';
import {
- Card, Form, Icon, IconButton, OverlayTrigger, Popover,
+ ActionRow,
+ Card,
+ Form,
+ Icon,
+ IconButton,
+ ModalPopup,
+ useToggle,
} from '@edx/paragon';
-import { Info, Warning } from '@edx/paragon/icons';
+import { InfoOutline, Warning } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
-import classNames from 'classnames';
import { capitalize } from 'lodash';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import TextareaAutosize from 'react-textarea-autosize';
@@ -12,47 +17,87 @@ import TextareaAutosize from 'react-textarea-autosize';
import messages from './messages';
const SettingCard = ({
- intl, showDeprecated, name, onChange, value, settingData, handleBlur,
+ name,
+ settingData,
+ handleBlur,
+ setEdited,
+ showSaveSettingsPrompt,
+ saveSettingsPrompt,
+ isEditableState,
+ setIsEditableState,
+ // injected
+ intl,
}) => {
const { deprecated, help, displayName } = settingData;
+ const initialValue = JSON.stringify(settingData.value, null, 4);
+ const [isOpen, open, close] = useToggle(false);
+ const [target, setTarget] = useState(null);
+ const [newValue, setNewValue] = useState(initialValue);
+
+ const handleSettingChange = (e) => {
+ const { value } = e.target;
+ setNewValue(e.target.value);
+ if (value !== initialValue) {
+ if (!saveSettingsPrompt) {
+ showSaveSettingsPrompt(true);
+ }
+ if (!isEditableState) {
+ setIsEditableState(true);
+ }
+ }
+ };
+
+ const handleCardBlur = () => {
+ setEdited((prevEditedSettings) => ({
+ ...prevEditedSettings,
+ [name]: newValue,
+ }));
+ handleBlur();
+ };
+
return (
-
+
-
+
-
- {/* eslint-disable-next-line react/no-danger */}
-
-
-
- )}
- >
+ title={(
+
+ {capitalize(displayName)}
-
+
+
+
+
)}
/>
@@ -73,22 +118,21 @@ SettingCard.propTypes = {
deprecated: PropTypes.bool,
help: PropTypes.string,
displayName: PropTypes.string,
+ value: PropTypes.PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.bool,
+ PropTypes.number,
+ PropTypes.object,
+ PropTypes.array,
+ ]),
}).isRequired,
- value: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.bool,
- PropTypes.number,
- PropTypes.object,
- PropTypes.array,
- ]),
- onChange: PropTypes.func.isRequired,
- showDeprecated: PropTypes.bool.isRequired,
+ setEdited: PropTypes.func.isRequired,
+ showSaveSettingsPrompt: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
handleBlur: PropTypes.func.isRequired,
-};
-
-SettingCard.defaultProps = {
- value: undefined,
+ saveSettingsPrompt: PropTypes.bool.isRequired,
+ isEditableState: PropTypes.bool.isRequired,
+ setIsEditableState: PropTypes.func.isRequired,
};
export default injectIntl(SettingCard);
diff --git a/src/advanced-settings/setting-card/SettingCard.test.jsx b/src/advanced-settings/setting-card/SettingCard.test.jsx
index cb3aef74a3..a03f51a985 100644
--- a/src/advanced-settings/setting-card/SettingCard.test.jsx
+++ b/src/advanced-settings/setting-card/SettingCard.test.jsx
@@ -1,16 +1,21 @@
import React from 'react';
-import { render } from '@testing-library/react';
+import { fireEvent, render, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import SettingCard from './SettingCard';
import messages from './messages';
-const handleChange = jest.fn();
+const setEdited = jest.fn();
+const showSaveSettingsPrompt = jest.fn();
+const setIsEditableState = jest.fn();
+const handleBlur = jest.fn();
const settingData = {
deprecated: false,
help: 'This is a help message',
displayName: 'Setting Name',
+ value: 'Setting Value',
};
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
@@ -27,9 +32,13 @@ const RootWrapper = () => (
intl={{}}
isOn
name="settingName"
- onChange={handleChange}
- value="Setting Value"
+ setEdited={setEdited}
+ setIsEditableState={setIsEditableState}
+ showSaveSettingsPrompt={showSaveSettingsPrompt}
settingData={settingData}
+ handleBlur={handleBlur}
+ isEditableState
+ saveSettingsPrompt={false}
/>
);
@@ -42,7 +51,7 @@ describe('', () => {
const input = getByLabelText(/Setting Name/i);
expect(cardTitle).toBeInTheDocument();
expect(input).toBeInTheDocument();
- expect(input.value).toBe('Setting Value');
+ expect(input.value).toBe(JSON.stringify(settingData.value, null, 4));
});
it('displays the deprecated status when the setting is deprecated', () => {
const deprecatedSettingData = { ...settingData, deprecated: true };
@@ -52,9 +61,13 @@ describe('', () => {
intl={{}}
isOn
name="settingName"
- onChange={handleChange}
- value="Setting Value"
+ setEdited={setEdited}
+ setIsEditableState={setIsEditableState}
+ showSaveSettingsPrompt={showSaveSettingsPrompt}
settingData={deprecatedSettingData}
+ handleBlur={handleBlur}
+ isEditable={false}
+ saveSettingsPrompt
/>
,
);
@@ -65,4 +78,19 @@ describe('', () => {
const { queryByText } = render();
expect(queryByText(messages.deprecated.defaultMessage)).toBeNull();
});
+ it('calls setEdited on blur', async () => {
+ const { getByLabelText } = render();
+ const inputBox = getByLabelText(/Setting Name/i);
+ fireEvent.focus(inputBox);
+ userEvent.clear(inputBox);
+ userEvent.type(inputBox, '3, 2, 1');
+ await waitFor(() => {
+ expect(inputBox).toHaveValue('3, 2, 1');
+ });
+ await (async () => {
+ expect(setEdited).toHaveBeenCalled();
+ expect(handleBlur).toHaveBeenCalled();
+ });
+ fireEvent.focusOut(inputBox);
+ });
});
diff --git a/src/custom-pages/CustomPages.jsx b/src/custom-pages/CustomPages.jsx
index 87e3ead4c1..b7019ba10e 100644
--- a/src/custom-pages/CustomPages.jsx
+++ b/src/custom-pages/CustomPages.jsx
@@ -16,9 +16,10 @@ import {
useToggle,
Image,
ModalDialog,
+ Container,
} from '@edx/paragon';
import { Add, SpinnerSimple } from '@edx/paragon/icons';
-import {
+import Placeholder, {
DraggableList,
SortableItem,
ErrorAlert,
@@ -96,16 +97,21 @@ const CustomPages = ({
},
disabledStates: ['pending'],
};
-
useEffect(() => { setOrderedPages(pages); }, [customPagesIds, savingStatus]);
if (loadingStatus === RequestStatus.IN_PROGRESS) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return (<>>);
}
-
+ if (loadingStatus === RequestStatus.DENIED) {
+ return (
+
+ );
+ }
return (
-
+
-
+
);
};
diff --git a/src/custom-pages/CustomPages.test.jsx b/src/custom-pages/CustomPages.test.jsx
index 2ef77a0ab5..979145082a 100644
--- a/src/custom-pages/CustomPages.test.jsx
+++ b/src/custom-pages/CustomPages.test.jsx
@@ -19,6 +19,7 @@ import CustomPages from './CustomPages';
import {
generateFetchPageApiResponse,
generateNewPageApiResponse,
+ getStatusValue,
courseId,
initialState,
} from './factories/mockApiResponses';
@@ -45,12 +46,12 @@ const renderComponent = () => {
);
};
-const mockStore = async () => {
+const mockStore = async (status) => {
const xblockAddUrl = `${getApiBaseUrl()}/xblock/`;
const reorderUrl = `${getTabHandlerUrl(courseId)}/reorder`;
const fetchPagesUrl = `${getTabHandlerUrl(courseId)}`;
- axiosMock.onGet(fetchPagesUrl).reply(200, generateFetchPageApiResponse());
+ axiosMock.onGet(fetchPagesUrl).reply(getStatusValue(status), generateFetchPageApiResponse());
axiosMock.onPost(reorderUrl).reply(204);
axiosMock.onPut(xblockAddUrl).reply(200, generateNewPageApiResponse());
@@ -72,21 +73,26 @@ describe('CustomPages', () => {
store = initializeStore(initialState);
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
+ it('should ', async () => {
+ renderComponent();
+ await mockStore(RequestStatus.DENIED);
+ expect(screen.getByTestId('under-construction-placeholder')).toBeVisible();
+ });
it('should have breadecrumbs', async () => {
renderComponent();
- await mockStore();
+ await mockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByLabelText('Custom Page breadcrumbs')).toBeVisible();
});
it('should contain header row with title, add button and view live button', async () => {
renderComponent();
- await mockStore();
+ await mockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByText(messages.heading.defaultMessage)).toBeVisible();
expect(screen.getByTestId('header-add-button')).toBeVisible();
expect(screen.getByTestId('header-view-live-button')).toBeVisible();
});
it('should add new page when "add a new page button" is clicked', async () => {
renderComponent();
- await mockStore();
+ await mockStore(RequestStatus.SUCCESSFUL);
const addButton = screen.getByTestId('body-add-button');
expect(addButton).toBeVisible();
await act(async () => { fireEvent.click(addButton); });
@@ -95,7 +101,7 @@ describe('CustomPages', () => {
});
it('should open student view modal when "add a new page button" is clicked', async () => {
renderComponent();
- await mockStore();
+ await mockStore(RequestStatus.SUCCESSFUL);
const viewButton = screen.getByTestId('student-view-example-button');
expect(viewButton).toBeVisible();
expect(screen.queryByLabelText(messages.studentViewModalTitle.defaultMessage)).toBeNull();
@@ -104,7 +110,7 @@ describe('CustomPages', () => {
});
it('should update page order on drag', async () => {
renderComponent();
- await mockStore();
+ await mockStore(RequestStatus.SUCCESSFUL);
const buttons = await screen.queryAllByRole('button');
const draggableButton = buttons[9];
expect(draggableButton).toBeVisible();
diff --git a/src/custom-pages/data/slice.js b/src/custom-pages/data/slice.js
index cc1f247285..1e30f93187 100644
--- a/src/custom-pages/data/slice.js
+++ b/src/custom-pages/data/slice.js
@@ -11,7 +11,6 @@ const slice = createSlice({
savingStatus: '',
addingStatus: 'default',
deletingStatus: '',
- customPagesApiStatus: {},
},
reducers: {
setPageIds: (state, { payload }) => {
diff --git a/src/custom-pages/data/thunks.js b/src/custom-pages/data/thunks.js
index 23458efd9a..7950e3750a 100644
--- a/src/custom-pages/data/thunks.js
+++ b/src/custom-pages/data/thunks.js
@@ -39,9 +39,10 @@ export function fetchCustomPages(courseId) {
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
} catch (error) {
if (error.response && error.response.status === 403) {
- dispatch(updateCustomPagesApiStatus({ status: RequestStatus.DENIED }));
+ dispatch(updateLoadingStatus({ courseId, status: RequestStatus.DENIED }));
+ } else {
+ dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
}
- dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
}
};
}
diff --git a/src/custom-pages/factories/mockApiResponses.jsx b/src/custom-pages/factories/mockApiResponses.jsx
index 7acdf68b45..15f3882f6a 100644
--- a/src/custom-pages/factories/mockApiResponses.jsx
+++ b/src/custom-pages/factories/mockApiResponses.jsx
@@ -1,3 +1,5 @@
+import { RequestStatus } from '../../data/constants';
+
export const courseId = 'course-v1:edX+DemoX+Demo_Course';
export const initialState = {
@@ -62,3 +64,12 @@ export const generateNewPageApiResponse = () => ({
locator: 'mOckID2',
courseKey: courseId,
});
+
+export const getStatusValue = (status) => {
+ switch (status) {
+ case RequestStatus.DENIED:
+ return 403;
+ default:
+ return 200;
+ }
+};
diff --git a/src/i18n/index.js b/src/i18n/index.js
index d0dc802c3c..2abb2b7cf4 100644
--- a/src/i18n/index.js
+++ b/src/i18n/index.js
@@ -1,5 +1,5 @@
import { messages as footerMessages } from '@edx/frontend-component-footer';
-
+import { messages as paragonMessages } from '@edx/paragon';
import arMessages from './messages/ar.json';
import frMessages from './messages/fr.json';
import es419Messages from './messages/es_419.json';
@@ -35,5 +35,6 @@ const appMessages = {
export default [
footerMessages,
+ paragonMessages,
appMessages,
];
diff --git a/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx b/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx
index 73af4b9e0f..310fca7690 100644
--- a/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx
+++ b/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx
@@ -162,10 +162,12 @@ describe('DiscussionsSettings', () => {
expect(queryByTestId(container, 'appConfigForm')).toBeInTheDocument();
- userEvent.click(queryByText(container, appMessages.backButton.defaultMessage));
+ await act(() => userEvent.click(queryByText(container, appMessages.backButton.defaultMessage)));
- expect(queryByTestId(container, 'appList')).toBeInTheDocument();
- expect(queryByTestId(container, 'appConfigForm')).not.toBeInTheDocument();
+ await waitFor(() => {
+ expect(queryByTestId(container, 'appList')).toBeInTheDocument();
+ expect(queryByTestId(container, 'appConfigForm')).not.toBeInTheDocument();
+ });
});
test('successfully closes the modal', async () => {
diff --git a/src/pages-and-resources/discussions/app-list/AppList.jsx b/src/pages-and-resources/discussions/app-list/AppList.jsx
index 04509d3a21..4b1ad4eb2c 100644
--- a/src/pages-and-resources/discussions/app-list/AppList.jsx
+++ b/src/pages-and-resources/discussions/app-list/AppList.jsx
@@ -1,6 +1,10 @@
-import React, { useCallback, useEffect, useMemo } from 'react';
+import React, {
+ useCallback, useEffect, useMemo, useState, useContext,
+} from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { CardGrid, Container, breakpoints } from '@edx/paragon';
+import {
+ CardGrid, Container, breakpoints, Form, ActionRow, AlertModal, Button,
+} from '@edx/paragon';
import { useDispatch, useSelector } from 'react-redux';
import Responsive from 'react-responsive';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
@@ -14,16 +18,23 @@ import messages from './messages';
import FeaturesTable from './FeaturesTable';
import AppListNextButton from './AppListNextButton';
import Loading from '../../../generic/Loading';
+import useIsOnSmallScreen from '../data/hook';
+import { saveProviderConfig, fetchDiscussionSettings } from '../data/thunks';
+import { PagesAndResourcesContext } from '../../PagesAndResourcesProvider';
+import { discussionRestriction } from '../data/constants';
const AppList = ({ intl }) => {
const dispatch = useDispatch();
+ const { courseId } = useContext(PagesAndResourcesContext);
const {
- appIds, featureIds, status, activeAppId, selectedAppId,
+ appIds, featureIds, status, activeAppId, selectedAppId, enabled, postingRestrictions,
} = useSelector(state => state.discussions);
+ const [discussionEnabled, setDiscussionEnabled] = useState(enabled);
const apps = useModels('apps', appIds);
const features = useModels('features', featureIds);
const isGlobalStaff = getAuthenticatedUser().administrator;
const ltiProvider = !['openedx', 'legacy'].includes(activeAppId);
+ const isOnSmallcreen = useIsOnSmallScreen();
const showOneEdxProvider = useMemo(() => apps.filter(app => (
activeAppId === 'openedx' ? app.id !== 'legacy' : app.id !== 'openedx'
@@ -42,9 +53,48 @@ const AppList = ({ intl }) => {
dispatch(updateValidationStatus({ hasError: false }));
}, [selectedAppId, activeAppId]);
+ useEffect(() => {
+ setDiscussionEnabled(enabled);
+ }, [enabled]);
+
+ useEffect(() => {
+ if (!postingRestrictions) {
+ dispatch(fetchDiscussionSettings(courseId, selectedAppId));
+ }
+ }, [courseId]);
+
const handleSelectApp = useCallback((appId) => {
dispatch(selectApp({ appId }));
- }, [selectedAppId]);
+ }, []);
+
+ const updateSettings = useCallback((enabledDiscussion) => {
+ dispatch(saveProviderConfig(
+ courseId,
+ selectedAppId,
+ {
+ enabled: enabledDiscussion,
+ postingRestrictions:
+ enabledDiscussion ? postingRestrictions : discussionRestriction.ENABLED,
+ },
+ ));
+ }, [courseId, selectedAppId, postingRestrictions]);
+
+ const handleClose = useCallback(() => {
+ setDiscussionEnabled(enabled);
+ }, [enabled]);
+
+ const handleOk = useCallback(() => {
+ setDiscussionEnabled(false);
+ updateSettings(false);
+ }, [updateSettings]);
+
+ const handleChange = useCallback((e) => {
+ const toggleVal = e.target.checked;
+ setDiscussionEnabled(!toggleVal);
+ if (!toggleVal) {
+ updateSettings(!toggleVal);
+ }
+ }, [updateSettings]);
if (!selectedAppId || status === LOADING) {
return (
@@ -71,10 +121,21 @@ const AppList = ({ intl }) => {
));
return (
-
-
- {intl.formatMessage(messages.heading)}
-
+
+
+
+ {intl.formatMessage(messages.heading)}
+
+
+ Hide discussion tab
+
+
{
lg: 4,
xl: 4,
}}
+ className={!isOnSmallcreen && 'mt-5'}
>
{(isGlobalStaff || ltiProvider) ? showAppCard(apps) : showAppCard(showOneEdxProvider)}
@@ -96,6 +158,23 @@ const AppList = ({ intl }) => {
/>
+
+
+
+
+ )}
+ >
+
+ {intl.formatMessage(messages.hideDiscussionTabMessage)}
+
+
);
};
diff --git a/src/pages-and-resources/discussions/app-list/AppList.scss b/src/pages-and-resources/discussions/app-list/AppList.scss
index 0182483c85..83d3c3c915 100644
--- a/src/pages-and-resources/discussions/app-list/AppList.scss
+++ b/src/pages-and-resources/discussions/app-list/AppList.scss
@@ -46,10 +46,39 @@
padding: 0 7px;
}
+.line-height-24 {
+ line-height: 24px !important;
+}
+
+.hide-discussion-modal {
+ .pgn__modal-header {
+ padding-top: 24px;
+
+ h2 {
+ color: $primary-500;
+ line-height: 28px;
+ font-size: 22px;
+ }
+ }
+
+ .bg-black {
+ color: #000000;
+ }
+
+ .pgn__modal-footer {
+ padding-top: 8px;
+ padding-bottom: 24px;
+ }
+
+ button {
+ font-weight: 500;
+ }
+}
+
.discussion-restriction {
.unselected-button {
&:hover {
- background: #E9E6E4 !important;
+ background: #E9E6E4;
}
}
diff --git a/src/pages-and-resources/discussions/app-list/AppList.test.jsx b/src/pages-and-resources/discussions/app-list/AppList.test.jsx
index 08a1b3e421..9bfa74d8f7 100644
--- a/src/pages-and-resources/discussions/app-list/AppList.test.jsx
+++ b/src/pages-and-resources/discussions/app-list/AppList.test.jsx
@@ -1,7 +1,7 @@
/* eslint-disable react/jsx-no-constructed-context-values */
import React from 'react';
import {
- render, screen, within, queryAllByRole,
+ render, screen, within, queryAllByRole, waitFor,
} from '@testing-library/react';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
@@ -69,36 +69,47 @@ describe('AppList', () => {
test('display a card for each available app', async () => {
renderComponent();
- const appCount = store.getState().discussions.appIds.length;
- expect(screen.queryAllByRole('radio')).toHaveLength(appCount);
+
+ await waitFor(async () => {
+ const appCount = await store.getState().discussions.appIds.length;
+ expect(screen.queryAllByRole('radio')).toHaveLength(appCount);
+ });
});
test('displays the FeaturesTable at desktop sizes', async () => {
renderComponent();
- expect(screen.queryByRole('table')).toBeInTheDocument();
+ await waitFor(() => expect(screen.queryByRole('table')).toBeInTheDocument());
});
test('hides the FeaturesTable at mobile sizes', async () => {
renderComponent(breakpoints.extraSmall.maxWidth);
- expect(screen.queryByRole('table')).not.toBeInTheDocument();
+ await waitFor(() => expect(screen.queryByRole('table')).not.toBeInTheDocument());
});
test('hides the FeaturesList at desktop sizes', async () => {
renderComponent();
- expect(screen.queryByText(messages['supportedFeatureList-mobile-show'].defaultMessage)).not.toBeInTheDocument();
+ await waitFor(() => expect(screen.queryByText(messages['supportedFeatureList-mobile-show'].defaultMessage))
+ .not.toBeInTheDocument());
});
test('displays the FeaturesList at mobile sizes', async () => {
renderComponent(breakpoints.extraSmall.maxWidth);
- const appCount = store.getState().discussions.appIds.length;
- expect(screen.queryAllByText(messages['supportedFeatureList-mobile-show'].defaultMessage)).toHaveLength(appCount);
+
+ await waitFor(async () => {
+ const appCount = await store.getState().discussions.appIds.length;
+ expect(screen.queryAllByText(messages['supportedFeatureList-mobile-show'].defaultMessage))
+ .toHaveLength(appCount);
+ });
});
test('selectApp is called when an app is clicked', async () => {
renderComponent();
- userEvent.click(screen.getByLabelText('Select Piazza'));
- const clickedCard = screen.getByRole('radio', { checked: true });
- expect(within(clickedCard).queryByLabelText('Select Piazza')).toBeInTheDocument();
+
+ await waitFor(() => {
+ userEvent.click(screen.getByLabelText('Select Piazza'));
+ const clickedCard = screen.getByRole('radio', { checked: true });
+ expect(within(clickedCard).queryByLabelText('Select Piazza')).toBeInTheDocument();
+ });
});
});
@@ -121,7 +132,7 @@ describe('AppList', () => {
test('does not display two edx providers card for non admin role', async () => {
renderComponent();
const appCount = store.getState().discussions.appIds.length;
- expect(queryAllByRole(container, 'radio')).toHaveLength(appCount - 1);
+ await waitFor(() => expect(queryAllByRole(container, 'radio')).toHaveLength(appCount - 1));
});
});
});
diff --git a/src/pages-and-resources/discussions/app-list/messages.js b/src/pages-and-resources/discussions/app-list/messages.js
index 531d333cbb..96b882e3cf 100644
--- a/src/pages-and-resources/discussions/app-list/messages.js
+++ b/src/pages-and-resources/discussions/app-list/messages.js
@@ -239,6 +239,26 @@ const messages = defineMessages({
defaultMessage: 'Commonly requested',
description: 'The type of a discussions feature.',
},
+ hideDiscussionTabTitle: {
+ id: 'authoring.discussions.hide-tab-title',
+ defaultMessage: 'Hide the discussion tab?',
+ description: 'Title message to hide discussion tab',
+ },
+ hideDiscussionTabMessage: {
+ id: 'authoring.discussions.hide-tab-message',
+ defaultMessage: 'The discussion tab will no longer be visible to learners in the LMS. Additionally, posting to the discussion forums will be disabled. Are you sure you want to proceed?',
+ description: 'Help message to hide discussion tab',
+ },
+ hideDiscussionOkButton: {
+ id: 'authoring.discussions.hide-ok-button',
+ defaultMessage: 'Ok',
+ description: 'Ok button title',
+ },
+ hideDiscussionCancelButton: {
+ id: 'authoring.discussions.hide-cancel-button',
+ defaultMessage: 'Cancel',
+ description: 'Cancel button title',
+ },
});
export default messages;
diff --git a/src/pages-and-resources/discussions/data/api.js b/src/pages-and-resources/discussions/data/api.js
index a960b6a272..12d0177f87 100644
--- a/src/pages-and-resources/discussions/data/api.js
+++ b/src/pages-and-resources/discussions/data/api.js
@@ -233,7 +233,7 @@ function denormalizeData(courseId, appId, data) {
const apiData = {
context_key: courseId,
- enabled: true,
+ enabled: data.enabled,
lti_configuration: ltiConfiguration,
plugin_configuration: pluginConfiguration,
provider_type: appId,
diff --git a/src/pages-and-resources/discussions/data/hook.js b/src/pages-and-resources/discussions/data/hook.js
new file mode 100644
index 0000000000..f6b45f40ba
--- /dev/null
+++ b/src/pages-and-resources/discussions/data/hook.js
@@ -0,0 +1,6 @@
+import { breakpoints, useWindowSize } from '@edx/paragon';
+
+export default function useIsOnSmallScreen() {
+ const windowSize = useWindowSize();
+ return windowSize.width < breakpoints.medium.minWidth;
+}
diff --git a/src/pages-and-resources/discussions/data/redux.test.js b/src/pages-and-resources/discussions/data/redux.test.js
index 9eb72e0232..baf6ff3d6f 100644
--- a/src/pages-and-resources/discussions/data/redux.test.js
+++ b/src/pages-and-resources/discussions/data/redux.test.js
@@ -2,6 +2,7 @@ import { history } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { initializeMockApp } from '@edx/frontend-platform/testing';
import MockAdapter from 'axios-mock-adapter';
+import { waitFor } from '@testing-library/react';
import { DivisionSchemes } from '../../../data/constants';
import { LOADED } from '../../../data/slice';
import initializeStore from '../../../store';
@@ -371,23 +372,25 @@ describe('Data layer integration tests', () => {
pagesAndResourcesPath,
), store.dispatch);
- expect(window.location.pathname).toEqual(pagesAndResourcesPath);
- expect(store.getState().discussions).toEqual(
- expect.objectContaining({
- appIds: ['legacy', 'openedx', 'piazza', 'discourse'],
- featureIds,
- activeAppId: 'piazza',
- selectedAppId: 'piazza',
- status: LOADED,
- saveStatus: SAVED,
- hasValidationError: false,
- }),
- );
- expect(store.getState().models.appConfigs.piazza).toEqual({
- id: 'piazza',
- consumerKey: 'new_consumer_key',
- consumerSecret: 'new_consumer_secret',
- launchUrl: 'https://localhost/new_launch_url',
+ waitFor(() => {
+ expect(window.location.pathname).toEqual(pagesAndResourcesPath);
+ expect(store.getState().discussions).toEqual(
+ expect.objectContaining({
+ appIds: ['legacy', 'openedx', 'piazza', 'discourse'],
+ featureIds,
+ activeAppId: 'piazza',
+ selectedAppId: 'piazza',
+ status: LOADED,
+ saveStatus: SAVED,
+ hasValidationError: false,
+ }),
+ );
+ expect(store.getState().models.appConfigs.piazza).toEqual({
+ id: 'piazza',
+ consumerKey: 'new_consumer_key',
+ consumerSecret: 'new_consumer_secret',
+ launchUrl: 'https://localhost/new_launch_url',
+ });
});
});
@@ -465,35 +468,37 @@ describe('Data layer integration tests', () => {
},
pagesAndResourcesPath,
), store.dispatch);
- expect(window.location.pathname).toEqual(pagesAndResourcesPath);
- expect(store.getState().discussions).toEqual(
- expect.objectContaining({
- appIds: ['legacy', 'openedx', 'piazza', 'discourse'],
- featureIds,
- activeAppId: 'legacy',
- selectedAppId: 'legacy',
- status: LOADED,
- saveStatus: SAVED,
- hasValidationError: false,
- divideDiscussionIds,
- discussionTopicIds,
- }),
- );
- expect(store.getState().models.appConfigs.legacy).toEqual({
- id: 'legacy',
- // These three fields should be updated.
- allowAnonymousPosts: true,
- allowAnonymousPostsPeers: true,
- reportedContentEmailNotifications: true,
- alwaysDivideInlineDiscussions: true,
- restrictedDates: [],
- // TODO: Note! The values we tried to save were ignored, this test reflects what currently
- // happens, but NOT what we want to have happen!
- divideByCohorts: true,
- divisionScheme: DivisionSchemes.COHORT,
- cohortsEnabled: false,
- allowDivisionByUnit: false,
- divideCourseTopicsByCohorts: true,
+ waitFor(() => {
+ expect(window.location.pathname).toEqual(pagesAndResourcesPath);
+ expect(store.getState().discussions).toEqual(
+ expect.objectContaining({
+ appIds: ['legacy', 'openedx', 'piazza', 'discourse'],
+ featureIds,
+ activeAppId: 'legacy',
+ selectedAppId: 'legacy',
+ status: LOADED,
+ saveStatus: SAVED,
+ hasValidationError: false,
+ divideDiscussionIds,
+ discussionTopicIds,
+ }),
+ );
+ expect(store.getState().models.appConfigs.legacy).toEqual({
+ id: 'legacy',
+ // These three fields should be updated.
+ allowAnonymousPosts: true,
+ allowAnonymousPostsPeers: true,
+ reportedContentEmailNotifications: true,
+ alwaysDivideInlineDiscussions: true,
+ restrictedDates: [],
+ // TODO: Note! The values we tried to save were ignored, this test reflects what currently
+ // happens, but NOT what we want to have happen!
+ divideByCohorts: true,
+ divisionScheme: DivisionSchemes.COHORT,
+ cohortsEnabled: false,
+ allowDivisionByUnit: false,
+ divideCourseTopicsByCohorts: true,
+ });
});
});
});
diff --git a/src/pages-and-resources/discussions/data/slice.js b/src/pages-and-resources/discussions/data/slice.js
index 3029b00ecf..8608d30fe4 100644
--- a/src/pages-and-resources/discussions/data/slice.js
+++ b/src/pages-and-resources/discussions/data/slice.js
@@ -29,6 +29,7 @@ const slice = createSlice({
enableGradedUnits: false,
unitLevelVisibility: false,
postingRestrictions: null,
+ enabled: true,
},
reducers: {
loadApps: (state, { payload }) => {
diff --git a/src/pages-and-resources/pages/PageCard.jsx b/src/pages-and-resources/pages/PageCard.jsx
index 5891b01ce6..4d7458bbdb 100644
--- a/src/pages-and-resources/pages/PageCard.jsx
+++ b/src/pages-and-resources/pages/PageCard.jsx
@@ -35,18 +35,6 @@ const PageCard = ({
// eslint-disable-next-line react/no-unstable-nested-components
const SettingsButton = () => {
if (page.legacyLink) {
- if (process.env.ENABLE_NEW_CUSTOM_PAGES === 'true' && page.name === 'Custom pages') {
- return (
-
-
-
- );
- }
return (
{
renderComponent();
expect(queryAllByRole(container, 'button')).toHaveLength(3);
});
- it('should navigate to custom-pages', async () => {
- renderComponent();
- const [customPagesSettingsButton] = queryAllByRole(container, 'link');
- expect(customPagesSettingsButton).toHaveAttribute('href', 'custom-pages');
- });
it('should navigate to legacyLink', async () => {
renderComponent();
const textbookSettingsButton = queryAllByRole(container, 'link')[1];
diff --git a/src/studio-header/Header.jsx b/src/studio-header/Header.jsx
index 500937d65e..650bb4e5b0 100644
--- a/src/studio-header/Header.jsx
+++ b/src/studio-header/Header.jsx
@@ -41,7 +41,7 @@ const Header = ({
{intl.formatMessage(messages['header.links.updates'])}