From 04d2312282fc9c53650e904b82e2ee6bdc6fa821 Mon Sep 17 00:00:00 2001 From: David Nuon Date: Tue, 25 Jul 2023 09:47:07 -0700 Subject: [PATCH 01/11] feat: Add setting for Xpert unit summaries Adds setting modal for Xpert unit summaries --- src/pages-and-resources/PagesAndResources.jsx | 17 ++ src/pages-and-resources/messages.js | 4 + .../XpertUnitSummarySettings.jsx | 41 +++ .../xpert-unit-summary/appInfo.js | 13 + .../xpert-unit-summary/data/api.js | 22 ++ .../xpert-unit-summary/data/thunks.js | 51 ++++ .../xpert-unit-summary/index.js | 7 + .../xpert-unit-summary/messages.js | 31 ++ .../settings-modal/SettingsModal.jsx | 281 ++++++++++++++++++ .../settings-modal/messages.js | 42 +++ 10 files changed, 509 insertions(+) create mode 100644 src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.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 9a19f25c06..bfbc75d75a 100644 --- a/src/pages-and-resources/PagesAndResources.jsx +++ b/src/pages-and-resources/PagesAndResources.jsx @@ -9,6 +9,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { Button, Hyperlink } from '@edx/paragon'; import messages from './messages'; import DiscussionsSettings from './discussions'; +import { XpertUnitSummarySettings, appInfo } from './xpert-unit-summary'; import PageGrid from './pages/PageGrid'; import { fetchCourseApps } from './data/thunks'; @@ -17,6 +18,7 @@ import { getLoadingStatus } from './data/selectors'; import PagesAndResourcesProvider from './PagesAndResourcesProvider'; import { RequestStatus } from '../data/constants'; +const permissonPages = [appInfo]; const PagesAndResources = ({ courseId, intl }) => { const { path, url } = useRouteMatch(); @@ -54,6 +56,12 @@ const PagesAndResources = ({ courseId, intl }) => { + +
+

{intl.formatMessage(messages.contentPermissions)}

+
+ + { > + + + + + { ({ match, history }) => { diff --git a/src/pages-and-resources/messages.js b/src/pages-and-resources/messages.js index 3fbe5f849f..97148c29c1 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 0000000000..eaf3c2ba20 --- /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/appInfo.js b/src/pages-and-resources/xpert-unit-summary/appInfo.js new file mode 100644 index 0000000000..3452aa9303 --- /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 0000000000..e6d581e4b4 --- /dev/null +++ b/src/pages-and-resources/xpert-unit-summary/data/api.js @@ -0,0 +1,22 @@ +import { getConfig } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; + +function getXpertSettingsUrl(courseId) { + return `${getConfig().LMS_BASE_URL}/ai_aside/v1/${courseId}`; +} + +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; +} 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 0000000000..e52dbf4bd3 --- /dev/null +++ b/src/pages-and-resources/xpert-unit-summary/data/thunks.js @@ -0,0 +1,51 @@ +import { getXpertSettings, postXpertSettings } 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 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 0000000000..144bdaed81 --- /dev/null +++ b/src/pages-and-resources/xpert-unit-summary/index.js @@ -0,0 +1,7 @@ +import XpertUnitSummarySettings from './XpertUnitSummarySettings'; +import appInfo from './appInfo'; + +export { + XpertUnitSummarySettings, + appInfo, +}; 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 0000000000..a0cb1eac19 --- /dev/null +++ b/src/pages-and-resources/xpert-unit-summary/messages.js @@ -0,0 +1,31 @@ +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. Control the availability of summaries in the settings dialog for each unit. + `, + }, + 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 0000000000..1fe08eceba --- /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); + } + await 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) { + await 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 0000000000..b5586e993d --- /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; From 1a34e5299cc9f996abf15526c664767044524d02 Mon Sep 17 00:00:00 2001 From: David Nuon Date: Tue, 25 Jul 2023 10:18:32 -0700 Subject: [PATCH 02/11] style: Fix linting to 2-spaces --- .../xpert-unit-summary/appInfo.js | 22 +++--- .../xpert-unit-summary/data/api.js | 18 ++--- .../xpert-unit-summary/data/thunks.js | 78 +++++++++---------- .../xpert-unit-summary/index.js | 4 +- 4 files changed, 61 insertions(+), 61 deletions(-) diff --git a/src/pages-and-resources/xpert-unit-summary/appInfo.js b/src/pages-and-resources/xpert-unit-summary/appInfo.js index 3452aa9303..5b58f5fe33 100644 --- a/src/pages-and-resources/xpert-unit-summary/appInfo.js +++ b/src/pages-and-resources/xpert-unit-summary/appInfo.js @@ -1,13 +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: '', - }, + 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 index e6d581e4b4..9d02ab9c73 100644 --- a/src/pages-and-resources/xpert-unit-summary/data/api.js +++ b/src/pages-and-resources/xpert-unit-summary/data/api.js @@ -2,21 +2,21 @@ import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; function getXpertSettingsUrl(courseId) { - return `${getConfig().LMS_BASE_URL}/ai_aside/v1/${courseId}`; + return `${getConfig().LMS_BASE_URL}/ai_aside/v1/${courseId}`; } export async function getXpertSettings(courseId) { - const { data } = await getAuthenticatedHttpClient() - .get(getXpertSettingsUrl(courseId)); + const { data } = await getAuthenticatedHttpClient() + .get(getXpertSettingsUrl(courseId)); - return data; + return data; } export async function postXpertSettings(courseId, state) { - const { data } = await getAuthenticatedHttpClient() - .post(getXpertSettingsUrl(courseId), { - enabled: state.enabled, - }); + const { data } = await getAuthenticatedHttpClient() + .post(getXpertSettingsUrl(courseId), { + enabled: state.enabled, + }); - return data; + 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 index e52dbf4bd3..4427113685 100644 --- a/src/pages-and-resources/xpert-unit-summary/data/thunks.js +++ b/src/pages-and-resources/xpert-unit-summary/data/thunks.js @@ -6,46 +6,46 @@ 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; - } - }; + 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 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 })); - }; + 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 index 144bdaed81..bf4b46eaee 100644 --- a/src/pages-and-resources/xpert-unit-summary/index.js +++ b/src/pages-and-resources/xpert-unit-summary/index.js @@ -2,6 +2,6 @@ import XpertUnitSummarySettings from './XpertUnitSummarySettings'; import appInfo from './appInfo'; export { - XpertUnitSummarySettings, - appInfo, + XpertUnitSummarySettings, + appInfo, }; From ed52c9095d41e22fcc7e42bcb370c46bb85efd36 Mon Sep 17 00:00:00 2001 From: David Nuon Date: Tue, 25 Jul 2023 10:51:25 -0700 Subject: [PATCH 03/11] feat: Change copy for Xpert config dialog --- src/pages-and-resources/xpert-unit-summary/messages.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages-and-resources/xpert-unit-summary/messages.js b/src/pages-and-resources/xpert-unit-summary/messages.js index a0cb1eac19..db080c10e4 100644 --- a/src/pages-and-resources/xpert-unit-summary/messages.js +++ b/src/pages-and-resources/xpert-unit-summary/messages.js @@ -11,8 +11,7 @@ const messages = defineMessages({ }, enableXpertUnitSummaryHelp: { id: 'course-authoring.pages-resources.xpert-unit-summary.enable-xpert-unit-summary.help', - defaultMessage: `Enable concise summaries of text and video content. Control the availability of summaries in the settings dialog for each unit. - `, + defaultMessage: 'Enable concise summaries of text and video content.', }, enableXpertUnitSummaryLink: { id: 'course-authoring.pages-resources.xpert-unit-summary.enable-xpert-unit-summary.link', From b80181f581ee2844e3f88a187ae709d1bb63097d Mon Sep 17 00:00:00 2001 From: David Nuon Date: Mon, 31 Jul 2023 07:31:08 -0700 Subject: [PATCH 04/11] test: Add tests for XpertUnitSummarySettings --- .../XpertUnitSummarySettings.test.jsx | 136 ++++++++++++++++++ .../xpert-unit-summary/data/api.js | 2 +- 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx 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 0000000000..1c3d66e480 --- /dev/null +++ b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx @@ -0,0 +1,136 @@ +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, +} from '@testing-library/react'; +import MockAdapter from 'axios-mock-adapter'; +import PagesAndResourcesProvider from '../PagesAndResourcesProvider'; +import { XpertUnitSummarySettings } from './index'; +import initializeStore from '../../store'; +import { getXpertSettingsUrl } from './data/api'; + +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()); + + // Leave the DiscussionsSettings route after the test. + history.push('/xpert-unit-summary/settings'); + }); + + describe('with successful network connections', () => { + beforeEach(() => { + axiosMock.onGet(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(); + }); + }); + + describe('first time course configuration', () => { + beforeEach(() => { + axiosMock.onGet(getXpertSettingsUrl(courseId)) + .reply(400, generateCourseLevelAPIRepsonse({ + success: false, + enabled: false, + })); + + renderComponent(); + }); + + test('Does not show as enabled if first time', 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(); + }); + }); +}); diff --git a/src/pages-and-resources/xpert-unit-summary/data/api.js b/src/pages-and-resources/xpert-unit-summary/data/api.js index 9d02ab9c73..71620848e5 100644 --- a/src/pages-and-resources/xpert-unit-summary/data/api.js +++ b/src/pages-and-resources/xpert-unit-summary/data/api.js @@ -1,7 +1,7 @@ import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -function getXpertSettingsUrl(courseId) { +export function getXpertSettingsUrl(courseId) { return `${getConfig().LMS_BASE_URL}/ai_aside/v1/${courseId}`; } From 387c3aca5f0e902871f763d5abb8eeca11e2420c Mon Sep 17 00:00:00 2001 From: David Nuon Date: Mon, 31 Jul 2023 09:28:58 -0700 Subject: [PATCH 05/11] feat: Add 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 | 20 ++++++++++++---- .../xpert-unit-summary/data/api.js | 11 +++++++++ .../xpert-unit-summary/data/thunks.js | 24 ++++++++++++++++++- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/pages-and-resources/PagesAndResources.jsx b/src/pages-and-resources/PagesAndResources.jsx index bfbc75d75a..6da89667be 100644 --- a/src/pages-and-resources/PagesAndResources.jsx +++ b/src/pages-and-resources/PagesAndResources.jsx @@ -13,10 +13,11 @@ import { XpertUnitSummarySettings, 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 { getLoadingStatus } from './data/selectors'; import PagesAndResourcesProvider from './PagesAndResourcesProvider'; import { RequestStatus } from '../data/constants'; +import { fetchXpertPluginConfigurable } from './xpert-unit-summary/data/thunks'; const permissonPages = [appInfo]; const PagesAndResources = ({ courseId, intl }) => { @@ -25,6 +26,7 @@ const PagesAndResources = ({ courseId, intl }) => { const dispatch = useDispatch(); useEffect(() => { dispatch(fetchCourseApps(courseId)); + dispatch(fetchXpertPluginConfigurable(courseId)); }, [courseId]); const courseAppIds = useSelector(state => state.pagesAndResources.courseAppIds); @@ -35,6 +37,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 <>; @@ -57,10 +61,16 @@ const PagesAndResources = ({ courseId, intl }) => { -
-

{intl.formatMessage(messages.contentPermissions)}

-
- + { + xpertPluginConfigurable?.enabled ? ( + <> +
+

{intl.formatMessage(messages.contentPermissions)}

+
+ + + ) : '' + } { + 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; From a035aa2d84154024fc55465d890d2228b35b5b7c Mon Sep 17 00:00:00 2001 From: David Nuon Date: Mon, 31 Jul 2023 09:52:30 -0700 Subject: [PATCH 06/11] chore: Update PagesAndResources imports --- src/pages-and-resources/PagesAndResources.jsx | 3 +-- src/pages-and-resources/xpert-unit-summary/index.js | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages-and-resources/PagesAndResources.jsx b/src/pages-and-resources/PagesAndResources.jsx index 6da89667be..a854d01b12 100644 --- a/src/pages-and-resources/PagesAndResources.jsx +++ b/src/pages-and-resources/PagesAndResources.jsx @@ -9,7 +9,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { Button, Hyperlink } from '@edx/paragon'; import messages from './messages'; import DiscussionsSettings from './discussions'; -import { XpertUnitSummarySettings, appInfo } from './xpert-unit-summary'; +import { XpertUnitSummarySettings, fetchXpertPluginConfigurable, appInfo } from './xpert-unit-summary'; import PageGrid from './pages/PageGrid'; import { fetchCourseApps } from './data/thunks'; @@ -17,7 +17,6 @@ import { useModels, useModel } from '../generic/model-store'; import { getLoadingStatus } from './data/selectors'; import PagesAndResourcesProvider from './PagesAndResourcesProvider'; import { RequestStatus } from '../data/constants'; -import { fetchXpertPluginConfigurable } from './xpert-unit-summary/data/thunks'; const permissonPages = [appInfo]; const PagesAndResources = ({ courseId, intl }) => { diff --git a/src/pages-and-resources/xpert-unit-summary/index.js b/src/pages-and-resources/xpert-unit-summary/index.js index bf4b46eaee..274b7e1182 100644 --- a/src/pages-and-resources/xpert-unit-summary/index.js +++ b/src/pages-and-resources/xpert-unit-summary/index.js @@ -1,7 +1,9 @@ import XpertUnitSummarySettings from './XpertUnitSummarySettings'; import appInfo from './appInfo'; +import { fetchXpertPluginConfigurable } from './data/thunks'; export { XpertUnitSummarySettings, appInfo, + fetchXpertPluginConfigurable, }; From ed4ba408bf551df3277940f8ba8a68203de9e94a Mon Sep 17 00:00:00 2001 From: David Nuon Date: Mon, 31 Jul 2023 10:03:23 -0700 Subject: [PATCH 07/11] style: Fix lint --- src/pages-and-resources/xpert-unit-summary/data/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages-and-resources/xpert-unit-summary/data/api.js b/src/pages-and-resources/xpert-unit-summary/data/api.js index 76c40e7acb..82c215da71 100644 --- a/src/pages-and-resources/xpert-unit-summary/data/api.js +++ b/src/pages-and-resources/xpert-unit-summary/data/api.js @@ -27,7 +27,7 @@ export async function postXpertSettings(courseId, state) { export async function getXpertPluginConfigurable(courseId) { const { data } = await getAuthenticatedHttpClient() - .get(getXpertConfigurationStatusUrl(courseId)); + .get(getXpertConfigurationStatusUrl(courseId)); return data; } From 8ce5f0d67d411ba6079e01f2a1673a4e250b90ac Mon Sep 17 00:00:00 2001 From: David Nuon Date: Mon, 31 Jul 2023 10:12:19 -0700 Subject: [PATCH 08/11] fix: Change to use STUDIO_BASE_URL instead of LMS_BASE_URL --- src/pages-and-resources/xpert-unit-summary/data/api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages-and-resources/xpert-unit-summary/data/api.js b/src/pages-and-resources/xpert-unit-summary/data/api.js index 82c215da71..b7b489861a 100644 --- a/src/pages-and-resources/xpert-unit-summary/data/api.js +++ b/src/pages-and-resources/xpert-unit-summary/data/api.js @@ -2,11 +2,11 @@ import { getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; export function getXpertSettingsUrl(courseId) { - return `${getConfig().LMS_BASE_URL}/ai_aside/v1/${courseId}`; + return `${getConfig().STUDIO_BASE_URL}/ai_aside/v1/${courseId}`; } export function getXpertConfigurationStatusUrl(courseId) { - return `${getConfig().LMS_BASE_URL}/ai_aside/v1/${courseId}/configurable`; + return `${getConfig().STUDIO_BASE_URL}/ai_aside/v1/${courseId}/configurable`; } export async function getXpertSettings(courseId) { From 0badc4ed218da6c87d15f695cab7b35eba9614f1 Mon Sep 17 00:00:00 2001 From: David Nuon Date: Mon, 31 Jul 2023 11:15:04 -0700 Subject: [PATCH 09/11] test: Add tests for saving xpertconfigsettings --- .../XpertUnitSummarySettings.test.jsx | 44 ++++++++++++++++--- .../xpert-unit-summary/data/thunks.js | 1 - 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx index 1c3d66e480..d942a42f60 100644 --- a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx +++ b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx @@ -7,13 +7,13 @@ import { import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider, PageRoute } from '@edx/frontend-platform/react'; import { - queryByTestId, render, waitFor, + 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 { getXpertSettingsUrl } from './data/api'; +import * as API from './data/api'; const courseId = 'course-v1:edX+TestX+Test_Course'; let axiosMock; @@ -101,7 +101,7 @@ describe('XpertUnitSummarySettings', () => { describe('with successful network connections', () => { beforeEach(() => { - axiosMock.onGet(getXpertSettingsUrl(courseId)) + axiosMock.onGet(API.getXpertSettingsUrl(courseId)) .reply(200, generateCourseLevelAPIRepsonse({ success: true, enabled: true, @@ -114,11 +114,24 @@ describe('XpertUnitSummarySettings', () => { 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(getXpertSettingsUrl(courseId)) + axiosMock.onGet(API.getXpertSettingsUrl(courseId)) .reply(400, generateCourseLevelAPIRepsonse({ success: false, enabled: false, @@ -127,10 +140,31 @@ describe('XpertUnitSummarySettings', () => { renderComponent(); }); - test('Does not show as enabled if first time', async () => { + 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(); + }); +}); diff --git a/src/pages-and-resources/xpert-unit-summary/data/thunks.js b/src/pages-and-resources/xpert-unit-summary/data/thunks.js index 50d6a9477a..e257bbb6d4 100644 --- a/src/pages-and-resources/xpert-unit-summary/data/thunks.js +++ b/src/pages-and-resources/xpert-unit-summary/data/thunks.js @@ -16,7 +16,6 @@ export function updateXpertSettings(courseId, state) { dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL })); return true; } - dispatch(updateSavingStatus({ status: RequestStatus.FAILED })); return false; } catch (error) { From a43ed0b65ad01d014cd71339409fd55fc86791f8 Mon Sep 17 00:00:00 2001 From: David Nuon Date: Mon, 31 Jul 2023 12:36:36 -0700 Subject: [PATCH 10/11] test: Update tests --- .../XpertUnitSummarySettings.test.jsx | 51 +++++++++++++------ .../xpert-unit-summary/data/thunks.js | 1 - 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx index d942a42f60..c6f2e98b6b 100644 --- a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx +++ b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx @@ -14,6 +14,8 @@ 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; @@ -95,7 +97,7 @@ describe('XpertUnitSummarySettings', () => { }); axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - // Leave the DiscussionsSettings route after the test. + // Go back to settings route history.push('/xpert-unit-summary/settings'); }); @@ -146,25 +148,42 @@ describe('XpertUnitSummarySettings', () => { expect(queryByTestId(container, 'enable-badge')).not.toBeTruthy(); }); }); -}); -describe('saving configuration changes', () => { - beforeEach(() => { - axiosMock.onPost(API.getXpertSettingsUrl(courseId)) - .reply(200, generateCourseLevelAPIRepsonse({ - success: true, - enabled: true, - })); + describe('saving configuration changes', () => { + beforeEach(() => { + axiosMock.onPost(API.getXpertSettingsUrl(courseId)) + .reply(200, generateCourseLevelAPIRepsonse({ + success: true, + enabled: true, + })); + + renderComponent(); + }); - 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(); + }); }); - test('Saving configuration changes', async () => { - jest.spyOn(API, 'postXpertSettings'); + 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(); + }); - 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(); + test('getting Xpert Plugin configurable status', () => { + expect(API.getXpertPluginConfigurable).toBeCalled(); + }); }); }); diff --git a/src/pages-and-resources/xpert-unit-summary/data/thunks.js b/src/pages-and-resources/xpert-unit-summary/data/thunks.js index e257bbb6d4..5be88abeb4 100644 --- a/src/pages-and-resources/xpert-unit-summary/data/thunks.js +++ b/src/pages-and-resources/xpert-unit-summary/data/thunks.js @@ -31,7 +31,6 @@ export function fetchXpertPluginConfigurable(courseId) { dispatch(updateLoadingStatus({ status: RequestStatus.PENDING })); try { const { response } = await getXpertPluginConfigurable(courseId); - enabled = response?.enabled; } catch (e) { enabled = false; From d478ead1b30db1ce9842cd79b6ac220011ac1ea8 Mon Sep 17 00:00:00 2001 From: David Nuon Date: Mon, 31 Jul 2023 13:03:32 -0700 Subject: [PATCH 11/11] fix: remove extraneous `await` in for setSaveError --- .../xpert-unit-summary/settings-modal/SettingsModal.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 1fe08eceba..95fd53b611 100644 --- a/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.jsx +++ b/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.jsx @@ -143,7 +143,7 @@ const SettingsModal = ({ if (onSettingsSave) { success = success && await onSettingsSave(values); } - await setSaveError(!success); + setSaveError(!success); !success && alertRef?.current.scrollIntoView(); // eslint-disable-line no-unused-expressions }; @@ -151,7 +151,7 @@ const SettingsModal = ({ // If submitting the form with errors, show the alert and scroll to it. await handleSubmit(event); if (Object.keys(errors).length > 0) { - await setSaveError(true); + setSaveError(true); alertRef?.current.scrollIntoView?.(); // eslint-disable-line no-unused-expressions } };