Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ui bugs #542

Merged
merged 8 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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