fix: ui bugs (#542)
This commit is contained in:
@@ -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';
|
||||
@@ -25,10 +25,6 @@ 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);
|
||||
@@ -36,10 +32,21 @@ const AdvancedSettings = ({ intl, courseId }) => {
|
||||
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: {
|
||||
@@ -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);
|
||||
@@ -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 = () => {
|
||||
@@ -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);
|
||||
}
|
||||
@@ -123,7 +107,6 @@ const AdvancedSettings = ({ intl, courseId }) => {
|
||||
setInternetConnectionError(true);
|
||||
showSaveSettingsPrompt(false);
|
||||
setShowSuccessAlert(false);
|
||||
setIsQueryPending(false);
|
||||
};
|
||||
|
||||
const handleQueryProcessing = () => {
|
||||
@@ -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
|
||||
@@ -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 }]}
|
||||
@@ -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'}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -233,7 +221,7 @@ const AdvancedSettings = ({ intl, courseId }) => {
|
||||
</section>
|
||||
</Container>
|
||||
<div className="alert-toast">
|
||||
{!isEditableState && (
|
||||
{isQueryPending && (
|
||||
<InternetConnectionAlert
|
||||
isFailed={savingStatus === RequestStatus.FAILED}
|
||||
isQueryPending={isQueryPending}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 () => {
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
.instructions,
|
||||
strong {
|
||||
font: normal $font-weight-normal .875rem/1.5rem $font-family-base;
|
||||
color: $text-color-base;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,6 @@
|
||||
|
||||
.pgn__card-header .pgn__card-header-title-md {
|
||||
font-size: 1.125rem;
|
||||
padding-top: .625rem;
|
||||
}
|
||||
|
||||
.pgn__icon {
|
||||
color: $headings-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +39,6 @@
|
||||
}
|
||||
|
||||
.form-control {
|
||||
resize: none;
|
||||
min-height: 2.75rem;
|
||||
width: $setting-form-control-width;
|
||||
}
|
||||
@@ -54,7 +48,8 @@
|
||||
}
|
||||
|
||||
.pgn__card-header {
|
||||
padding: 0 1.5rem;
|
||||
padding: 0 0 0 1.5rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.pgn__card-status {
|
||||
@@ -66,17 +61,10 @@
|
||||
margin-bottom: 1.438rem;
|
||||
display: flex;
|
||||
flex-direction: revert;
|
||||
|
||||
.pgn__card-header-subtitle-md {
|
||||
margin-left: .375rem;
|
||||
margin-top: .125rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-sidebar-supplementary {
|
||||
margin-top: 1.875rem;
|
||||
|
||||
.setting-sidebar-supplementary-about {
|
||||
.setting-sidebar-supplementary-about-title {
|
||||
font: normal $font-weight-bold 1.125rem/1.5rem $font-family-base;
|
||||
@@ -123,3 +111,15 @@
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-popup-content {
|
||||
max-width: 200px;
|
||||
color: $white;
|
||||
background-color: $black;
|
||||
filter: drop-shadow(0 2px 4px rgba(0 0 0 / .15));
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pgn__modal-popup__arrow::after {
|
||||
border-top-color: $black;
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card, Form, Icon, IconButton, OverlayTrigger, Popover,
|
||||
ActionRow,
|
||||
Card,
|
||||
Form,
|
||||
Icon,
|
||||
IconButton,
|
||||
ModalPopup,
|
||||
useToggle,
|
||||
} from '@edx/paragon';
|
||||
import { Info, Warning } from '@edx/paragon/icons';
|
||||
import { InfoOutline, Warning } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { capitalize } from 'lodash';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
@@ -12,47 +17,87 @@ import TextareaAutosize from 'react-textarea-autosize';
|
||||
import messages from './messages';
|
||||
|
||||
const SettingCard = ({
|
||||
intl, showDeprecated, name, onChange, value, settingData, handleBlur,
|
||||
name,
|
||||
settingData,
|
||||
handleBlur,
|
||||
setEdited,
|
||||
showSaveSettingsPrompt,
|
||||
saveSettingsPrompt,
|
||||
isEditableState,
|
||||
setIsEditableState,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const { deprecated, help, displayName } = settingData;
|
||||
const initialValue = JSON.stringify(settingData.value, null, 4);
|
||||
const [isOpen, open, close] = useToggle(false);
|
||||
const [target, setTarget] = useState(null);
|
||||
const [newValue, setNewValue] = useState(initialValue);
|
||||
|
||||
const handleSettingChange = (e) => {
|
||||
const { value } = e.target;
|
||||
setNewValue(e.target.value);
|
||||
if (value !== initialValue) {
|
||||
if (!saveSettingsPrompt) {
|
||||
showSaveSettingsPrompt(true);
|
||||
}
|
||||
if (!isEditableState) {
|
||||
setIsEditableState(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCardBlur = () => {
|
||||
setEdited((prevEditedSettings) => ({
|
||||
...prevEditedSettings,
|
||||
[name]: newValue,
|
||||
}));
|
||||
handleBlur();
|
||||
};
|
||||
|
||||
return (
|
||||
<li className={classNames('field-group course-advanced-policy-list-item', { 'd-none': deprecated && !showDeprecated })}>
|
||||
<li className="field-group course-advanced-policy-list-item">
|
||||
<Card className="flex-column setting-card">
|
||||
<Card.Body className="d-flex justify-content-between">
|
||||
<Card.Body className="d-flex">
|
||||
<Card.Header
|
||||
title={capitalize(displayName)}
|
||||
subtitle={(
|
||||
<OverlayTrigger
|
||||
trigger="click"
|
||||
rootClose
|
||||
placement="bottom"
|
||||
overlay={(
|
||||
<Popover id="popover-positioned">
|
||||
<Popover.Content>
|
||||
{/* eslint-disable-next-line react/no-danger */}
|
||||
<div dangerouslySetInnerHTML={{ __html: help }} />
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
>
|
||||
title={(
|
||||
<ActionRow>
|
||||
{capitalize(displayName)}
|
||||
<IconButton
|
||||
src={Info}
|
||||
ref={setTarget}
|
||||
onClick={open}
|
||||
src={InfoOutline}
|
||||
iconAs={Icon}
|
||||
alt={intl.formatMessage(messages.helpButtonText)}
|
||||
variant="light"
|
||||
variant="primary"
|
||||
className=" ml-1 mr-2"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
<ModalPopup
|
||||
hasArrow
|
||||
placement="right"
|
||||
positionRef={target}
|
||||
isOpen={isOpen}
|
||||
onClose={close}
|
||||
className="pgn__modal-popup__arrow"
|
||||
>
|
||||
<div
|
||||
className="p-2 x-small rounded modal-popup-content"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: help }}
|
||||
/>
|
||||
</ModalPopup>
|
||||
</ActionRow>
|
||||
)}
|
||||
/>
|
||||
<Card.Section>
|
||||
<Form.Group className="m-0">
|
||||
<Form.Control
|
||||
as={TextareaAutosize}
|
||||
value={value}
|
||||
value={isEditableState ? newValue : initialValue}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
onChange={handleSettingChange}
|
||||
aria-label={displayName}
|
||||
onBlur={handleBlur}
|
||||
onBlur={handleCardBlur}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Card.Section>
|
||||
@@ -73,22 +118,21 @@ SettingCard.propTypes = {
|
||||
deprecated: PropTypes.bool,
|
||||
help: PropTypes.string,
|
||||
displayName: PropTypes.string,
|
||||
value: PropTypes.PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
PropTypes.number,
|
||||
PropTypes.object,
|
||||
PropTypes.array,
|
||||
]),
|
||||
}).isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
PropTypes.number,
|
||||
PropTypes.object,
|
||||
PropTypes.array,
|
||||
]),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
showDeprecated: PropTypes.bool.isRequired,
|
||||
setEdited: PropTypes.func.isRequired,
|
||||
showSaveSettingsPrompt: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
handleBlur: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SettingCard.defaultProps = {
|
||||
value: undefined,
|
||||
saveSettingsPrompt: PropTypes.bool.isRequired,
|
||||
isEditableState: PropTypes.bool.isRequired,
|
||||
setIsEditableState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SettingCard);
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import SettingCard from './SettingCard';
|
||||
import messages from './messages';
|
||||
|
||||
const handleChange = jest.fn();
|
||||
const setEdited = jest.fn();
|
||||
const showSaveSettingsPrompt = jest.fn();
|
||||
const setIsEditableState = jest.fn();
|
||||
const handleBlur = jest.fn();
|
||||
|
||||
const settingData = {
|
||||
deprecated: false,
|
||||
help: 'This is a help message',
|
||||
displayName: 'Setting Name',
|
||||
value: 'Setting Value',
|
||||
};
|
||||
|
||||
jest.mock('react-textarea-autosize', () => jest.fn((props) => (
|
||||
@@ -27,9 +32,13 @@ const RootWrapper = () => (
|
||||
intl={{}}
|
||||
isOn
|
||||
name="settingName"
|
||||
onChange={handleChange}
|
||||
value="Setting Value"
|
||||
setEdited={setEdited}
|
||||
setIsEditableState={setIsEditableState}
|
||||
showSaveSettingsPrompt={showSaveSettingsPrompt}
|
||||
settingData={settingData}
|
||||
handleBlur={handleBlur}
|
||||
isEditableState
|
||||
saveSettingsPrompt={false}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
@@ -42,7 +51,7 @@ describe('<SettingCard />', () => {
|
||||
const input = getByLabelText(/Setting Name/i);
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input.value).toBe('Setting Value');
|
||||
expect(input.value).toBe(JSON.stringify(settingData.value, null, 4));
|
||||
});
|
||||
it('displays the deprecated status when the setting is deprecated', () => {
|
||||
const deprecatedSettingData = { ...settingData, deprecated: true };
|
||||
@@ -52,9 +61,13 @@ describe('<SettingCard />', () => {
|
||||
intl={{}}
|
||||
isOn
|
||||
name="settingName"
|
||||
onChange={handleChange}
|
||||
value="Setting Value"
|
||||
setEdited={setEdited}
|
||||
setIsEditableState={setIsEditableState}
|
||||
showSaveSettingsPrompt={showSaveSettingsPrompt}
|
||||
settingData={deprecatedSettingData}
|
||||
handleBlur={handleBlur}
|
||||
isEditable={false}
|
||||
saveSettingsPrompt
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
@@ -65,4 +78,19 @@ describe('<SettingCard />', () => {
|
||||
const { queryByText } = render(<RootWrapper />);
|
||||
expect(queryByText(messages.deprecated.defaultMessage)).toBeNull();
|
||||
});
|
||||
it('calls setEdited on blur', async () => {
|
||||
const { getByLabelText } = render(<RootWrapper />);
|
||||
const inputBox = getByLabelText(/Setting Name/i);
|
||||
fireEvent.focus(inputBox);
|
||||
userEvent.clear(inputBox);
|
||||
userEvent.type(inputBox, '3, 2, 1');
|
||||
await waitFor(() => {
|
||||
expect(inputBox).toHaveValue('3, 2, 1');
|
||||
});
|
||||
await (async () => {
|
||||
expect(setEdited).toHaveBeenCalled();
|
||||
expect(handleBlur).toHaveBeenCalled();
|
||||
});
|
||||
fireEvent.focusOut(inputBox);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
useToggle,
|
||||
Image,
|
||||
ModalDialog,
|
||||
Container,
|
||||
} from '@edx/paragon';
|
||||
import { Add, SpinnerSimple } from '@edx/paragon/icons';
|
||||
import Placeholder, {
|
||||
@@ -110,7 +111,7 @@ const CustomPages = ({
|
||||
}
|
||||
return (
|
||||
<CustomPagesProvider courseId={courseId}>
|
||||
<main className="container container-mw-xl p-4 pt-5">
|
||||
<Container size="xl" className="p-4 pt-5">
|
||||
<div className="small gray-700">
|
||||
<Breadcrumb
|
||||
ariaLabel="Custom Page breadcrumbs"
|
||||
@@ -255,7 +256,7 @@ const CustomPages = ({
|
||||
)}
|
||||
</PageRoute>
|
||||
</Switch>
|
||||
</main>
|
||||
</Container>
|
||||
</CustomPagesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
@import "~@edx/frontend-component-footer/dist/footer";
|
||||
@import "proctored-exam-settings/proctoredExamSettings";
|
||||
@import "pages-and-resources/discussions/app-list/AppList";
|
||||
@import "advanced-settings/scss/AdvencedSettings";
|
||||
@import "advanced-settings/scss/AdvancedSettings";
|
||||
@import "generic/styles";
|
||||
|
||||
Reference in New Issue
Block a user