From 1dba6208a5a2386137c204acd9e22664c21085d2 Mon Sep 17 00:00:00 2001 From: David Nuon Date: Tue, 1 Aug 2023 06:07:08 -0700 Subject: [PATCH] feat: configuration for xpert unit summaries (#540) Adds setting modal for Xpert unit summaries Includes hiding the config section for xpert summary - this is done based on a flag from https://github.com/edx/ai-aside/commit/3d113d267c3344c5175bef16ab51cbd423bf7620 --- src/pages-and-resources/PagesAndResources.jsx | 28 +- src/pages-and-resources/messages.js | 4 + .../XpertUnitSummarySettings.jsx | 41 +++ .../XpertUnitSummarySettings.test.jsx | 189 ++++++++++++ .../xpert-unit-summary/appInfo.js | 13 + .../xpert-unit-summary/data/api.js | 33 ++ .../xpert-unit-summary/data/thunks.js | 71 +++++ .../xpert-unit-summary/index.js | 9 + .../xpert-unit-summary/messages.js | 30 ++ .../settings-modal/SettingsModal.jsx | 281 ++++++++++++++++++ .../settings-modal/messages.js | 42 +++ 11 files changed, 740 insertions(+), 1 deletion(-) create mode 100644 src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx create mode 100644 src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx create mode 100644 src/pages-and-resources/xpert-unit-summary/appInfo.js create mode 100644 src/pages-and-resources/xpert-unit-summary/data/api.js create mode 100644 src/pages-and-resources/xpert-unit-summary/data/thunks.js create mode 100644 src/pages-and-resources/xpert-unit-summary/index.js create mode 100644 src/pages-and-resources/xpert-unit-summary/messages.js create mode 100644 src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.jsx create mode 100644 src/pages-and-resources/xpert-unit-summary/settings-modal/messages.js 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) => ( +
+ + )} + > + {saveError && ( + + + {formikProps.errors.enabled?.title || intl.formatMessage(messages.errorSavingTitle)} + + {formikProps.errors.enabled?.message || intl.formatMessage(messages.errorSavingMessage)} + + )} + formikProps.handleChange(event)} + onBlur={formikProps.handleBlur} + checked={formikProps.values.enabled} + label={( +
+ {enableAppLabel} + {formikProps.values.enabled && ( + + {intl.formatMessage(messages.enabled)} + + )} +
+ )} + helpText={( +
+

{enableAppHelp}

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