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) => ( +
+

{title}

+ + {enableAppLabel}  + + + )} + helpText={(

{enableAppHelp}
{learnMoreLink}

)} + /> + + {children} + + {formikProps.values.enabled && children + && } +
+ + +
+ + )} +
+ ) + } + {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.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);