diff --git a/.env b/.env
index 410a9f750..9332343bb 100644
--- a/.env
+++ b/.env
@@ -6,7 +6,6 @@ CSRF_TOKEN_API_PATH=null
DISCOVERY_API_BASE_URL=
ECOMMERCE_BASE_URL=null
FAVICON_URL=''
-FAVICON_URL=null
LANGUAGE_PREFERENCE_COOKIE_NAME=null
LMS_BASE_URL=null
LOGIN_URL=null
diff --git a/.env.test b/.env.test
index 0e50cf2d7..b2e446dfa 100644
--- a/.env.test
+++ b/.env.test
@@ -5,7 +5,6 @@ CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DISCOVERY_API_BASE_URL='http://localhost:18381'
ECOMMERCE_BASE_URL='http://localhost:18130'
FAVICON_URL='https://edx-cdn.org/v3/default/favicon.ico'
-FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
diff --git a/package.json b/package.json
index 3434b7458..f49ab4c53 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"is-es5": "es-check es5 ./dist/*.js",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
+ "lint:fix": "fedx-scripts eslint --ext .js --ext .jsx . --fix",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests"
diff --git a/src/data/constants.js b/src/data/constants.js
new file mode 100644
index 000000000..2feca441c
--- /dev/null
+++ b/src/data/constants.js
@@ -0,0 +1,13 @@
+/* eslint-disable import/prefer-default-export */
+
+/**
+ * Enum for request status.
+ * @readonly
+ * @enum {string}
+ */
+export const RequestStatus = {
+ IN_PROGRESS: 'in-progress',
+ SUCCESSFUL: 'successful',
+ FAILED: 'failed',
+ DENIED: 'denied',
+};
diff --git a/src/generic/FormSwitchGroup.jsx b/src/generic/FormSwitchGroup.jsx
index b0eeb78fd..c8604cf47 100644
--- a/src/generic/FormSwitchGroup.jsx
+++ b/src/generic/FormSwitchGroup.jsx
@@ -1,9 +1,16 @@
-import React from 'react';
-import PropTypes from 'prop-types';
import { Form, SwitchControl } from '@edx/paragon';
+import PropTypes from 'prop-types';
+import React from 'react';
export default function FormSwitchGroup({
- id, label, helpText, className, onChange, onBlur, checked,
+ id,
+ name,
+ label,
+ helpText,
+ className,
+ onChange,
+ onBlur,
+ checked,
}) {
const helpTextId = `${id}HelpText`;
@@ -15,28 +22,27 @@ export default function FormSwitchGroup({
controlId={id}
className={className}
>
-
-
-
+
+
+
{label}
-
- {helpText}
-
-
-
+
+ {helpText}
+
@@ -44,13 +50,16 @@ export default function FormSwitchGroup({
}
FormSwitchGroup.propTypes = {
id: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
- helpText: PropTypes.string.isRequired,
+ label: PropTypes.node.isRequired,
+ name: PropTypes.string,
+ helpText: PropTypes.node.isRequired,
className: PropTypes.string,
onChange: PropTypes.func.isRequired,
- onBlur: PropTypes.func.isRequired,
+ onBlur: PropTypes.func,
checked: PropTypes.bool.isRequired,
};
FormSwitchGroup.defaultProps = {
className: null,
+ onBlur: null,
+ name: null,
};
diff --git a/src/generic/status-badge/StatusBadge.jsx b/src/generic/status-badge/StatusBadge.jsx
new file mode 100644
index 000000000..0cb4740ab
--- /dev/null
+++ b/src/generic/status-badge/StatusBadge.jsx
@@ -0,0 +1,29 @@
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { Badge } from '@edx/paragon';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import messages from './messages';
+
+function StatusBadge({ intl, status, label }) {
+ return (
+ <>
+ {label && `${label} `}
+ {status
+ ? {intl.formatMessage(messages.enabled)}
+ : {intl.formatMessage(messages.disabled)}}
+ >
+ );
+}
+
+StatusBadge.propTypes = {
+ intl: intlShape.isRequired,
+ status: PropTypes.bool.isRequired,
+ label: PropTypes.string,
+};
+
+StatusBadge.defaultProps = {
+ label: null,
+};
+
+export default injectIntl(StatusBadge);
diff --git a/src/generic/status-badge/messages.js b/src/generic/status-badge/messages.js
new file mode 100644
index 000000000..321e7a6a9
--- /dev/null
+++ b/src/generic/status-badge/messages.js
@@ -0,0 +1,15 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ enabled: {
+ id: 'course-authoring.badge.enabled',
+ defaultMessage: 'Enabled',
+ },
+ disabled: {
+ id: 'course-authoring.badge.disabled',
+ defaultMessage: 'Disabled',
+ },
+
+});
+
+export default messages;
diff --git a/src/index.jsx b/src/index.jsx
index b84b9ca41..574bb9be8 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -45,6 +45,7 @@ initialize({
config: () => {
mergeConfig({
SUPPORT_URL: process.env.SUPPORT_URL || null,
+ CALCULATOR_HELP_URL: process.env.CALCULATOR_HELP_URL || null,
}, 'CourseAuthoringConfig');
},
},
diff --git a/src/index.scss b/src/index.scss
index 93c7415a8..f6bdd4fba 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -7,4 +7,4 @@
@import "~@edx/frontend-component-footer/dist/footer";
@import "proctored-exam-settings/proctoredExamSettings";
-@import "pages-and-resources/discussions/app-list/AppList";
\ No newline at end of file
+@import "pages-and-resources/discussions/app-list/AppList";
diff --git a/src/pages-and-resources/PagesAndResources.jsx b/src/pages-and-resources/PagesAndResources.jsx
index 511f84cf0..4bb8e00f7 100644
--- a/src/pages-and-resources/PagesAndResources.jsx
+++ b/src/pages-and-resources/PagesAndResources.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, Suspense } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { PageRoute } from '@edx/frontend-platform/react';
@@ -12,40 +12,60 @@ import DiscussionsSettings from './discussions';
import PageGrid from './pages/PageGrid';
import ResourceList from './resources/ResourcesList';
-import { fetchPages } from './data/thunks';
+import { fetchCourseApps } from './data/thunks';
import { useModels } from '../generic/model-store';
import PagesAndResourcesProvider from './PagesAndResourcesProvider';
function PagesAndResources({ courseId, intl }) {
- const { path } = useRouteMatch();
+ const { path, url } = useRouteMatch();
const dispatch = useDispatch();
useEffect(() => {
- dispatch(fetchPages(courseId));
+ dispatch(fetchCourseApps(courseId));
}, [courseId]);
- const pageIds = useSelector(state => state.pagesAndResources.pageIds);
- const pages = useModels('pages', pageIds);
+ const courseAppIds = useSelector(state => state.pagesAndResources.courseAppIds);
+ // Each page here is driven by a course app
+ const pages = useModels('courseApps', courseAppIds);
return (
-
-
-
-
{intl.formatMessage(messages.heading)}
-
-
-
-
+
+ {intl.formatMessage(messages.heading)}
+
+
+
+ {
+ ({ match, history }) => {
+ const SettingsComponent = React.lazy(async () => {
+ try {
+ // There seems to be a bug in babel-eslint that causes the checker to crash with the following error
+ // if we use a template string here:
+ // TypeError: Cannot read property 'range' of null with using template strings here.
+ // Ref: https://github.com/babel/babel-eslint/issues/530
+ return await import('./' + match.params.appId + '/Settings.jsx'); // eslint-disable-line
+ } catch (error) {
+ console.trace(error); // eslint-disable-line no-console
+ return null;
+ }
+ });
+ return (
+
+ history.push(url)} />
+
+ );
+ }
+ }
+
diff --git a/src/pages-and-resources/PagesAndResourcesProvider.jsx b/src/pages-and-resources/PagesAndResourcesProvider.jsx
index 0b34d9ed8..b96983291 100644
--- a/src/pages-and-resources/PagesAndResourcesProvider.jsx
+++ b/src/pages-and-resources/PagesAndResourcesProvider.jsx
@@ -7,6 +7,7 @@ export default function PagesAndResourcesProvider({ courseId, children }) {
return (
diff --git a/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx b/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx
new file mode 100644
index 000000000..7ce46a506
--- /dev/null
+++ b/src/pages-and-resources/app-settings-modal/AppSettingsModal.jsx
@@ -0,0 +1,176 @@
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import {
+ Button, Form, Hyperlink, ModalLayer, Spinner, TransitionReplace,
+} from '@edx/paragon';
+import { Formik } from 'formik';
+import PropTypes from 'prop-types';
+import React, { useContext } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import * as Yup from 'yup';
+import { RequestStatus } from '../../data/constants';
+import FormSwitchGroup from '../../generic/FormSwitchGroup';
+import { useModel } from '../../generic/model-store';
+import StatusBadge from '../../generic/status-badge/StatusBadge';
+import { getLoadingStatus } from '../data/selectors';
+import { updateAppStatus } from '../data/thunks';
+import AppConfigFormDivider from '../discussions/app-config-form/apps/shared/AppConfigFormDivider';
+import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
+import messages from './messages';
+
+function AppSettingsForm({ formikProps, children }) {
+ return children && (
+
+ {formikProps.values.enabled
+ ? (
+
+ {children(formikProps)}
+
+ ) : (
+
+ )}
+
+ );
+}
+
+AppSettingsForm.propTypes = {
+ formikProps: PropTypes.shape({
+ values: PropTypes.shape({ enabled: PropTypes.bool.isRequired }),
+ }).isRequired,
+ children: PropTypes.func,
+};
+
+AppSettingsForm.defaultProps = {
+ children: null,
+};
+
+function AppSettingsModal({
+ intl,
+ appId,
+ title,
+ children,
+ initialValues,
+ validationSchema,
+ onClose,
+ onSettingsSave,
+ enableAppLabel,
+ enableAppHelp,
+ learnMoreURL,
+ learnMoreText,
+}) {
+ const { courseId } = useContext(PagesAndResourcesContext);
+ const loadingStatus = useSelector(getLoadingStatus);
+ const appInfo = useModel('courseApps', appId);
+ const dispatch = useDispatch();
+ const handleFormSubmit = (values) => {
+ // If the app's enabled/disabled loadingStatus has changed, set that first.
+ if (appInfo.enabled !== values.enabled) {
+ dispatch(updateAppStatus(courseId, appInfo.id, values.enabled, true));
+ }
+ // Call the submit handler for the settings component to save its settings
+ if (onSettingsSave) {
+ onSettingsSave();
+ }
+ };
+ const learnMoreLink = (
+
+ {learnMoreText}
+
+ );
+
+ return (
+
+
+ {
+ loadingStatus === RequestStatus.SUCCESSFUL && (
+
{(formikProps) => (
+
+ )}
+
+ )
+ }
+ {loadingStatus === RequestStatus.IN_PROGRESS && (
+
+ )}
+
+
+ );
+}
+
+AppSettingsModal.propTypes = {
+ intl: intlShape.isRequired,
+ title: PropTypes.string.isRequired,
+ appId: PropTypes.string.isRequired,
+ children: PropTypes.func,
+ onSettingsSave: PropTypes.func,
+ initialValues: PropTypes.objectOf(PropTypes.any),
+ validationSchema: PropTypes.objectOf(PropTypes.func),
+ onClose: PropTypes.func.isRequired,
+ enableAppLabel: PropTypes.string.isRequired,
+ enableAppHelp: PropTypes.string.isRequired,
+ learnMoreURL: PropTypes.string.isRequired,
+ learnMoreText: PropTypes.string.isRequired,
+};
+
+AppSettingsModal.defaultProps = {
+ children: null,
+ onSettingsSave: null,
+ initialValues: {},
+ validationSchema: {},
+};
+
+export default injectIntl(AppSettingsModal);
diff --git a/src/pages-and-resources/app-settings-modal/messages.js b/src/pages-and-resources/app-settings-modal/messages.js
new file mode 100644
index 000000000..db2dd4e0c
--- /dev/null
+++ b/src/pages-and-resources/app-settings-modal/messages.js
@@ -0,0 +1,35 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ cancel: {
+ id: 'course-authoring.pages-resources.app-settings-modal.button.cancel',
+ defaultMessage: 'Cancel',
+ },
+ apply: {
+ id: 'course-authoring.pages-resources.app-settings-modal.button.apply',
+ defaultMessage: 'Apply',
+ },
+ applying: {
+ id: 'course-authoring.pages-resources.app-settings-modal.button.applying',
+ defaultMessage: 'Applying',
+ },
+ applied: {
+ id: 'course-authoring.pages-resources.app-settings-modal.button.applied',
+ defaultMessage: 'Applied',
+ },
+ retry: {
+ id: 'course-authoring.pages-resources.app-settings-modal.button.retry',
+ defaultMessage: 'Retry',
+ },
+ enabled: {
+ id: 'course-authoring.pages-resources.app-settings-modal.badge.enabled',
+ defaultMessage: 'Enabled',
+ },
+ disabled: {
+ id: 'course-authoring.pages-resources.app-settings-modal.badge.disabled',
+ defaultMessage: 'Disabled',
+ },
+
+});
+
+export default messages;
diff --git a/src/pages-and-resources/calculator/Settings.jsx b/src/pages-and-resources/calculator/Settings.jsx
new file mode 100644
index 000000000..54fa5c204
--- /dev/null
+++ b/src/pages-and-resources/calculator/Settings.jsx
@@ -0,0 +1,31 @@
+import { getConfig } from '@edx/frontend-platform';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import PropTypes from 'prop-types';
+import React from 'react';
+import AppSettingsModal from '../app-settings-modal/AppSettingsModal';
+
+import messages from './messages';
+
+const CALCULATOR_HELP_URL = getConfig().CALCULATOR_HELP_URL
+ || 'https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/exercises_tools/calculator.html';
+
+function CalculatorSettings({ intl, onClose }) {
+ return (
+
+ );
+}
+
+CalculatorSettings.propTypes = {
+ intl: intlShape.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
+
+export default injectIntl(CalculatorSettings);
diff --git a/src/pages-and-resources/calculator/messages.js b/src/pages-and-resources/calculator/messages.js
new file mode 100644
index 000000000..4a8c0627f
--- /dev/null
+++ b/src/pages-and-resources/calculator/messages.js
@@ -0,0 +1,24 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ heading: {
+ id: 'course-authoring.pages-resources.calculator.heading',
+ defaultMessage: 'Configure calculator',
+ },
+ enableProgressLabel: {
+ id: 'course-authoring.pages-resources.calculator.enable-calculator.label',
+ defaultMessage: 'Calculator',
+ },
+ enableProgressHelp: {
+ id: 'course-authoring.pages-resources.calculator.enable-calculator.help',
+ defaultMessage: `The calculator supports numbers, operators, constants,
+ functions, and other mathematical concepts. When enabled, an icon to
+ access the calculator appears on all pages in the body of your course.`,
+ },
+ enableProgressLink: {
+ id: 'course-authoring.pages-resources.calculator.enable-calculator.link',
+ defaultMessage: 'Learn more about the calculator',
+ },
+});
+
+export default messages;
diff --git a/src/pages-and-resources/data/api.js b/src/pages-and-resources/data/api.js
index df51949f7..5d67a064d 100644
--- a/src/pages-and-resources/data/api.js
+++ b/src/pages-and-resources/data/api.js
@@ -1,61 +1,39 @@
/* eslint-disable import/prefer-default-export */
-export function getPages() {
- return Promise.resolve({
- pages: [
- {
- id: 'discussions',
- title: 'Discussions',
- isEnabled: false,
- showSettings: false,
- showStatus: false,
- showEnable: true,
- description: 'Encourage participation and engagement in your course with discussion forums.',
- },
- {
- id: 'teams',
- title: 'Teams',
- isEnabled: true,
- showSettings: true,
- showStatus: true,
- showEnable: false,
- description: 'Leverage teams to allow learners to connect by topic of interest.',
- },
- {
- id: 'progress',
- title: 'Progress',
- isEnabled: false,
- showSettings: true,
- showStatus: true,
- showEnable: false,
- description: 'Allow students to track their progress throughout the course lorem ipsum.',
- },
- {
- id: 'textbooks',
- title: 'Textbooks',
- isEnabled: true,
- showSettings: true,
- showStatus: true,
- showEnable: false,
- description: 'Provide links to applicable resources for your course.',
- },
- {
- id: 'notes',
- title: 'Notes',
- isEnabled: true,
- showSettings: true,
- showStatus: true,
- showEnable: false,
- description: 'Support individual note taking that is visible only to the students.',
- },
- {
- id: 'wiki',
- title: 'Wiki',
- isEnabled: false,
- showSettings: false,
- showStatus: false,
- showEnable: true,
- description: 'Share your wiki content to provide additional course material.',
- },
- ],
- });
+import { camelCaseObject, ensureConfig, getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+
+ensureConfig([
+ 'STUDIO_BASE_URL',
+], 'Course Apps API service');
+
+const apiBaseUrl = getConfig().STUDIO_BASE_URL;
+
+const courseAppsApiUrl = `${apiBaseUrl}/api/course_apps/v1/apps`;
+
+/**
+ * Fetches the course apps installed for provided course
+ * @param {string} courseId
+ * @returns {Promise<[{}]>}
+ */
+export async function getCourseApps(courseId) {
+ const { data } = await getAuthenticatedHttpClient()
+ .get(`${courseAppsApiUrl}/${courseId}`);
+ return camelCaseObject(data);
+}
+
+/**
+ * Updates the status of a course app.
+ * @param {string} courseId Course ID for the course to operate on
+ * @param {string} appId ID for the application to operate on
+ * @param {boolean} state The new state
+ */
+export async function updateCourseApp(courseId, appId, state) {
+ await getAuthenticatedHttpClient()
+ .patch(
+ `${courseAppsApiUrl}/${courseId}`,
+ {
+ id: appId,
+ enabled: state,
+ },
+ );
}
diff --git a/src/pages-and-resources/data/selectors.js b/src/pages-and-resources/data/selectors.js
new file mode 100644
index 000000000..ce02809ad
--- /dev/null
+++ b/src/pages-and-resources/data/selectors.js
@@ -0,0 +1,4 @@
+/* eslint-disable import/prefer-default-export */
+
+export const getLoadingStatus = (status) => status.pagesAndResources.loadingStatus;
+export const getSavingStatus = (status) => status.pagesAndResources.savingStatus;
diff --git a/src/pages-and-resources/data/slice.js b/src/pages-and-resources/data/slice.js
index a2c8d2de5..c84533fcb 100644
--- a/src/pages-and-resources/data/slice.js
+++ b/src/pages-and-resources/data/slice.js
@@ -1,29 +1,31 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
-
-export const LOADING = 'LOADING';
-export const LOADED = 'LOADED';
-export const FAILED = 'FAILED';
+import { RequestStatus } from '../../data/constants';
const slice = createSlice({
name: 'pagesAndResources',
initialState: {
- pageIds: [],
- status: LOADING,
+ courseAppIds: [],
+ loadingStatus: RequestStatus.IN_PROGRESS,
+ savingStatus: RequestStatus.SUCCESSFUL,
},
reducers: {
- fetchPagesSuccess: (state, { payload }) => {
- state.pageIds = payload.pageIds;
+ fetchCourseAppsSuccess: (state, { payload }) => {
+ state.courseAppIds = payload.courseAppIds;
},
- updateStatus: (state, { payload }) => {
- state.status = payload.status;
+ updateLoadingStatus: (state, { payload }) => {
+ state.loadingStatus = payload.status;
+ },
+ updateSavingStatus: (state, { payload }) => {
+ state.savingStatus = payload.status;
},
},
});
export const {
- fetchPagesSuccess,
- updateStatus,
+ fetchCourseAppsSuccess,
+ updateLoadingStatus,
+ updateSavingStatus,
} = slice.actions;
export const {
diff --git a/src/pages-and-resources/data/thunks.js b/src/pages-and-resources/data/thunks.js
index 046df678f..4f19cc14e 100644
--- a/src/pages-and-resources/data/thunks.js
+++ b/src/pages-and-resources/data/thunks.js
@@ -1,33 +1,47 @@
+import { RequestStatus } from '../../data/constants';
import {
- getPages,
+ getCourseApps,
+ updateCourseApp,
} from './api';
-import { addModels } from '../../generic/model-store';
+import { addModels, updateModel } from '../../generic/model-store';
import {
- FAILED,
- fetchPagesSuccess,
- LOADING,
- updateStatus,
- LOADED,
+ fetchCourseAppsSuccess,
+ updateLoadingStatus,
+ updateSavingStatus,
} from './slice';
/* eslint-disable import/prefer-default-export */
-export function fetchPages(courseId) {
+export function fetchCourseApps(courseId) {
return async (dispatch) => {
- dispatch(updateStatus({ courseId, status: LOADING }));
+ dispatch(updateLoadingStatus({ courseId, status: RequestStatus.IN_PROGRESS }));
try {
- const { pages } = await getPages(courseId);
+ const courseApps = await getCourseApps(courseId);
- dispatch(addModels({ modelType: 'pages', models: pages }));
- dispatch(fetchPagesSuccess({
- pageIds: pages.map(page => page.id),
+ dispatch(addModels({ modelType: 'courseApps', models: courseApps }));
+ dispatch(fetchCourseAppsSuccess({
+ courseAppIds: courseApps.map(courseApp => courseApp.id),
}));
- dispatch(updateStatus({ courseId, status: LOADED }));
+ dispatch(updateLoadingStatus({ courseId, status: RequestStatus.SUCCESSFUL }));
} catch (error) {
// TODO: We need generic error handling in the app for when a request just fails... in other
// parts of the app (proctored exam settings) we show a nice message and ask the user to
// reload/try again later.
- dispatch(updateStatus({ courseId, status: FAILED }));
+ dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
+ }
+ };
+}
+
+export function updateAppStatus(courseId, appId, state) {
+ return async (dispatch) => {
+ dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
+
+ try {
+ await updateCourseApp(courseId, appId, state);
+ dispatch(updateModel({ modelType: 'courseApps', model: { id: appId, enabled: state } }));
+ dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
+ } catch (error) {
+ dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
}
};
}
diff --git a/src/pages-and-resources/discussions/DiscussionsSettings.jsx b/src/pages-and-resources/discussions/DiscussionsSettings.jsx
index dd88331c1..57f985344 100644
--- a/src/pages-and-resources/discussions/DiscussionsSettings.jsx
+++ b/src/pages-and-resources/discussions/DiscussionsSettings.jsx
@@ -36,7 +36,7 @@ function DiscussionsSettings({ courseId, intl }) {
dispatch(fetchApps(courseId));
}, [courseId]);
- const discussionsPath = `${pagesAndResourcesPath}/discussions`;
+ const discussionsPath = `${pagesAndResourcesPath}/discussion`;
const { params: { appId } } = useRouteMatch();
const startStep = appId ? SETTINGS_STEP : SELECTION_STEP;
diff --git a/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx b/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx
index 6b24777cc..9d50ec566 100644
--- a/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx
+++ b/src/pages-and-resources/discussions/DiscussionsSettings.test.jsx
@@ -40,8 +40,8 @@ function renderComponent() {
@@ -79,7 +79,7 @@ describe('DiscussionsSettings', () => {
});
test('sets selection step from routes', async () => {
- history.push(`/course/${courseId}/pages-and-resources/discussions`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion`);
// This is an important line that ensures the spinner has been removed - and thus our main
// content has been loaded - prior to proceeding with our expectations.
@@ -90,7 +90,7 @@ describe('DiscussionsSettings', () => {
});
test('sets settings step from routes', async () => {
- history.push(`/course/${courseId}/pages-and-resources/discussions/configure/piazza`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion/configure/piazza`);
// This is an important line that ensures the spinner has been removed - and thus our main
// content has been loaded - prior to proceeding with our expectations.
@@ -101,7 +101,7 @@ describe('DiscussionsSettings', () => {
});
test('successfully advances to settings step for lti', async () => {
- history.push(`/course/${courseId}/pages-and-resources/discussions`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion`);
// This is an important line that ensures the spinner has been removed - and thus our main
// content has been loaded - prior to proceeding with our expectations.
@@ -119,7 +119,7 @@ describe('DiscussionsSettings', () => {
test('successfully advances to settings step for legacy', async () => {
axiosMock.onGet(getAppsUrl(courseId)).reply(200, legacyApiResponse);
renderComponent();
- history.push(`/course/${courseId}/pages-and-resources/discussions`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion`);
// This is an important line that ensures the spinner has been removed - and thus our main
// content has been loaded - prior to proceeding with our expectations.
@@ -135,7 +135,7 @@ describe('DiscussionsSettings', () => {
});
test('successfully goes back to first step', async () => {
- history.push(`/course/${courseId}/pages-and-resources/discussions/configure/piazza`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion/configure/piazza`);
// This is an important line that ensures the spinner has been removed - and thus our main
// content has been loaded - prior to proceeding with our expectations.
@@ -150,7 +150,7 @@ describe('DiscussionsSettings', () => {
});
test('successfully closes the modal', async () => {
- history.push(`/course/${courseId}/pages-and-resources/discussions`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion`);
// This is an important line that ensures the spinner has been removed - and thus our main
// content has been loaded - prior to proceeding with our expectations.
@@ -167,7 +167,7 @@ describe('DiscussionsSettings', () => {
});
test('successfully submit the modal', async () => {
- history.push(`/course/${courseId}/pages-and-resources/discussions`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion`);
axiosMock.onPost(getAppsUrl(courseId)).reply(200, piazzaApiResponse);
@@ -205,7 +205,7 @@ describe('DiscussionsSettings', () => {
});
test('shows connection error alert', async () => {
- history.push(`/course/${courseId}/pages-and-resources/discussions`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion`);
// This is an important line that ensures the spinner has been removed - and thus our main
// content has been loaded - prior to proceeding with our expectations.
@@ -232,7 +232,7 @@ describe('DiscussionsSettings', () => {
});
test('shows connection error alert at top of form', async () => {
- history.push(`/course/${courseId}/pages-and-resources/discussions/configure/piazza`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion/configure/piazza`);
// This is an important line that ensures the spinner has been removed - and thus our main
// content has been loaded - prior to proceeding with our expectations.
@@ -262,7 +262,7 @@ describe('DiscussionsSettings', () => {
});
test('shows permission denied alert', async () => {
- history.push(`/course/${courseId}/pages-and-resources/discussions`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion`);
// This is an important line that ensures the spinner has been removed - and thus our main
// content has been loaded - prior to proceeding with our expectations.
@@ -283,7 +283,7 @@ describe('DiscussionsSettings', () => {
});
test('shows permission denied alert at top of form', async () => {
- history.push(`/course/${courseId}/pages-and-resources/discussions/configure/piazza`);
+ history.push(`/course/${courseId}/pages-and-resources/discussion/configure/piazza`);
// This is an important line that ensures the spinner has been removed - and thus our main
// content has been loaded - prior to proceeding with our expectations.
@@ -300,7 +300,7 @@ describe('DiscussionsSettings', () => {
expect(queryByTestId(container, 'appConfigForm')).not.toBeInTheDocument();
// We don't technically leave the route in this case, though the modal is hidden.
- expect(window.location.pathname).toEqual(`/course/${courseId}/pages-and-resources/discussions/configure/piazza`);
+ expect(window.location.pathname).toEqual(`/course/${courseId}/pages-and-resources/discussion/configure/piazza`);
const alert = queryByRole(container, 'alert');
expect(alert).toBeInTheDocument();
diff --git a/src/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider.jsx b/src/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider.jsx
index c49a9ca6d..76ae61a05 100644
--- a/src/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider.jsx
+++ b/src/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider.jsx
@@ -1,10 +1,17 @@
-import React from 'react';
+import classNames from 'classnames';
import PropTypes from 'prop-types';
+import React from 'react';
-export default function AppConfigFormDivider({ thick }) {
+export default function AppConfigFormDivider({ thick, marginAdj }) {
return (
{
history.push(`${pagesAndResourcesPath}/${page.id}`);
};
return (
-
-
-
- {page.title}
- {page.showSettings && }
-
+
+
+ {page.name}
+ {(page.allowedOperations.configure || page.allowedOperations.enable)
+ && (
+ history.push(`${pagesAndResourcesPath}/${page.id}/settings`)}
+ />
+ )}
+
-
- {page.showStatus && {intl.formatMessage(messages[pageStatusMsgId])}}
-
+
-
+
+ {page.description}
+
- {page.showEnable && !page.isEnabled && (
-
-
-
+ {(page.allowedOperations.enable && !page.enabled) && (
+
)}
-
-
+
+
);
}
diff --git a/src/pages-and-resources/pages/PageGrid.jsx b/src/pages-and-resources/pages/PageGrid.jsx
index 98f2e92e2..5ceb7ede5 100644
--- a/src/pages-and-resources/pages/PageGrid.jsx
+++ b/src/pages-and-resources/pages/PageGrid.jsx
@@ -1,38 +1,26 @@
-import React from 'react';
import PropTypes from 'prop-types';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import PageCard from './PageCard';
+import React from 'react';
+import { injectIntl } from '@edx/frontend-platform/i18n';
+import { CardGrid } from '@edx/paragon';
+import PageCard, { CoursePageShape } from './PageCard';
-import messages from '../messages';
-
-function PageGrid({ intl, pages }) {
+function PageGrid({ pages }) {
return (
-
-
- {intl.formatMessage(messages['pages.subheading'])}
-
-
- {pages.map((page) => (
-
- ))}
-
-
+
+ {pages.map((page) => (
+
+ ))}
+
);
}
-const pageShape = PropTypes.shape({
- id: PropTypes.string.isRequired,
- title: PropTypes.string.isRequired,
- isEnabled: PropTypes.bool.isRequired,
- showSettings: PropTypes.bool.isRequired,
- showStatus: PropTypes.bool.isRequired,
- showEnable: PropTypes.bool.isRequired,
- description: PropTypes.string.isRequired,
-});
-
PageGrid.propTypes = {
- intl: intlShape.isRequired,
- pages: PropTypes.arrayOf(pageShape).isRequired,
+ pages: PropTypes.arrayOf(CoursePageShape.isRequired).isRequired,
};
export default injectIntl(PageGrid);