feat: implement export page (#586)

This commit is contained in:
Kyrylo Kholodenko
2023-09-14 16:07:24 +03:00
committed by GitHub
parent fb28693854
commit 1888993113
54 changed files with 2202 additions and 21 deletions

View 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);
});
});

View 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;
}
}
}

View 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);

View 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;

View File

@@ -4,3 +4,4 @@
@import "./section-sub-header/SectionSubHeader";
@import "./processing-notification/ProccessingNotification";
@import "./WysiwygEditor";
@import "./course-stepper/CouseStepper";