feat: implement export page (#586)
This commit is contained in:
committed by
GitHub
parent
fb28693854
commit
1888993113
107
src/generic/course-stepper/CourseStepper.test.jsx
Normal file
107
src/generic/course-stepper/CourseStepper.test.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import CourseStepper from '.';
|
||||
|
||||
const stepsMock = [
|
||||
{
|
||||
title: 'Preparing',
|
||||
description: 'Preparing to start the export',
|
||||
},
|
||||
{
|
||||
title: 'Exporting',
|
||||
description: 'Creating the export data files (You can now leave this page safely, but avoid making drastic changes to content until this export is complete',
|
||||
},
|
||||
{
|
||||
title: 'Compressing',
|
||||
description: 'Compressing the exported data and preparing it for download',
|
||||
},
|
||||
{
|
||||
title: 'Success',
|
||||
description: 'Your exported course can now be downloaded',
|
||||
},
|
||||
];
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<IntlProvider locale="en">
|
||||
<CourseStepper steps={stepsMock} {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
describe('<CourseStepper />', () => {
|
||||
it('renders CourseStepper correctly', () => {
|
||||
const {
|
||||
getByText, getByTestId, getAllByTestId, queryByTestId,
|
||||
} = renderComponent({ activeKey: 0 });
|
||||
|
||||
const steps = getAllByTestId('course-stepper__step');
|
||||
expect(steps.length).toBe(stepsMock.length);
|
||||
|
||||
stepsMock.forEach((step) => {
|
||||
expect(getByText(step.title)).toBeInTheDocument();
|
||||
expect(getByText(step.description)).toBeInTheDocument();
|
||||
expect(getByTestId(`${step.title}-icon`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const percentElement = queryByTestId('course-stepper__step-percent');
|
||||
expect(percentElement).toBeNull();
|
||||
});
|
||||
|
||||
it('marks the active and done steps correctly', () => {
|
||||
const activeKey = 1;
|
||||
const { getAllByTestId } = renderComponent({ activeKey });
|
||||
|
||||
const steps = getAllByTestId('course-stepper__step');
|
||||
stepsMock.forEach((_, index) => {
|
||||
const stepElement = steps[index];
|
||||
if (index === activeKey) {
|
||||
expect(stepElement).toHaveClass('active');
|
||||
expect(stepElement).not.toHaveClass('done');
|
||||
}
|
||||
if (index < activeKey) {
|
||||
expect(stepElement).not.toHaveClass('active');
|
||||
expect(stepElement).toHaveClass('done');
|
||||
}
|
||||
if (index > activeKey) {
|
||||
expect(stepElement).not.toHaveClass('active');
|
||||
expect(stepElement).not.toHaveClass('done');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('mark the error step correctly', () => {
|
||||
const { getAllByTestId } = renderComponent({ activeKey: 1, hasError: true });
|
||||
|
||||
const errorStep = getAllByTestId('course-stepper__step')[1];
|
||||
expect(errorStep).toHaveClass('error');
|
||||
});
|
||||
|
||||
it('shows error message for error step', () => {
|
||||
const errorMessage = 'Some error text';
|
||||
const { getAllByTestId } = renderComponent({ activeKey: 1, hasError: true, errorMessage });
|
||||
|
||||
const errorStep = getAllByTestId('course-stepper__step')[1];
|
||||
expect(errorStep).toHaveClass('error');
|
||||
});
|
||||
|
||||
it('shows percentage for active step', () => {
|
||||
const percent = 50;
|
||||
const { getByTestId } = renderComponent({ activeKey: 1, percent });
|
||||
|
||||
const percentElement = getByTestId('course-stepper__step-percent');
|
||||
expect(percentElement).toBeInTheDocument();
|
||||
expect(percentElement).toHaveTextContent(`${percent}%`);
|
||||
});
|
||||
|
||||
it('shows null when steps length equal to zero', () => {
|
||||
const { queryByTestId } = render(
|
||||
<IntlProvider locale="en">
|
||||
<CourseStepper steps={[]} activeKey={0} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const steps = queryByTestId('[data-testid="course-stepper__step"]');
|
||||
expect(steps).toBe(null);
|
||||
});
|
||||
});
|
||||
68
src/generic/course-stepper/CouseStepper.scss
Normal file
68
src/generic/course-stepper/CouseStepper.scss
Normal file
@@ -0,0 +1,68 @@
|
||||
.course-stepper {
|
||||
.course-stepper__step {
|
||||
display: flex;
|
||||
gap: $spacer;
|
||||
padding: 1.25rem 0;
|
||||
opacity: .5;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $gray-200;
|
||||
}
|
||||
|
||||
.course-stepper__step-icon {
|
||||
position: relative;
|
||||
|
||||
& svg {
|
||||
position: absolute;
|
||||
top: .875rem;
|
||||
left: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.course-stepper__step-info {
|
||||
margin-left: 1.875rem;
|
||||
}
|
||||
|
||||
.course-stepper__step-title {
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.course-stepper__step-percent {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.course-stepper__step-description {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: $gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
.course-stepper__step.active {
|
||||
opacity: 1;
|
||||
|
||||
& svg {
|
||||
animation: rotate 2s infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
.course-stepper__step.done {
|
||||
opacity: 1;
|
||||
|
||||
& svg,
|
||||
.course-stepper__step-title {
|
||||
color: $success-500;
|
||||
}
|
||||
}
|
||||
|
||||
.course-stepper__step.error {
|
||||
opacity: 1;
|
||||
|
||||
.course-stepper__step-title,
|
||||
.course-stepper__step-description,
|
||||
& svg {
|
||||
color: $danger-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
114
src/generic/course-stepper/index.jsx
Normal file
114
src/generic/course-stepper/index.jsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Settings as SettingsIcon,
|
||||
CheckBoxOutlineBlank as SuccessIcon,
|
||||
LibraryAddCheck as SuccessDoneIcon,
|
||||
Warning as ErrorIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const CourseStepper = ({
|
||||
steps,
|
||||
activeKey,
|
||||
percent,
|
||||
hasError,
|
||||
errorMessage,
|
||||
}) => {
|
||||
const getStepperSettings = (index) => {
|
||||
const lastStepIndex = steps.length - 1;
|
||||
const isActiveStep = index === activeKey;
|
||||
const isLastStep = index === lastStepIndex;
|
||||
const isErrorStep = isActiveStep && hasError;
|
||||
const isLastStepDone = isLastStep && isActiveStep;
|
||||
|
||||
const getStepIcon = () => {
|
||||
if (hasError && isActiveStep) {
|
||||
return ErrorIcon;
|
||||
}
|
||||
if (isLastStep && !isActiveStep) {
|
||||
return SuccessIcon;
|
||||
}
|
||||
if (isLastStepDone) {
|
||||
return SuccessDoneIcon;
|
||||
}
|
||||
|
||||
return SettingsIcon;
|
||||
};
|
||||
|
||||
return {
|
||||
stepIcon: getStepIcon(index),
|
||||
isPercentShow: Boolean(percent) && percent !== 100 && isActiveStep && !hasError,
|
||||
isErrorMessageShow: isErrorStep && errorMessage,
|
||||
isActiveClass: isActiveStep && !isLastStep && !hasError,
|
||||
isDoneClass: index < activeKey || isLastStepDone,
|
||||
isErrorClass: isErrorStep,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="course-stepper">
|
||||
{steps.length ? steps.map(({ title, description }, index) => {
|
||||
const {
|
||||
stepIcon,
|
||||
isPercentShow,
|
||||
isErrorMessageShow,
|
||||
isActiveClass,
|
||||
isDoneClass,
|
||||
isErrorClass,
|
||||
} = getStepperSettings(index);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('course-stepper__step', {
|
||||
active: isActiveClass,
|
||||
done: isDoneClass,
|
||||
error: isErrorClass,
|
||||
})}
|
||||
key={title}
|
||||
data-testid="course-stepper__step"
|
||||
>
|
||||
<div className="course-stepper__step-icon">
|
||||
<Icon src={stepIcon} alt={title} data-testid={`${title}-icon`} />
|
||||
</div>
|
||||
<div className="course-stepper__step-info">
|
||||
<h3 className="h4 title course-stepper__step-title font-weight-600">{title}</h3>
|
||||
{isPercentShow && (
|
||||
<p
|
||||
className="course-stepper__step-percent font-weight-400"
|
||||
data-testid="course-stepper__step-percent"
|
||||
>
|
||||
{percent}%
|
||||
</p>
|
||||
)}
|
||||
<p className="course-stepper__step-description font-weight-400">
|
||||
{isErrorMessageShow ? errorMessage : description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CourseStepper.defaultProps = {
|
||||
percent: false,
|
||||
hasError: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
|
||||
CourseStepper.propTypes = {
|
||||
steps: PropTypes.arrayOf(PropTypes.shape({
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
activeKey: PropTypes.number.isRequired,
|
||||
percent: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),
|
||||
errorMessage: PropTypes.string,
|
||||
hasError: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseStepper);
|
||||
33
src/generic/modal-error/ModalError.jsx
Normal file
33
src/generic/modal-error/ModalError.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ActionRow, AlertModal, Button } from '@edx/paragon';
|
||||
|
||||
const ModalError = ({
|
||||
isOpen, title, message, handleCancel, handleAction, cancelButtonText, actionButtonText,
|
||||
}) => (
|
||||
<AlertModal
|
||||
title={title}
|
||||
isOpen={isOpen}
|
||||
variant="danger"
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="tertiary" onClick={handleCancel}>{cancelButtonText}</Button>
|
||||
<Button onClick={handleAction}>{actionButtonText}</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<p>{message}</p>
|
||||
</AlertModal>
|
||||
);
|
||||
|
||||
ModalError.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
handleCancel: PropTypes.func.isRequired,
|
||||
handleAction: PropTypes.func.isRequired,
|
||||
cancelButtonText: PropTypes.string.isRequired,
|
||||
actionButtonText: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ModalError;
|
||||
@@ -4,3 +4,4 @@
|
||||
@import "./section-sub-header/SectionSubHeader";
|
||||
@import "./processing-notification/ProccessingNotification";
|
||||
@import "./WysiwygEditor";
|
||||
@import "./course-stepper/CouseStepper";
|
||||
|
||||
Reference in New Issue
Block a user