import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { ActionRow, Alert, Badge, Form, Icon, ModalDialog, OverlayTrigger, StatefulButton, Tooltip, TransitionReplace, Hyperlink, } from '@openedx/paragon'; import { Info, CheckCircleOutline, SpinnerSimple, } from '@openedx/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 'CourseAuthoring/data/constants'; import ConnectionErrorAlert from 'CourseAuthoring/generic/ConnectionErrorAlert'; import FormSwitchGroup from 'CourseAuthoring/generic/FormSwitchGroup'; import Loading from 'CourseAuthoring/generic/Loading'; import { useModel } from 'CourseAuthoring/generic/model-store'; import PermissionDeniedAlert from 'CourseAuthoring/generic/PermissionDeniedAlert'; import { useIsMobile } from 'CourseAuthoring/utils'; import { getLoadingStatus, getSavingStatus, getResetStatus } from 'CourseAuthoring/pages-and-resources/data/selectors'; import { updateSavingStatus, updateResetStatus } from 'CourseAuthoring/pages-and-resources/data/slice'; import AppConfigFormDivider from 'CourseAuthoring/pages-and-resources/discussions/app-config-form/apps/shared/AppConfigFormDivider'; import { PagesAndResourcesContext } from 'CourseAuthoring/pages-and-resources/PagesAndResourcesProvider'; import { updateXpertSettings, resetXpertSettings, removeXpertSettings } from '../data/thunks'; import messages from './messages'; import appInfo from '../appInfo'; import ResetIcon from './ResetIcon'; import './SettingsModal.scss'; 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 ResetUnitsButton = ({ intl, courseId, checked, visible, }) => { const resetStatusRequestStatus = useSelector(getResetStatus); const dispatch = useDispatch(); useEffect(() => { if (resetStatusRequestStatus === RequestStatus.SUCCESSFUL) { setTimeout(() => { dispatch(updateResetStatus({ status: '' })); }, 2000); } }, [resetStatusRequestStatus]); const handleResetUnits = () => { dispatch(resetXpertSettings(courseId, { enabled: checked === 'true', reset: true })); }; const getResetButtonState = () => { switch (resetStatusRequestStatus) { case RequestStatus.PENDING: return 'pending'; case RequestStatus.SUCCESSFUL: return 'finish'; default: return 'default'; } }; if (!visible) { return null; } const messageKey = checked === 'true' ? 'resetAllUnitsTooltipChecked' : 'resetAllUnitsTooltipUnchecked'; return ( {intl.formatMessage(messages[messageKey])} )} > , pending: , finish: , }} state={getResetButtonState()} onClick={handleResetUnits} disabledStates={['pending', 'finish']} variant="outline" data-testid="reset-units" /> ); }; ResetUnitsButton.propTypes = { intl: intlShape.isRequired, courseId: PropTypes.string.isRequired, checked: PropTypes.oneOf(['true', 'false']).isRequired, visible: PropTypes.bool, }; ResetUnitsButton.defaultProps = { visible: false, }; const SettingsModal = ({ intl, appId, title, children, configureBeforeEnable, initialValues, validationSchema, onClose, onSettingsSave, enableAppLabel, enableAppHelp, learnMoreText, helpPrivacyText, enableReinitialize, allUnitsEnabledText, noUnitsEnabledText, }) => { 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 ({ enabled, checked, ...rest }) => { let success; const values = { ...rest, enabled: enabled ? checked === 'true' : undefined }; if (enabled) { success = await dispatch(updateXpertSettings(courseId, values)); } else { success = await dispatch(removeXpertSettings(courseId)); } 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 } }; const learnMoreLink = appInfo.documentationLinks?.learnMoreConfiguration && (
{learnMoreText}
); const helpPrivacyLink = (
{helpPrivacyText}
); if (loadingStatus === RequestStatus.SUCCESSFUL) { return ( {(formikProps) => (
)} > {saveError && ( {formikProps.errors.enabled?.title || intl.formatMessage(messages.errorSavingTitle)} {formikProps.errors.enabled?.message || intl.formatMessage(messages.errorSavingMessage)} )} {enableAppLabel} {formikProps.values.enabled && ( {intl.formatMessage(messages.enabled)} )} )} helpText={(

{enableAppHelp}

{helpPrivacyLink} {learnMoreLink}
)} /> {(formikProps.values.enabled || configureBeforeEnable) && ( {allUnitsEnabledText} {noUnitsEnabledText} )} {(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, learnMoreText: PropTypes.string.isRequired, helpPrivacyText: PropTypes.string.isRequired, allUnitsEnabledText: PropTypes.string.isRequired, noUnitsEnabledText: 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);