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 3d113d267c
This commit is contained in:
@@ -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 }) => {
|
||||
</div>
|
||||
|
||||
<PageGrid pages={pages} />
|
||||
|
||||
{
|
||||
xpertPluginConfigurable?.enabled ? (
|
||||
<>
|
||||
<div className="d-flex justify-content-between my-4 my-md-5 align-items-center">
|
||||
<h3 className="m-0">{intl.formatMessage(messages.contentPermissions)}</h3>
|
||||
</div>
|
||||
<PageGrid pages={permissonPages} />
|
||||
</>
|
||||
) : ''
|
||||
}
|
||||
|
||||
<Switch>
|
||||
<PageRoute
|
||||
path={[
|
||||
@@ -71,6 +88,15 @@ const PagesAndResources = ({ courseId, intl }) => {
|
||||
>
|
||||
<DiscussionsSettings courseId={courseId} />
|
||||
</PageRoute>
|
||||
|
||||
<PageRoute
|
||||
path={[
|
||||
`${path}/xpert-unit-summary/settings`,
|
||||
]}
|
||||
>
|
||||
<XpertUnitSummarySettings courseId={courseId} />
|
||||
</PageRoute>
|
||||
|
||||
<PageRoute path={`${path}/:appId/settings`}>
|
||||
{
|
||||
({ match, history }) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<SettingsModal
|
||||
appId="xpert-unit-summary"
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
enableAppHelp={intl.formatMessage(messages.enableXpertUnitSummaryHelp)}
|
||||
enableAppLabel={intl.formatMessage(messages.enableXpertUnitSummaryLabel)}
|
||||
learnMoreText={intl.formatMessage(messages.enableXpertUnitSummaryLink)}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
XpertUnitSummarySettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(XpertUnitSummarySettings);
|
||||
@@ -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(
|
||||
<AppProvider store={store}>
|
||||
<PagesAndResourcesProvider courseId={courseId}>
|
||||
<Switch>
|
||||
<PageRoute
|
||||
path={[
|
||||
'/xpert-unit-summary/settings',
|
||||
]}
|
||||
>
|
||||
<XpertUnitSummarySettings courseId={courseId} />
|
||||
</PageRoute>
|
||||
|
||||
<PageRoute
|
||||
path={[
|
||||
'/',
|
||||
]}
|
||||
>
|
||||
<div />
|
||||
</PageRoute>
|
||||
</Switch>
|
||||
</PagesAndResourcesProvider>
|
||||
</AppProvider>,
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
13
src/pages-and-resources/xpert-unit-summary/appInfo.js
Normal file
13
src/pages-and-resources/xpert-unit-summary/appInfo.js
Normal file
@@ -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: '',
|
||||
},
|
||||
};
|
||||
33
src/pages-and-resources/xpert-unit-summary/data/api.js
Normal file
33
src/pages-and-resources/xpert-unit-summary/data/api.js
Normal file
@@ -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;
|
||||
}
|
||||
71
src/pages-and-resources/xpert-unit-summary/data/thunks.js
Normal file
71
src/pages-and-resources/xpert-unit-summary/data/thunks.js
Normal file
@@ -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 }));
|
||||
};
|
||||
}
|
||||
9
src/pages-and-resources/xpert-unit-summary/index.js
Normal file
9
src/pages-and-resources/xpert-unit-summary/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import XpertUnitSummarySettings from './XpertUnitSummarySettings';
|
||||
import appInfo from './appInfo';
|
||||
import { fetchXpertPluginConfigurable } from './data/thunks';
|
||||
|
||||
export {
|
||||
XpertUnitSummarySettings,
|
||||
appInfo,
|
||||
fetchXpertPluginConfigurable,
|
||||
};
|
||||
30
src/pages-and-resources/xpert-unit-summary/messages.js
Normal file
30
src/pages-and-resources/xpert-unit-summary/messages.js
Normal file
@@ -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;
|
||||
@@ -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 && (
|
||||
<TransitionReplace>
|
||||
{showForm ? (
|
||||
<React.Fragment key="app-enabled">
|
||||
{children(formikProps)}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment key="app-disabled" />
|
||||
)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
|
||||
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,
|
||||
}) => (
|
||||
<ModalDialog
|
||||
title={title}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
size="lg"
|
||||
variant={variant}
|
||||
hasCloseButton={isMobile}
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title data-testid="modal-title">
|
||||
{title}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
{children}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="p-4">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.cancel)}
|
||||
</ModalDialog.CloseButton>
|
||||
{footer}
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
|
||||
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 (
|
||||
<Formik
|
||||
initialValues={{
|
||||
enabled: !!xpertSettings?.enabled,
|
||||
...initialValues,
|
||||
}}
|
||||
validationSchema={
|
||||
Yup.object()
|
||||
.shape({
|
||||
enabled: Yup.boolean(),
|
||||
...validationSchema,
|
||||
})
|
||||
}
|
||||
onSubmit={handleFormSubmit}
|
||||
enableReinitialize={enableReinitialize}
|
||||
>
|
||||
{(formikProps) => (
|
||||
<Form onSubmit={handleFormikSubmit(formikProps)}>
|
||||
<SettingsModalBase
|
||||
title={title}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
variant={modalVariant}
|
||||
isMobile={isMobile}
|
||||
isFullscreenOnMobile
|
||||
intl={intl}
|
||||
footer={(
|
||||
<StatefulButton
|
||||
labels={{
|
||||
default: intl.formatMessage(messages.save),
|
||||
pending: intl.formatMessage(messages.saving),
|
||||
complete: intl.formatMessage(messages.saved),
|
||||
}}
|
||||
state={submitButtonState}
|
||||
onClick={handleFormikSubmit(formikProps)}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{saveError && (
|
||||
<Alert variant="danger" icon={Info} ref={alertRef}>
|
||||
<Alert.Heading>
|
||||
{formikProps.errors.enabled?.title || intl.formatMessage(messages.errorSavingTitle)}
|
||||
</Alert.Heading>
|
||||
{formikProps.errors.enabled?.message || intl.formatMessage(messages.errorSavingMessage)}
|
||||
</Alert>
|
||||
)}
|
||||
<FormSwitchGroup
|
||||
id={`enable-${appId}-toggle`}
|
||||
name="enabled"
|
||||
onChange={(event) => formikProps.handleChange(event)}
|
||||
onBlur={formikProps.handleBlur}
|
||||
checked={formikProps.values.enabled}
|
||||
label={(
|
||||
<div className="d-flex align-items-center">
|
||||
{enableAppLabel}
|
||||
{formikProps.values.enabled && (
|
||||
<Badge className="ml-2" variant="success" data-testid="enable-badge">
|
||||
{intl.formatMessage(messages.enabled)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
helpText={(
|
||||
<div>
|
||||
<p>{enableAppHelp}</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{(formikProps.values.enabled || configureBeforeEnable) && children
|
||||
&& <AppConfigFormDivider marginAdj={{ default: 0, sm: 0 }} />}
|
||||
<AppSettingsForm formikProps={formikProps} showForm={formikProps.values.enabled || configureBeforeEnable}>
|
||||
{children}
|
||||
</AppSettingsForm>
|
||||
</SettingsModalBase>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SettingsModalBase
|
||||
intl={intl}
|
||||
title={title}
|
||||
isOpen
|
||||
onClose={onClose}
|
||||
size="sm"
|
||||
variant={modalVariant}
|
||||
isMobile={isMobile}
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
{loadingStatus === RequestStatus.IN_PROGRESS && <Loading />}
|
||||
{loadingStatus === RequestStatus.FAILED && <ConnectionErrorAlert />}
|
||||
{loadingStatus === RequestStatus.DENIED && <PermissionDeniedAlert />}
|
||||
</SettingsModalBase>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user