diff --git a/src/pages-and-resources/PagesAndResources.jsx b/src/pages-and-resources/PagesAndResources.jsx
index b165ef633..cdd140ff9 100644
--- a/src/pages-and-resources/PagesAndResources.jsx
+++ b/src/pages-and-resources/PagesAndResources.jsx
@@ -9,21 +9,24 @@ import { useDispatch, useSelector } from 'react-redux';
import { Button, Hyperlink } from '@edx/paragon';
import messages from './messages';
import DiscussionsSettings from './discussions';
+import { XpertUnitSummarySettings, fetchXpertPluginConfigurable, appInfo } from './xpert-unit-summary';
import PageGrid from './pages/PageGrid';
import { fetchCourseApps } from './data/thunks';
-import { useModels } from '../generic/model-store';
+import { useModels, useModel } from '../generic/model-store';
import { getCourseAppsApiStatus, getLoadingStatus } from './data/selectors';
import PagesAndResourcesProvider from './PagesAndResourcesProvider';
import { RequestStatus } from '../data/constants';
import PermissionDeniedAlert from '../generic/PermissionDeniedAlert';
+const permissonPages = [appInfo];
const PagesAndResources = ({ courseId, intl }) => {
const { path, url } = useRouteMatch();
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchCourseApps(courseId));
+ dispatch(fetchXpertPluginConfigurable(courseId));
}, [courseId]);
const courseAppIds = useSelector(state => state.pagesAndResources.courseAppIds);
@@ -35,6 +38,8 @@ const PagesAndResources = ({ courseId, intl }) => {
// Each page here is driven by a course app
const pages = useModels('courseApps', courseAppIds);
+ const xpertPluginConfigurable = useModel('XpertSettings.enabled', 'xpert-unit-summary');
+
if (loadingStatus === RequestStatus.IN_PROGRESS) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>>;
@@ -62,6 +67,18 @@ const PagesAndResources = ({ courseId, intl }) => {
+
+ {
+ xpertPluginConfigurable?.enabled ? (
+ <>
+
+
{intl.formatMessage(messages.contentPermissions)}
+
+
+ >
+ ) : ''
+ }
+
{
>
+
+
+
+
+
{
({ match, history }) => {
diff --git a/src/pages-and-resources/messages.js b/src/pages-and-resources/messages.js
index 3fbe5f849..97148c29c 100644
--- a/src/pages-and-resources/messages.js
+++ b/src/pages-and-resources/messages.js
@@ -17,6 +17,10 @@ const messages = defineMessages({
id: 'course-authoring.badge.enabled',
defaultMessage: 'Enabled',
},
+ contentPermissions: {
+ id: 'course-authoring.pages-resources.content-permissions.heading',
+ defaultMessage: 'Content permissions',
+ },
});
export default messages;
diff --git a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx
new file mode 100644
index 000000000..eaf3c2ba2
--- /dev/null
+++ b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx
@@ -0,0 +1,41 @@
+import React, { useCallback, useContext, useEffect } from 'react';
+import { history } from '@edx/frontend-platform';
+import { useDispatch } from 'react-redux';
+
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { PagesAndResourcesContext } from '../PagesAndResourcesProvider';
+
+import SettingsModal from './settings-modal/SettingsModal';
+import messages from './messages';
+
+import { fetchXpertSettings } from './data/thunks';
+
+const XpertUnitSummarySettings = ({ intl }) => {
+ const { path: pagesAndResourcesPath, courseId } = useContext(PagesAndResourcesContext);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch(fetchXpertSettings(courseId));
+ }, [courseId]);
+
+ const handleClose = useCallback(() => {
+ history.push(pagesAndResourcesPath);
+ }, [pagesAndResourcesPath]);
+
+ return (
+
+ );
+};
+
+XpertUnitSummarySettings.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(XpertUnitSummarySettings);
diff --git a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx
new file mode 100644
index 000000000..c6f2e98b6
--- /dev/null
+++ b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx
@@ -0,0 +1,189 @@
+import ReactDOM from 'react-dom';
+import React from 'react';
+import { Switch } from 'react-router';
+import {
+ getConfig, history, initializeMockApp, setConfig,
+} from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { AppProvider, PageRoute } from '@edx/frontend-platform/react';
+import {
+ queryByTestId, render, waitFor, getByText, fireEvent,
+} from '@testing-library/react';
+import MockAdapter from 'axios-mock-adapter';
+import PagesAndResourcesProvider from '../PagesAndResourcesProvider';
+import { XpertUnitSummarySettings } from './index';
+import initializeStore from '../../store';
+import * as API from './data/api';
+import * as Thunks from './data/thunks';
+import { executeThunk } from '../../utils';
+
+const courseId = 'course-v1:edX+TestX+Test_Course';
+let axiosMock;
+let store;
+let container;
+
+// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
+ReactDOM.createPortal = jest.fn(node => node);
+
+function renderComponent() {
+ const wrapper = render(
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ container = wrapper.container;
+}
+
+function generateCourseLevelAPIRepsonse({
+ success, enabled,
+}) {
+ return {
+ response: {
+ success, enabled,
+ },
+ };
+}
+
+describe('XpertUnitSummarySettings', () => {
+ beforeEach(() => {
+ setConfig({
+ ...getConfig(),
+ BASE_URL: 'http://test.edx.org',
+ LMS_BASE_URL: 'http://lmstest.edx.org',
+ CMS_BASE_URL: 'http://cmstest.edx.org',
+ LOGIN_URL: 'http://support.edx.org/login',
+ LOGOUT_URL: 'http://support.edx.org/logout',
+ REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://support.edx.org/access_token',
+ ACCESS_TOKEN_COOKIE_NAME: 'cookie',
+ CSRF_TOKEN_API_PATH: '/',
+ SUPPORT_URL: 'http://support.edx.org',
+ });
+
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+
+ store = initializeStore({
+ models: {
+ courseDetails: {
+ [courseId]: {
+ start: Date(),
+ },
+ },
+ },
+ });
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+
+ // Go back to settings route
+ history.push('/xpert-unit-summary/settings');
+ });
+
+ describe('with successful network connections', () => {
+ beforeEach(() => {
+ axiosMock.onGet(API.getXpertSettingsUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: true,
+ }));
+
+ renderComponent();
+ });
+
+ test('Shows enabled if enabled from backend', async () => {
+ expect(container.querySelector('#enable-xpert-unit-summary-toggle').checked).toBeTruthy();
+ expect(queryByTestId(container, 'enable-badge')).toBeTruthy();
+ });
+
+ test('Does not show enabled if disabled from backend', async () => {
+ axiosMock.onGet(API.getXpertSettingsUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: false,
+ }));
+
+ renderComponent();
+ await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
+ expect(container.querySelector('#enable-xpert-unit-summary-toggle').checked).not.toBeTruthy();
+ expect(queryByTestId(container, 'enable-badge')).not.toBeTruthy();
+ });
+ });
+
+ describe('first time course configuration', () => {
+ beforeEach(() => {
+ axiosMock.onGet(API.getXpertSettingsUrl(courseId))
+ .reply(400, generateCourseLevelAPIRepsonse({
+ success: false,
+ enabled: false,
+ }));
+
+ renderComponent();
+ });
+
+ test('Does not show as enabled if configuation does not exist', async () => {
+ await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
+ expect(container.querySelector('#enable-xpert-unit-summary-toggle').checked).not.toBeTruthy();
+ expect(queryByTestId(container, 'enable-badge')).not.toBeTruthy();
+ });
+ });
+
+ describe('saving configuration changes', () => {
+ beforeEach(() => {
+ axiosMock.onPost(API.getXpertSettingsUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: true,
+ }));
+
+ renderComponent();
+ });
+
+ test('Saving configuration changes', async () => {
+ jest.spyOn(API, 'postXpertSettings');
+
+ await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
+ fireEvent.click(getByText(container, 'Save'));
+ await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).not.toBeTruthy());
+ expect(API.postXpertSettings).toBeCalled();
+ });
+ });
+
+ describe('testing configurable gating', () => {
+ beforeEach(async () => {
+ axiosMock.onGet(API.getXpertConfigurationStatusUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: true,
+ }));
+ jest.spyOn(API, 'getXpertPluginConfigurable');
+ await executeThunk(Thunks.fetchXpertPluginConfigurable(courseId), store.dispatch);
+ renderComponent();
+ });
+
+ test('getting Xpert Plugin configurable status', () => {
+ expect(API.getXpertPluginConfigurable).toBeCalled();
+ });
+ });
+});
diff --git a/src/pages-and-resources/xpert-unit-summary/appInfo.js b/src/pages-and-resources/xpert-unit-summary/appInfo.js
new file mode 100644
index 000000000..5b58f5fe3
--- /dev/null
+++ b/src/pages-and-resources/xpert-unit-summary/appInfo.js
@@ -0,0 +1,13 @@
+export default {
+ id: 'xpert-unit-summary',
+ enabled: false,
+ name: 'Xpert unit summaries',
+ description: 'Harness ChatGPT for quick, focused summaries of text and video content.',
+ allowedOperations: {
+ enable: true,
+ configure: true,
+ },
+ documentationLinks: {
+ learnMoreConfiguration: '',
+ },
+};
diff --git a/src/pages-and-resources/xpert-unit-summary/data/api.js b/src/pages-and-resources/xpert-unit-summary/data/api.js
new file mode 100644
index 000000000..b7b489861
--- /dev/null
+++ b/src/pages-and-resources/xpert-unit-summary/data/api.js
@@ -0,0 +1,33 @@
+import { getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+
+export function getXpertSettingsUrl(courseId) {
+ return `${getConfig().STUDIO_BASE_URL}/ai_aside/v1/${courseId}`;
+}
+
+export function getXpertConfigurationStatusUrl(courseId) {
+ return `${getConfig().STUDIO_BASE_URL}/ai_aside/v1/${courseId}/configurable`;
+}
+
+export async function getXpertSettings(courseId) {
+ const { data } = await getAuthenticatedHttpClient()
+ .get(getXpertSettingsUrl(courseId));
+
+ return data;
+}
+
+export async function postXpertSettings(courseId, state) {
+ const { data } = await getAuthenticatedHttpClient()
+ .post(getXpertSettingsUrl(courseId), {
+ enabled: state.enabled,
+ });
+
+ return data;
+}
+
+export async function getXpertPluginConfigurable(courseId) {
+ const { data } = await getAuthenticatedHttpClient()
+ .get(getXpertConfigurationStatusUrl(courseId));
+
+ return data;
+}
diff --git a/src/pages-and-resources/xpert-unit-summary/data/thunks.js b/src/pages-and-resources/xpert-unit-summary/data/thunks.js
new file mode 100644
index 000000000..5be88abeb
--- /dev/null
+++ b/src/pages-and-resources/xpert-unit-summary/data/thunks.js
@@ -0,0 +1,71 @@
+import { getXpertSettings, postXpertSettings, getXpertPluginConfigurable } from './api';
+
+import { updateSavingStatus, updateLoadingStatus } from '../../data/slice';
+import { RequestStatus } from '../../../data/constants';
+
+import { addModel, updateModel } from '../../../generic/model-store';
+
+export function updateXpertSettings(courseId, state) {
+ return async (dispatch) => {
+ dispatch(updateSavingStatus({ status: RequestStatus.IN_PROGRESS }));
+ try {
+ const { response } = await postXpertSettings(courseId, state);
+ const { success, enabled } = response;
+ if (success) {
+ dispatch(updateModel({ modelType: 'XpertSettings', model: { id: 'xpert-unit-summary', enabled } }));
+ dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
+ return true;
+ }
+ dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
+ return false;
+ } catch (error) {
+ dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
+ return false;
+ }
+ };
+}
+
+export function fetchXpertPluginConfigurable(courseId) {
+ return async (dispatch) => {
+ let enabled = false;
+ dispatch(updateLoadingStatus({ status: RequestStatus.PENDING }));
+ try {
+ const { response } = await getXpertPluginConfigurable(courseId);
+ enabled = response?.enabled;
+ } catch (e) {
+ enabled = false;
+ }
+
+ dispatch(addModel({
+ modelType: 'XpertSettings.enabled',
+ model: {
+ id: 'xpert-unit-summary',
+ enabled,
+ },
+ }));
+ };
+}
+
+export function fetchXpertSettings(courseId) {
+ return async (dispatch) => {
+ let enabled = false;
+ dispatch(updateLoadingStatus({ status: RequestStatus.PENDING }));
+
+ try {
+ const { response } = await getXpertSettings(courseId);
+ enabled = response?.enabled;
+ } catch (e) {
+ enabled = false;
+ }
+
+ dispatch(addModel({
+ modelType: 'XpertSettings',
+ model: {
+ id: 'xpert-unit-summary',
+ enabled,
+ },
+ }));
+
+ dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
+ };
+}
diff --git a/src/pages-and-resources/xpert-unit-summary/index.js b/src/pages-and-resources/xpert-unit-summary/index.js
new file mode 100644
index 000000000..274b7e118
--- /dev/null
+++ b/src/pages-and-resources/xpert-unit-summary/index.js
@@ -0,0 +1,9 @@
+import XpertUnitSummarySettings from './XpertUnitSummarySettings';
+import appInfo from './appInfo';
+import { fetchXpertPluginConfigurable } from './data/thunks';
+
+export {
+ XpertUnitSummarySettings,
+ appInfo,
+ fetchXpertPluginConfigurable,
+};
diff --git a/src/pages-and-resources/xpert-unit-summary/messages.js b/src/pages-and-resources/xpert-unit-summary/messages.js
new file mode 100644
index 000000000..db080c10e
--- /dev/null
+++ b/src/pages-and-resources/xpert-unit-summary/messages.js
@@ -0,0 +1,30 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ heading: {
+ id: 'course-authoring.pages-resources.xpert-unit-summary.heading',
+ defaultMessage: 'Configure Xpert unit summaries',
+ },
+ enableXpertUnitSummaryLabel: {
+ id: 'course-authoring.pages-resources.xpert-unit-summary.enable-xpert-unit-summary.label',
+ defaultMessage: 'Xpert unit summaries',
+ },
+ enableXpertUnitSummaryHelp: {
+ id: 'course-authoring.pages-resources.xpert-unit-summary.enable-xpert-unit-summary.help',
+ defaultMessage: 'Enable concise summaries of text and video content.',
+ },
+ enableXpertUnitSummaryLink: {
+ id: 'course-authoring.pages-resources.xpert-unit-summary.enable-xpert-unit-summary.link',
+ defaultMessage: 'Learn more about the Xpert unit summaries',
+ },
+ allUnitsEnabledByDefault: {
+ id: 'course-authoring.pages-resources.xpert-unit-summary.all-units-enabled-by-default',
+ defaultMessage: 'All units enabled by default',
+ },
+ noUnitsEnabledByDefault: {
+ id: 'course-authoring.pages-resources.xpert-unit-summary.no-units-enabled-by-default',
+ defaultMessage: 'No units enabled by default',
+ },
+});
+
+export default messages;
diff --git a/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.jsx b/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.jsx
new file mode 100644
index 000000000..95fd53b61
--- /dev/null
+++ b/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.jsx
@@ -0,0 +1,281 @@
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import {
+ ActionRow,
+ Alert,
+ Badge,
+ Form,
+ ModalDialog,
+ StatefulButton,
+ TransitionReplace,
+} from '@edx/paragon';
+import { Info } from '@edx/paragon/icons';
+
+import { Formik } from 'formik';
+import PropTypes from 'prop-types';
+import React, {
+ useContext, useEffect, useRef, useState,
+} from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import * as Yup from 'yup';
+
+import { RequestStatus } from '../../../data/constants';
+import ConnectionErrorAlert from '../../../generic/ConnectionErrorAlert';
+import FormSwitchGroup from '../../../generic/FormSwitchGroup';
+import Loading from '../../../generic/Loading';
+import { useModel } from '../../../generic/model-store';
+import PermissionDeniedAlert from '../../../generic/PermissionDeniedAlert';
+import { useIsMobile } from '../../../utils';
+import { getLoadingStatus, getSavingStatus } from '../../data/selectors';
+import { updateSavingStatus } from '../../data/slice';
+import { updateXpertSettings } from '../data/thunks';
+import AppConfigFormDivider from '../../discussions/app-config-form/apps/shared/AppConfigFormDivider';
+import { PagesAndResourcesContext } from '../../PagesAndResourcesProvider';
+import messages from './messages';
+
+const AppSettingsForm = ({
+ formikProps, children, showForm,
+}) => children && (
+
+ {showForm ? (
+
+ {children(formikProps)}
+
+ ) : (
+
+ )}
+
+);
+
+AppSettingsForm.propTypes = {
+ // Ignore the warning here since we're just passing along the props as-is and the child component should validate
+ // eslint-disable-next-line react/forbid-prop-types
+ formikProps: PropTypes.object.isRequired,
+ showForm: PropTypes.bool.isRequired,
+ children: PropTypes.func,
+};
+
+AppSettingsForm.defaultProps = {
+ children: null,
+};
+
+const SettingsModalBase = ({
+ intl, title, onClose, variant, isMobile, children, footer,
+}) => (
+
+
+
+ {title}
+
+
+
+ {children}
+
+
+
+
+ {intl.formatMessage(messages.cancel)}
+
+ {footer}
+
+
+
+);
+
+SettingsModalBase.propTypes = {
+ intl: intlShape.isRequired,
+ title: PropTypes.string.isRequired,
+ onClose: PropTypes.func.isRequired,
+ variant: PropTypes.oneOf(['default', 'dark']).isRequired,
+ isMobile: PropTypes.bool.isRequired,
+ children: PropTypes.node.isRequired,
+ footer: PropTypes.node,
+};
+
+SettingsModalBase.defaultProps = {
+ footer: null,
+};
+
+const SettingsModal = ({
+ intl,
+ appId,
+ title,
+ children,
+ configureBeforeEnable,
+ initialValues,
+ validationSchema,
+ onClose,
+ onSettingsSave,
+ enableAppLabel,
+ enableAppHelp,
+ enableReinitialize,
+}) => {
+ const { courseId } = useContext(PagesAndResourcesContext);
+ const loadingStatus = useSelector(getLoadingStatus);
+ const updateSettingsRequestStatus = useSelector(getSavingStatus);
+ const alertRef = useRef(null);
+ const [saveError, setSaveError] = useState(false);
+ const dispatch = useDispatch();
+ const submitButtonState = updateSettingsRequestStatus === RequestStatus.IN_PROGRESS ? 'pending' : 'default';
+ const isMobile = useIsMobile();
+ const modalVariant = isMobile ? 'dark' : 'default';
+
+ const xpertSettings = useModel('XpertSettings', appId);
+
+ useEffect(() => {
+ if (updateSettingsRequestStatus === RequestStatus.SUCCESSFUL) {
+ dispatch(updateSavingStatus({ status: '' }));
+ onClose();
+ }
+ }, [updateSettingsRequestStatus]);
+
+ const handleFormSubmit = async (values) => {
+ let success = true;
+ success = await dispatch(updateXpertSettings(courseId, values));
+
+ if (onSettingsSave) {
+ success = success && await onSettingsSave(values);
+ }
+ setSaveError(!success);
+ !success && alertRef?.current.scrollIntoView(); // eslint-disable-line no-unused-expressions
+ };
+
+ const handleFormikSubmit = ({ handleSubmit, errors }) => async (event) => {
+ // If submitting the form with errors, show the alert and scroll to it.
+ await handleSubmit(event);
+ if (Object.keys(errors).length > 0) {
+ setSaveError(true);
+ alertRef?.current.scrollIntoView?.(); // eslint-disable-line no-unused-expressions
+ }
+ };
+
+ if (loadingStatus === RequestStatus.SUCCESSFUL) {
+ return (
+
+ {(formikProps) => (
+
+ )}
+
+ );
+ }
+ return (
+
+ {loadingStatus === RequestStatus.IN_PROGRESS && }
+ {loadingStatus === RequestStatus.FAILED && }
+ {loadingStatus === RequestStatus.DENIED && }
+
+ );
+};
+
+SettingsModal.propTypes = {
+ intl: intlShape.isRequired,
+ title: PropTypes.string.isRequired,
+ appId: PropTypes.string.isRequired,
+ children: PropTypes.func,
+ onSettingsSave: PropTypes.func,
+ initialValues: PropTypes.shape({}),
+ validationSchema: PropTypes.shape({}),
+ onClose: PropTypes.func.isRequired,
+ enableAppLabel: PropTypes.string.isRequired,
+ enableAppHelp: PropTypes.string.isRequired,
+ configureBeforeEnable: PropTypes.bool,
+ enableReinitialize: PropTypes.bool,
+};
+
+SettingsModal.defaultProps = {
+ children: null,
+ onSettingsSave: null,
+ initialValues: {},
+ validationSchema: {},
+ configureBeforeEnable: false,
+ enableReinitialize: false,
+};
+
+export default injectIntl(SettingsModal);
diff --git a/src/pages-and-resources/xpert-unit-summary/settings-modal/messages.js b/src/pages-and-resources/xpert-unit-summary/settings-modal/messages.js
new file mode 100644
index 000000000..b5586e993
--- /dev/null
+++ b/src/pages-and-resources/xpert-unit-summary/settings-modal/messages.js
@@ -0,0 +1,42 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ cancel: {
+ id: 'course-authoring.pages-resources.app-settings-modal.button.cancel',
+ defaultMessage: 'Cancel',
+ },
+ save: {
+ id: 'course-authoring.pages-resources.app-settings-modal.button.save',
+ defaultMessage: 'Save',
+ },
+ saving: {
+ id: 'course-authoring.pages-resources.app-settings-modal.button.saving',
+ defaultMessage: 'Saving',
+ },
+ saved: {
+ id: 'course-authoring.pages-resources.app-settings-modal.button.saved',
+ defaultMessage: 'Saved',
+ },
+ 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',
+ },
+ errorSavingTitle: {
+ id: 'course-authoring.pages-resources.app-settings-modal.save-error.title',
+ defaultMessage: 'We couldn\'t apply your changes.',
+ },
+ errorSavingMessage: {
+ id: 'course-authoring.pages-resources.app-settings-modal.save-error.message',
+ defaultMessage: 'Please check your entries and try again.',
+ },
+});
+
+export default messages;