diff --git a/src/index.scss b/src/index.scss
index 62da213ee..83c5dac16 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -13,3 +13,4 @@
@import "grading-settings/scss/GradingSettings";
@import "generic/styles";
@import "schedule-and-details/ScheduleAndDetails";
+@import "pages-and-resources/PagesAndResources";
diff --git a/src/pages-and-resources/PagesAndResources.scss b/src/pages-and-resources/PagesAndResources.scss
new file mode 100644
index 000000000..b577fe0f4
--- /dev/null
+++ b/src/pages-and-resources/PagesAndResources.scss
@@ -0,0 +1 @@
+@import "./xpert-unit-summary/settings-modal/SettingsModal";
diff --git a/src/pages-and-resources/data/selectors.js b/src/pages-and-resources/data/selectors.js
index 9e97e10af..c3d93a080 100644
--- a/src/pages-and-resources/data/selectors.js
+++ b/src/pages-and-resources/data/selectors.js
@@ -6,3 +6,4 @@ export const getCourseAppsApiStatus = (state) => state.pagesAndResources.courseA
export const getCourseAppSettingValue = (setting) => (state) => (
state.pagesAndResources.courseAppSettings[setting]?.value
);
+export const getResetStatus = (state) => state.pagesAndResources.resetStatus;
diff --git a/src/pages-and-resources/data/slice.js b/src/pages-and-resources/data/slice.js
index 771e9cf6c..95379cd34 100644
--- a/src/pages-and-resources/data/slice.js
+++ b/src/pages-and-resources/data/slice.js
@@ -9,6 +9,7 @@ const slice = createSlice({
courseAppIds: [],
loadingStatus: RequestStatus.IN_PROGRESS,
savingStatus: '',
+ resetStatus: '',
courseAppsApiStatus: {},
courseAppSettings: {},
},
@@ -22,6 +23,9 @@ const slice = createSlice({
updateSavingStatus: (state, { payload }) => {
state.savingStatus = payload.status;
},
+ updateResetStatus: (state, { payload }) => {
+ state.resetStatus = payload.status;
+ },
updateCourseAppsApiStatus: (state, { payload }) => {
state.courseAppsApiStatus = payload.status;
},
@@ -38,6 +42,7 @@ export const {
fetchCourseAppsSuccess,
updateLoadingStatus,
updateSavingStatus,
+ updateResetStatus,
updateCourseAppsApiStatus,
fetchCourseAppsSettingsSuccess,
updateCourseAppsSettingsSuccess,
diff --git a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx
index b99e42257..c90d3a22b 100644
--- a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx
+++ b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.jsx
@@ -44,6 +44,8 @@ const XpertUnitSummarySettings = ({ intl }) => {
}
enableAppLabel={intl.formatMessage(messages.enableXpertUnitSummaryLabel)}
learnMoreText={intl.formatMessage(messages.enableXpertUnitSummaryLink)}
+ allUnitsEnabledText={intl.formatMessage(messages.allUnitsEnabledByDefault)}
+ noUnitsEnabledText={intl.formatMessage(messages.noUnitsEnabledByDefault)}
onClose={handleClose}
/>
);
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 c6f2e98b6..bd682d96a 100644
--- a/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx
+++ b/src/pages-and-resources/xpert-unit-summary/XpertUnitSummarySettings.test.jsx
@@ -112,12 +112,12 @@ describe('XpertUnitSummarySettings', () => {
renderComponent();
});
- test('Shows enabled if enabled from backend', async () => {
+ test('Shows switch on 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 () => {
+ test('Shows switch on if disabled from backend', async () => {
axiosMock.onGet(API.getXpertSettingsUrl(courseId))
.reply(200, generateCourseLevelAPIRepsonse({
success: true,
@@ -126,8 +126,25 @@ describe('XpertUnitSummarySettings', () => {
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();
+ expect(container.querySelector('#enable-xpert-unit-summary-toggle').checked).toBeTruthy();
+ expect(queryByTestId(container, 'enable-badge')).toBeTruthy();
+ });
+
+ test('Shows enable radio selected if enabled from backend', async () => {
+ await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
+ expect(queryByTestId(container, 'enable-radio').checked).toBeTruthy();
+ });
+
+ test('Shows disable radio selected if enabled 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(queryByTestId(container, 'disable-radio').checked).toBeTruthy();
});
});
@@ -136,7 +153,7 @@ describe('XpertUnitSummarySettings', () => {
axiosMock.onGet(API.getXpertSettingsUrl(courseId))
.reply(400, generateCourseLevelAPIRepsonse({
success: false,
- enabled: false,
+ enabled: undefined,
}));
renderComponent();
@@ -151,6 +168,12 @@ describe('XpertUnitSummarySettings', () => {
describe('saving configuration changes', () => {
beforeEach(() => {
+ axiosMock.onGet(API.getXpertSettingsUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: false,
+ }));
+
axiosMock.onPost(API.getXpertSettingsUrl(courseId))
.reply(200, generateCourseLevelAPIRepsonse({
success: true,
@@ -164,8 +187,10 @@ describe('XpertUnitSummarySettings', () => {
jest.spyOn(API, 'postXpertSettings');
await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
+ expect(queryByTestId(container, 'disable-radio').checked).toBeTruthy();
+ fireEvent.click(queryByTestId(container, 'enable-radio'));
fireEvent.click(getByText(container, 'Save'));
- await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).not.toBeTruthy());
+ await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
expect(API.postXpertSettings).toBeCalled();
});
});
@@ -186,4 +211,78 @@ describe('XpertUnitSummarySettings', () => {
expect(API.getXpertPluginConfigurable).toBeCalled();
});
});
+
+ describe('removing course configuration', () => {
+ beforeEach(() => {
+ axiosMock.onGet(API.getXpertSettingsUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: true,
+ }));
+
+ axiosMock.onDelete(API.getXpertSettingsUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: undefined,
+ }));
+
+ renderComponent();
+ });
+
+ test('Deleting course configuration', async () => {
+ jest.spyOn(API, 'deleteXpertSettings');
+
+ await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
+ fireEvent.click(container.querySelector('#enable-xpert-unit-summary-toggle'));
+ fireEvent.click(getByText(container, 'Save'));
+ await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
+ expect(API.deleteXpertSettings).toBeCalled();
+ });
+ });
+
+ describe('resetting course units', () => {
+ test('reset all units to be enabled', async () => {
+ axiosMock.onGet(API.getXpertSettingsUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: true,
+ }));
+
+ axiosMock.onPost(API.getXpertSettingsUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: true,
+ }));
+
+ renderComponent();
+
+ jest.spyOn(API, 'postXpertSettings');
+
+ await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
+ fireEvent.click(queryByTestId(container, 'reset-units'));
+ expect(API.postXpertSettings).toBeCalledWith(courseId, { reset: true, enabled: true });
+ });
+
+ test('reset all units to be disabled', async () => {
+ axiosMock.onGet(API.getXpertSettingsUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: false,
+ }));
+
+ axiosMock.onPost(API.getXpertSettingsUrl(courseId))
+ .reply(200, generateCourseLevelAPIRepsonse({
+ success: true,
+ enabled: false,
+ }));
+
+ renderComponent();
+
+ jest.spyOn(API, 'postXpertSettings');
+
+ await waitFor(() => expect(container.querySelector('#enable-xpert-unit-summary-toggle')).toBeTruthy());
+ fireEvent.click(queryByTestId(container, 'reset-units'));
+ expect(API.postXpertSettings).toBeCalledWith(courseId, { reset: true, enabled: false });
+ });
+ });
});
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 b7b489861..32233ac2b 100644
--- a/src/pages-and-resources/xpert-unit-summary/data/api.js
+++ b/src/pages-and-resources/xpert-unit-summary/data/api.js
@@ -20,6 +20,7 @@ export async function postXpertSettings(courseId, state) {
const { data } = await getAuthenticatedHttpClient()
.post(getXpertSettingsUrl(courseId), {
enabled: state.enabled,
+ reset: state.reset,
});
return data;
@@ -31,3 +32,10 @@ export async function getXpertPluginConfigurable(courseId) {
return data;
}
+
+export async function deleteXpertSettings(courseId) {
+ const { data } = await getAuthenticatedHttpClient()
+ .delete(getXpertSettingsUrl(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
index c10fb7529..b79a36ccb 100644
--- a/src/pages-and-resources/xpert-unit-summary/data/thunks.js
+++ b/src/pages-and-resources/xpert-unit-summary/data/thunks.js
@@ -1,6 +1,8 @@
-import { getXpertSettings, postXpertSettings, getXpertPluginConfigurable } from './api';
+import {
+ getXpertSettings, postXpertSettings, getXpertPluginConfigurable, deleteXpertSettings,
+} from './api';
-import { updateSavingStatus, updateLoadingStatus } from '../../data/slice';
+import { updateSavingStatus, updateLoadingStatus, updateResetStatus } from '../../data/slice';
import { RequestStatus } from '../../../data/constants';
import { addModel, updateModel } from '../../../generic/model-store';
@@ -27,13 +29,13 @@ export function updateXpertSettings(courseId, state) {
export function fetchXpertPluginConfigurable(courseId) {
return async (dispatch) => {
- let enabled = false;
+ let enabled;
dispatch(updateLoadingStatus({ status: RequestStatus.PENDING }));
try {
const { response } = await getXpertPluginConfigurable(courseId);
enabled = response?.enabled;
} catch (e) {
- enabled = false;
+ enabled = undefined;
}
dispatch(addModel({
@@ -48,14 +50,14 @@ export function fetchXpertPluginConfigurable(courseId) {
export function fetchXpertSettings(courseId) {
return async (dispatch) => {
- let enabled = false;
+ let enabled;
dispatch(updateLoadingStatus({ status: RequestStatus.PENDING }));
try {
const { response } = await getXpertSettings(courseId);
enabled = response?.enabled;
} catch (e) {
- enabled = false;
+ enabled = undefined;
}
dispatch(addModel({
@@ -69,3 +71,44 @@ export function fetchXpertSettings(courseId) {
dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
};
}
+
+export function removeXpertSettings(courseId) {
+ return async (dispatch) => {
+ dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
+
+ try {
+ const { response } = await deleteXpertSettings(courseId);
+ const { success } = response;
+ if (success) {
+ const model = { id: 'xpert-unit-summary', enabled: undefined };
+ dispatch(updateModel({ modelType: 'XpertSettings', model }));
+ 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 resetXpertSettings(courseId, state) {
+ return async (dispatch) => {
+ dispatch(updateResetStatus({ status: RequestStatus.PENDING }));
+ try {
+ const { response } = await postXpertSettings(courseId, state);
+ const { success } = response;
+ if (success) {
+ dispatch(updateResetStatus({ status: RequestStatus.SUCCESSFUL }));
+ return true;
+ }
+ dispatch(updateResetStatus({ status: RequestStatus.FAILED }));
+ return false;
+ } catch (error) {
+ dispatch(updateResetStatus({ status: RequestStatus.FAILED }));
+ return false;
+ }
+ };
+}
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 b75b7f374..733c09308 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
@@ -4,12 +4,17 @@ import {
Alert,
Badge,
Form,
+ Icon,
ModalDialog,
+ OverlayTrigger,
StatefulButton,
+ Tooltip,
TransitionReplace,
Hyperlink,
} from '@edx/paragon';
-import { Info } from '@edx/paragon/icons';
+import {
+ Info, CheckCircleOutline, RotateLeft, SpinnerSimple,
+} from '@edx/paragon/icons';
import { Formik } from 'formik';
import PropTypes from 'prop-types';
@@ -26,9 +31,9 @@ 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 { getLoadingStatus, getSavingStatus, getResetStatus } from '../../data/selectors';
+import { updateSavingStatus, updateResetStatus } from '../../data/slice';
+import { updateXpertSettings, resetXpertSettings, removeXpertSettings } from '../data/thunks';
import AppConfigFormDivider from '../../discussions/app-config-form/apps/shared/AppConfigFormDivider';
import { PagesAndResourcesContext } from '../../PagesAndResourcesProvider';
import messages from './messages';
@@ -105,6 +110,84 @@ 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,
@@ -119,6 +202,8 @@ const SettingsModal = ({
enableAppHelp,
learnMoreText,
enableReinitialize,
+ allUnitsEnabledText,
+ noUnitsEnabledText,
}) => {
const { courseId } = useContext(PagesAndResourcesContext);
const loadingStatus = useSelector(getLoadingStatus);
@@ -139,9 +224,15 @@ const SettingsModal = ({
}
}, [updateSettingsRequestStatus]);
- const handleFormSubmit = async (values) => {
- let success = true;
- success = await dispatch(updateXpertSettings(courseId, values));
+ 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);
@@ -174,13 +265,15 @@ const SettingsModal = ({
return (
)}
>
@@ -220,7 +314,7 @@ const SettingsModal = ({
formikProps.handleChange(event)}
+ onChange={formikProps.handleChange}
onBlur={formikProps.handleBlur}
checked={formikProps.values.enabled}
label={(
@@ -240,6 +334,41 @@ const SettingsModal = ({
)}
/>
+ {(formikProps.values.enabled || configureBeforeEnable) && (
+
+
+ {allUnitsEnabledText}
+
+
+
+ {noUnitsEnabledText}
+
+
+
+ )}
{(formikProps.values.enabled || configureBeforeEnable) && children
&& }
@@ -281,6 +410,8 @@ SettingsModal.propTypes = {
enableAppLabel: PropTypes.string.isRequired,
enableAppHelp: PropTypes.string.isRequired,
learnMoreText: PropTypes.string.isRequired,
+ allUnitsEnabledText: PropTypes.string.isRequired,
+ noUnitsEnabledText: PropTypes.string.isRequired,
configureBeforeEnable: PropTypes.bool,
enableReinitialize: PropTypes.bool,
};
diff --git a/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.scss b/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.scss
new file mode 100644
index 000000000..c37d257db
--- /dev/null
+++ b/src/pages-and-resources/xpert-unit-summary/settings-modal/SettingsModal.scss
@@ -0,0 +1,31 @@
+.summary-radio {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ border-width: $border-width;
+ border-color: $border-color;
+ border-radius: $border-radius;
+ border-style: solid;
+
+ &:has(input:checked) {
+ border-width: 3px;
+ border-color: theme-color("primary");
+ }
+
+ > div {
+ flex: 1;
+
+ > label {
+ height: 80px;
+ }
+ }
+}
+
+.reset-units-button {
+ color: $link-color;
+ border-width: $border-width;
+ border-color: $border-color;
+ border-radius: $border-radius;
+ border-style: solid;
+ margin-left: auto;
+}
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
index b5586e993..5dd2c2f4b 100644
--- a/src/pages-and-resources/xpert-unit-summary/settings-modal/messages.js
+++ b/src/pages-and-resources/xpert-unit-summary/settings-modal/messages.js
@@ -29,6 +29,22 @@ const messages = defineMessages({
id: 'course-authoring.pages-resources.app-settings-modal.badge.disabled',
defaultMessage: 'Disabled',
},
+ resetAllUnits: {
+ id: 'course-authoring.pages-resources.app-settings-modal.reset-all-units',
+ defaultMessage: 'Reset all units',
+ },
+ resetAllUnitsTooltipChecked: {
+ id: 'course-authoring.pages-resources.app-settings-modal.reset-all-units-tooltip.checked',
+ defaultMessage: 'Immediately reset any unit-level changes and checked "Enable summaries" on all units.',
+ },
+ resetAllUnitsTooltipUnchecked: {
+ id: 'course-authoring.pages-resources.app-settings-modal.reset-all-units-tooltip.unchecked',
+ defaultMessage: 'Immediately reset any unit-level changes and unchecked "Enable summaries" on all units.',
+ },
+ reset: {
+ id: 'course-authoring.pages-resources.app-settings-modal.reset',
+ defaultMessage: 'Reset',
+ },
errorSavingTitle: {
id: 'course-authoring.pages-resources.app-settings-modal.save-error.title',
defaultMessage: 'We couldn\'t apply your changes.',