Skip to content

Commit

Permalink
fix: ui bugs (openedx#542)
Browse files Browse the repository at this point in the history
  • Loading branch information
KristinAoki authored and PKulkoRaccoonGang committed Aug 1, 2023
1 parent 33e473e commit 65dda8d
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 138 deletions.
120 changes: 54 additions & 66 deletions src/advanced-settings/AdvancedSettings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import {
Container, Button, Layout, StatefulButton,
Container, Button, Layout, StatefulButton, TransitionReplace,
} from '@edx/paragon';
import { CheckCircle, Info, Warning } from '@edx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
Expand All @@ -25,21 +25,28 @@ import messages from './messages';
import ModalError from './modal-error/ModalError';

const AdvancedSettings = ({ intl, courseId }) => {
const advancedSettingsData = useSelector(getCourseAppSettings);
const savingStatus = useSelector(getSavingStatus);
const proctoringExamErrors = useSelector(getProctoringExamErrors);
const settingsWithSendErrors = useSelector(getSendRequestErrors) || {};
const dispatch = useDispatch();
const [saveSettingsPrompt, showSaveSettingsPrompt] = useState(false);
const [showDeprecated, setShowDeprecated] = useState(false);
const [errorModal, showErrorModal] = useState(false);
const [editedSettings, setEditedSettings] = useState({});
const [errorFields, setErrorFields] = useState([]);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const loadingSettingsStatus = useSelector(getLoadingStatus);
const [isQueryPending, setIsQueryPending] = useState(false);
const [isEditableState, setIsEditableState] = useState(false);
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);

useEffect(() => {
dispatch(fetchCourseAppSettings(courseId));
dispatch(fetchProctoringExamErrors(courseId));
}, [courseId]);

const advancedSettingsData = useSelector(getCourseAppSettings);
const savingStatus = useSelector(getSavingStatus);
const proctoringExamErrors = useSelector(getProctoringExamErrors);
const settingsWithSendErrors = useSelector(getSendRequestErrors) || {};
const loadingSettingsStatus = useSelector(getLoadingStatus);

const isLoading = loadingSettingsStatus === RequestStatus.IN_PROGRESS;
const updateSettingsButtonState = {
labels: {
Expand All @@ -49,20 +56,14 @@ const AdvancedSettings = ({ intl, courseId }) => {
disabledStates: ['pending'],
};

useEffect(() => {
dispatch(fetchCourseAppSettings(courseId));
dispatch(fetchProctoringExamErrors(courseId));
}, [courseId]);

useEffect(() => {
if (savingStatus === RequestStatus.SUCCESSFUL) {
setIsQueryPending(false);
setShowSuccessAlert(true);
setIsEditableState(false);
setTimeout(() => setShowSuccessAlert(false), 15000);
window.scrollTo({ top: 0, behavior: 'smooth' });

if (!isEditableState) {
showSaveSettingsPrompt(false);
}
showSaveSettingsPrompt(false);
} else if (savingStatus === RequestStatus.FAILED && !hasInternetConnectionError) {
setErrorFields(settingsWithSendErrors);
showErrorModal(true);
Expand All @@ -81,26 +82,11 @@ const AdvancedSettings = ({ intl, courseId }) => {
);
}

const handleSettingChange = (e, settingName) => {
const { value } = e.target;
if (!saveSettingsPrompt) {
showSaveSettingsPrompt(true);
}
setIsEditableState(true);
setShowSuccessAlert(false);
setEditedSettings((prevEditedSettings) => ({
...prevEditedSettings,
[settingName]: value,
}));
};

const handleResetSettingsValues = () => {
setIsEditableState(false);
showErrorModal(false);
setEditedSettings({});
showSaveSettingsPrompt(false);
setInternetConnectionError(false);
setIsQueryPending(false);
};

const handleSettingBlur = () => {
Expand All @@ -111,9 +97,7 @@ const AdvancedSettings = ({ intl, courseId }) => {
const isValid = validateAdvancedSettingsData(editedSettings, setErrorFields, setEditedSettings);
if (isValid) {
setIsQueryPending(true);
setIsEditableState(false);
} else {
setIsQueryPending(false);
showSaveSettingsPrompt(false);
showErrorModal(!errorModal);
}
Expand All @@ -123,7 +107,6 @@ const AdvancedSettings = ({ intl, courseId }) => {
setInternetConnectionError(true);
showSaveSettingsPrompt(false);
setShowSuccessAlert(false);
setIsQueryPending(false);
};

const handleQueryProcessing = () => {
Expand All @@ -132,15 +115,13 @@ const AdvancedSettings = ({ intl, courseId }) => {
};

const handleManuallyChangeClick = (setToState) => {
setIsEditableState(true);
showErrorModal(setToState);
showSaveSettingsPrompt(true);
setIsQueryPending(false);
};

return (
<>
<Container size="xl" className="m-4">
<Container size="xl" className="px-4">
<div className="setting-header mt-5">
{(proctoringExamErrors?.length > 0) && (
<AlertProctoringError
Expand All @@ -151,17 +132,27 @@ const AdvancedSettings = ({ intl, courseId }) => {
aria-describedby={intl.formatMessage(messages.alertProctoringDescribedby)}
/>
)}
<AlertMessage
show={showSuccessAlert}
variant="success"
icon={CheckCircle}
title={intl.formatMessage(messages.alertSuccess)}
description={intl.formatMessage(messages.alertSuccessDescriptions)}
aria-hidden="true"
aria-labelledby={intl.formatMessage(messages.alertSuccessAriaLabelledby)}
aria-describedby={intl.formatMessage(messages.alertSuccessAriaDescribedby)}
/>
<TransitionReplace>
{showSuccessAlert ? (
<AlertMessage
key={intl.formatMessage(messages.alertSuccessAriaLabelledby)}
show={showSuccessAlert}
variant="success"
icon={CheckCircle}
title={intl.formatMessage(messages.alertSuccess)}
description={intl.formatMessage(messages.alertSuccessDescriptions)}
aria-hidden="true"
aria-labelledby={intl.formatMessage(messages.alertSuccessAriaLabelledby)}
aria-describedby={intl.formatMessage(messages.alertSuccessAriaDescribedby)}
/>
) : null}
</TransitionReplace>
</div>
<SubHeader
subtitle={intl.formatMessage(messages.headingSubtitle)}
title={intl.formatMessage(messages.headingTitle)}
contentTitle={intl.formatMessage(messages.policy)}
/>
<section className="setting-items mb-4">
<Layout
lg={[{ span: 9 }, { span: 3 }]}
Expand All @@ -174,18 +165,13 @@ const AdvancedSettings = ({ intl, courseId }) => {
<article>
<div>
<section className="setting-items-policies">
<SubHeader
subtitle={intl.formatMessage(messages.headingSubtitle)}
title={intl.formatMessage(messages.headingTitle)}
contentTitle={intl.formatMessage(messages.policy)}
instruction={(
<FormattedMessage
id="course-authoring.advanced-settings.policies.description"
defaultMessage="{notice} Do not modify these policies unless you are familiar with their purpose."
values={{ notice: <strong>Warning: </strong> }}
/>
)}
/>
<div className="small">
<FormattedMessage
id="course-authoring.advanced-settings.policies.description"
defaultMessage="{notice} Do not modify these policies unless you are familiar with their purpose."
values={{ notice: <strong>Warning: </strong> }}
/>
</div>
<div className="setting-items-deprecated-setting">
<Button
variant={showDeprecated ? 'outline-brand' : 'tertiary'}
Expand All @@ -204,20 +190,22 @@ const AdvancedSettings = ({ intl, courseId }) => {
</Button>
</div>
<ul className="setting-items-list p-0">
{Object.keys(advancedSettingsData).sort().map((settingName) => {
{Object.keys(advancedSettingsData).map((settingName) => {
const settingData = advancedSettingsData[settingName];
const editedValue = editedSettings[settingName] !== undefined
? editedSettings[settingName] : JSON.stringify(settingData.value, null, 4);

if (settingData.deprecated && !showDeprecated) {
return null;
}
return (
<SettingCard
key={settingName}
settingData={settingData}
onChange={(e) => handleSettingChange(e, settingName)}
showDeprecated={showDeprecated}
name={settingName}
value={editedValue}
showSaveSettingsPrompt={showSaveSettingsPrompt}
saveSettingsPrompt={saveSettingsPrompt}
setEdited={setEditedSettings}
handleBlur={handleSettingBlur}
isEditableState={isEditableState}
setIsEditableState={setIsEditableState}
/>
);
})}
Expand All @@ -233,7 +221,7 @@ const AdvancedSettings = ({ intl, courseId }) => {
</section>
</Container>
<div className="alert-toast">
{!isEditableState && (
{isQueryPending && (
<InternetConnectionAlert
isFailed={savingStatus === RequestStatus.FAILED}
isQueryPending={isQueryPending}
Expand Down
49 changes: 39 additions & 10 deletions src/advanced-settings/AdvancedSettings.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { render, fireEvent, waitFor } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';

import initializeStore from '../store';
import { executeThunk } from '../utils';
import { advancedSettingsMock } from './__mocks__';
import { getCourseAdvancedSettingsApiUrl } from './data/api';
import { updateCourseAppSetting } from './data/thunks';
import AdvancedSettings from './AdvancedSettings';
import messages from './messages';

Expand Down Expand Up @@ -70,10 +72,11 @@ describe('<AdvancedSettings />', () => {
});
});
it('should render setting element', async () => {
const { getByText } = render(<RootWrapper />);
const { getByText, queryByText } = render(<RootWrapper />);
await waitFor(() => {
const advancedModuleListTitle = getByText(/Advanced Module List/i);
expect(advancedModuleListTitle).toBeInTheDocument();
expect(queryByText('Certificate web/html view enabled')).toBeNull();
});
});
it('should change to onСhange', async () => {
Expand Down Expand Up @@ -112,24 +115,50 @@ describe('<AdvancedSettings />', () => {
fireEvent.click(showDeprecatedItemsBtn);
expect(getByText(/Hide Deprecated Settings/i)).toBeInTheDocument();
});
expect(getByText('Certificate web/html view enabled')).toBeInTheDocument();
});
it('should reset to default value on click on Cancel button', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
let textarea;
await waitFor(() => {
const textarea = getByLabelText(/Advanced Module List/i);
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
fireEvent.click(getByText(messages.buttonCancelText.defaultMessage));
expect(textarea.value).toBe('[]');
textarea = getByLabelText(/Advanced Module List/i);
});
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
expect(textarea.value).toBe('[3, 2, 1]');
fireEvent.click(getByText(messages.buttonCancelText.defaultMessage));
expect(textarea.value).toBe('[]');
});
it('should update the textarea value and display the updated value after clicking "Change manually"', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
let textarea;
await waitFor(() => {
const textarea = getByLabelText(/Advanced Module List/i);
fireEvent.change(textarea, { target: { value: '[3, 2, 1' } });
fireEvent.click(getByText(messages.buttonSaveText.defaultMessage));
fireEvent.click(getByText(/Change manually/i));
expect(textarea.value).toBe('[3, 2, 1');
textarea = getByLabelText(/Advanced Module List/i);
});
fireEvent.change(textarea, { target: { value: '[3, 2, 1,' } });
expect(textarea.value).toBe('[3, 2, 1,');
fireEvent.click(getByText('Save changes'));
fireEvent.click(getByText('Change manually'));
expect(textarea.value).toBe('[3, 2, 1,');
});
it('should show success alert after save', async () => {
const { getByLabelText, getByText } = render(<RootWrapper />);
let textarea;
await waitFor(() => {
textarea = getByLabelText(/Advanced Module List/i);
});
fireEvent.change(textarea, { target: { value: '[3, 2, 1]' } });
expect(textarea.value).toBe('[3, 2, 1]');
axiosMock
.onPatch(`${getCourseAdvancedSettingsApiUrl(courseId)}`)
.reply(200, {
...advancedSettingsMock,
advancedModules: {
...advancedSettingsMock.advancedModules,
value: [3, 2, 1],
},
});
fireEvent.click(getByText('Save changes'));
await executeThunk(updateCourseAppSetting(courseId, [3, 2, 1]), store.dispatch);
expect(getByText('Your policy changes have been saved.')).toBeInTheDocument();
});
});
7 changes: 7 additions & 0 deletions src/advanced-settings/__mocks__/advancedSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ module.exports = {
hideOnEnabledPublisher: false,
value: [],
},
certHtmlViewEnabled: {
deprecated: true,
display_name: 'Certificate web/html view enabled',
help: 'If true, certificate Web/HTML views are enabled for the course.',
hide_on_enabled_publisher: false,
value: true,
},
};
15 changes: 14 additions & 1 deletion src/advanced-settings/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,20 @@ export function fetchCourseAppSettings(courseId) {

try {
const settingValues = await getCourseAdvancedSettings(courseId);
dispatch(fetchCourseAppsSettingsSuccess(settingValues));
const sortedDisplayName = [];
Object.values(settingValues).forEach(value => {
const { displayName } = value;
sortedDisplayName.push(displayName);
});
const sortedSettingValues = {};
sortedDisplayName.sort().forEach((displayName => {
Object.entries(settingValues).forEach(([key, value]) => {
if (value.displayName === displayName) {
sortedSettingValues[key] = value;
}
});
}));
dispatch(fetchCourseAppsSettingsSuccess(sortedSettingValues));
dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) {
if (error.response && error.response.status === 403) {
Expand Down
Loading

0 comments on commit 65dda8d

Please sign in to comment.