feat: add functionality to see unit draft preview (#1501)

* feat: add functionality to see unit draft preview

* feat: add tests for course link redirects

* fix: course redirect unit to sequnce unit redirect

* fix: test coverage
This commit is contained in:
Kristin Aoki
2024-10-28 10:31:17 -04:00
committed by GitHub
parent 6f1159617e
commit d47433ee83
17 changed files with 1196 additions and 173 deletions

View File

@@ -1,15 +1,8 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { breakpoints, Button, useWindowSize } from '@openedx/paragon';
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
import { breakpoints, useWindowSize } from '@openedx/paragon';
import classNames from 'classnames';
import {
injectIntl,
intlShape,
isRtl,
getLocale,
} from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { useSelector } from 'react-redux';
@@ -21,9 +14,10 @@ import { useSequenceNavigationMetadata } from './hooks';
import { useModel } from '../../../../generic/model-store';
import messages from './messages';
import PreviousButton from './generic/PreviousButton';
import NextButton from './generic/NextButton';
const SequenceNavigation = ({
intl,
unitId,
sequenceId,
className,
@@ -36,6 +30,7 @@ const SequenceNavigation = ({
open,
close,
}) => {
const intl = useIntl();
const sequence = useModel('sequences', sequenceId);
const {
isFirstUnit,
@@ -76,29 +71,21 @@ const SequenceNavigation = ({
);
};
const renderPreviousButton = () => {
const disabled = isFirstUnit;
const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft;
return navigationDisabledPrevSequence || (
<Button
variant="link"
className="previous-btn"
onClick={previousHandler}
disabled={disabled}
iconBefore={prevArrow}
as={disabled ? undefined : Link}
to={disabled ? undefined : previousLink}
>
{shouldDisplayNotificationTriggerInSequence ? null : intl.formatMessage(messages.previousButton)}
</Button>
);
};
const renderPreviousButton = () => navigationDisabledPrevSequence || (
<PreviousButton
variant="link"
buttonStyle="previous-btn"
onClick={previousHandler}
previousLink={previousLink}
isFirstUnit={isFirstUnit}
buttonLabel={shouldDisplayNotificationTriggerInSequence ? null : intl.formatMessage(messages.previousButton)}
/>
);
const renderNextButton = () => {
const { exitActive, exitText } = GetCourseExitNavigation(courseId, intl);
const buttonText = (isLastUnit && exitText) ? exitText : intl.formatMessage(messages.nextButton);
const disabled = isLastUnit && !exitActive;
const nextArrow = isRtl(getLocale()) ? ChevronLeft : ChevronRight;
return navigationDisabledNextSequence || (
<PluginSlot
@@ -106,10 +93,8 @@ const SequenceNavigation = ({
pluginProps={{
courseId,
disabled,
buttonText,
nextArrow,
buttonText: shouldDisplayNotificationTriggerInSequence ? null : buttonText,
nextLink,
shouldDisplayNotificationTriggerInSequence,
sequenceId,
unitId,
nextSequenceHandler,
@@ -117,20 +102,16 @@ const SequenceNavigation = ({
isOpen,
open,
close,
linkComponent: Link,
}}
>
<Button
<NextButton
variant="link"
className="next-btn"
buttonStyle="next-btn"
onClick={nextHandler}
nextLink={nextLink}
disabled={disabled}
iconAfter={nextArrow}
as={disabled ? undefined : Link}
to={disabled ? undefined : nextLink}
>
{shouldDisplayNotificationTriggerInSequence ? null : buttonText}
</Button>
buttonLabel={shouldDisplayNotificationTriggerInSequence ? null : buttonText}
/>
</PluginSlot>
);
};
@@ -145,7 +126,6 @@ const SequenceNavigation = ({
};
SequenceNavigation.propTypes = {
intl: intlShape.isRequired,
sequenceId: PropTypes.string.isRequired,
unitId: PropTypes.string,
className: PropTypes.string,
@@ -169,4 +149,4 @@ SequenceNavigation.defaultProps = {
nextSequenceHandler: null,
};
export default injectIntl(SequenceNavigation);
export default SequenceNavigation;

View File

@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect, useSelector } from 'react-redux';
import classNames from 'classnames';
@@ -22,6 +22,9 @@ const UnitButton = ({
showTitle,
}) => {
const { courseId, sequenceId } = useSelector(state => state.courseware);
const { pathname } = useLocation();
const basePath = `/course/${courseId}/${sequenceId}/${unitId}`;
const unitPath = pathname.startsWith('/preview') ? `/preview${basePath}` : basePath;
const handleClick = useCallback(() => {
onClick(unitId);
@@ -37,7 +40,7 @@ const UnitButton = ({
onClick={handleClick}
title={title}
as={Link}
to={`/course/${courseId}/${sequenceId}/${unitId}`}
to={unitPath}
>
<UnitIcon type={contentType} />
{showTitle && <span className="unit-title">{title}</span>}

View File

@@ -1,70 +1,53 @@
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { Button } from '@openedx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronLeft, faChevronRight } from '@fortawesome/free-solid-svg-icons';
import {
injectIntl, intlShape, isRtl, getLocale,
} from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux';
import { GetCourseExitNavigation } from '../../course-exit';
import UnitNavigationEffortEstimate from './UnitNavigationEffortEstimate';
import { useSequenceNavigationMetadata } from './hooks';
import messages from './messages';
import PreviousButton from './generic/PreviousButton';
import NextButton from './generic/NextButton';
const UnitNavigation = ({
intl,
sequenceId,
unitId,
onClickPrevious,
onClickNext,
isAtTop,
}) => {
const intl = useIntl();
const {
isFirstUnit, isLastUnit, nextLink, previousLink,
} = useSequenceNavigationMetadata(sequenceId, unitId);
const { courseId } = useSelector(state => state.courseware);
const renderPreviousButton = () => {
const disabled = isFirstUnit;
const prevArrow = isRtl(getLocale()) ? faChevronRight : faChevronLeft;
return (
<Button
variant="outline-secondary"
className="previous-button mr-sm-2 d-flex align-items-center justify-content-center"
disabled={disabled}
onClick={onClickPrevious}
as={disabled ? undefined : Link}
to={disabled ? undefined : previousLink}
>
<FontAwesomeIcon icon={prevArrow} className="mr-2" size="sm" />
{intl.formatMessage(messages.previousButton)}
</Button>
);
};
const renderPreviousButton = () => (
<PreviousButton
isFirstUnit={isFirstUnit}
variant="outline-secondary"
buttonLabel={intl.formatMessage(messages.previousButton)}
buttonStyle="previous-button justify-content-center"
onClick={onClickPrevious}
previousLink={previousLink}
/>
);
const renderNextButton = () => {
const { exitActive, exitText } = GetCourseExitNavigation(courseId, intl);
const buttonText = (isLastUnit && exitText) ? exitText : intl.formatMessage(messages.nextButton);
const disabled = isLastUnit && !exitActive;
const nextArrow = isRtl(getLocale()) ? faChevronLeft : faChevronRight;
return (
<Button
<NextButton
variant="outline-primary"
className="next-button d-flex align-items-center justify-content-center"
buttonStyle="next-button justify-content-center"
onClick={onClickNext}
disabled={disabled}
as={disabled ? undefined : Link}
to={disabled ? undefined : nextLink}
>
<UnitNavigationEffortEstimate sequenceId={sequenceId} unitId={unitId}>
{buttonText}
</UnitNavigationEffortEstimate>
<FontAwesomeIcon icon={nextArrow} className="ml-2" size="sm" />
</Button>
buttonLabel={buttonText}
nextLink={nextLink}
hasEffortEstimate
/>
);
};
@@ -77,7 +60,6 @@ const UnitNavigation = ({
};
UnitNavigation.propTypes = {
intl: intlShape.isRequired,
sequenceId: PropTypes.string.isRequired,
unitId: PropTypes.string,
onClickPrevious: PropTypes.func.isRequired,
@@ -90,4 +72,4 @@ UnitNavigation.defaultProps = {
isAtTop: false,
};
export default injectIntl(UnitNavigation);
export default UnitNavigation;

View File

@@ -0,0 +1,56 @@
import PropTypes from 'prop-types';
import { Link, useLocation } from 'react-router-dom';
import { Button } from '@openedx/paragon';
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
import { isRtl, getLocale } from '@edx/frontend-platform/i18n';
import UnitNavigationEffortEstimate from '../UnitNavigationEffortEstimate';
const NextButton = ({
onClick,
buttonLabel,
nextLink,
variant,
buttonStyle,
disabled,
hasEffortEstimate,
}) => {
const nextArrow = isRtl(getLocale()) ? ChevronLeft : ChevronRight;
const { pathname } = useLocation();
const navLink = pathname.startsWith('/preview') ? `/preview${nextLink}` : nextLink;
const buttonContent = hasEffortEstimate ? (
<UnitNavigationEffortEstimate>
{buttonLabel}
</UnitNavigationEffortEstimate>
) : buttonLabel;
return (
<Button
variant={variant}
className={buttonStyle}
disabled={disabled}
onClick={onClick}
as={disabled ? undefined : Link}
to={disabled ? undefined : navLink}
iconAfter={nextArrow}
>
{buttonContent}
</Button>
);
};
NextButton.defaultProps = {
hasEffortEstimate: false,
};
NextButton.propTypes = {
onClick: PropTypes.func.isRequired,
buttonLabel: PropTypes.string.isRequired,
nextLink: PropTypes.string.isRequired,
variant: PropTypes.string.isRequired,
buttonStyle: PropTypes.string.isRequired,
disabled: PropTypes.bool.isRequired,
hasEffortEstimate: PropTypes.bool,
};
export default NextButton;

View File

@@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import { Link, useLocation } from 'react-router-dom';
import { Button } from '@openedx/paragon';
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
import { isRtl, getLocale } from '@edx/frontend-platform/i18n';
const PreviousButton = ({
onClick,
buttonLabel,
previousLink,
variant,
buttonStyle,
isFirstUnit,
}) => {
const disabled = isFirstUnit;
const prevArrow = isRtl(getLocale()) ? ChevronRight : ChevronLeft;
const { pathname } = useLocation();
const navLink = pathname.startsWith('/preview') ? `/preview${previousLink}` : previousLink;
return (
<Button
variant={variant}
className={buttonStyle}
disabled={disabled}
onClick={onClick}
as={disabled ? undefined : Link}
to={disabled ? undefined : navLink}
iconBefore={prevArrow}
>
{buttonLabel}
</Button>
);
};
PreviousButton.propTypes = {
onClick: PropTypes.func.isRequired,
buttonLabel: PropTypes.string.isRequired,
previousLink: PropTypes.string.isRequired,
variant: PropTypes.string.isRequired,
buttonStyle: PropTypes.string.isRequired,
isFirstUnit: PropTypes.bool.isRequired,
};
export default PreviousButton;