feat: xblock status component
feat: add custom relative dates flag to state refactor: add gated status type refactor: alert style feat: add status text to units test: add tests fix: lint issues refactor: break up xblock status component fix: selector for isCustomRelativeDatesActive fix: prereq default value
This commit is contained in:
committed by
Kristin Aoki
parent
9c52b8b6c5
commit
2cb907e731
@@ -53,6 +53,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
statusBarData,
|
||||
courseActions,
|
||||
sectionsList,
|
||||
isCustomRelativeDatesActive,
|
||||
isLoading,
|
||||
isReIndexShow,
|
||||
showErrorAlert,
|
||||
@@ -309,6 +310,8 @@ const CourseOutline = ({ courseId }) => {
|
||||
section={section}
|
||||
index={sectionIndex}
|
||||
canMoveItem={canMoveItem(sections)}
|
||||
isSelfPaced={statusBarData.isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
savingStatus={savingStatus}
|
||||
onOpenHighlightsModal={handleOpenHighlightsModal}
|
||||
onOpenPublishModal={openPublishModal}
|
||||
@@ -332,6 +335,8 @@ const CourseOutline = ({ courseId }) => {
|
||||
subsection={subsection}
|
||||
index={subsectionIndex}
|
||||
canMoveItem={canMoveItem(section.childInfo.children)}
|
||||
isSelfPaced={statusBarData.isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
savingStatus={savingStatus}
|
||||
onOpenPublishModal={openPublishModal}
|
||||
onOpenDeleteModal={openDeleteModal}
|
||||
@@ -356,6 +361,8 @@ const CourseOutline = ({ courseId }) => {
|
||||
unit={unit}
|
||||
subsection={subsection}
|
||||
section={section}
|
||||
isSelfPaced={statusBarData.isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
index={unitIndex}
|
||||
canMoveItem={canMoveItem(subsection.childInfo.children)}
|
||||
savingStatus={savingStatus}
|
||||
|
||||
@@ -9,3 +9,4 @@
|
||||
@import "./publish-modal/PublishModal";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
@import "./drag-helper/ConditionalSortableElement";
|
||||
@import "./xblock-status/XBlockStatus";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.item-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: -.5rem;
|
||||
|
||||
.item-card-header__title-btn {
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -9,6 +9,10 @@ const messages = defineMessages({
|
||||
id: 'course-authoring.course-outline.card.status-badge.live',
|
||||
defaultMessage: 'Live',
|
||||
},
|
||||
statusBadgeGated: {
|
||||
id: 'course-authoring.course-outline.card.status-badge.gated',
|
||||
defaultMessage: 'Gated',
|
||||
},
|
||||
statusBadgePublishedNotLive: {
|
||||
id: 'course-authoring.course-outline.card.status-badge.published-not-live',
|
||||
defaultMessage: 'Published not live',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const ITEM_BADGE_STATUS = /** @type {const} */ ({
|
||||
live: 'live',
|
||||
gated: 'gated',
|
||||
publishedNotLive: 'published_not_live',
|
||||
unpublishedChanges: 'unpublished_changes',
|
||||
staffOnly: 'staff_only',
|
||||
|
||||
@@ -7,3 +7,4 @@ export const getCurrentItem = (state) => state.courseOutline.currentItem;
|
||||
export const getCurrentSection = (state) => state.courseOutline.currentSection;
|
||||
export const getCurrentSubsection = (state) => state.courseOutline.currentSubsection;
|
||||
export const getCourseActions = (state) => state.courseOutline.actions;
|
||||
export const getCustomRelativeDatesActiveFlag = (state) => state.courseOutline.isCustomRelativeDatesActive;
|
||||
|
||||
@@ -28,6 +28,7 @@ const slice = createSlice({
|
||||
videoSharingOptions: VIDEO_SHARING_OPTIONS.perVideo,
|
||||
},
|
||||
sectionsList: [],
|
||||
isCustomRelativeDatesActive: false,
|
||||
currentSection: {},
|
||||
currentSubsection: {},
|
||||
currentItem: {},
|
||||
@@ -42,6 +43,7 @@ const slice = createSlice({
|
||||
fetchOutlineIndexSuccess: (state, { payload }) => {
|
||||
state.outlineIndexData = payload;
|
||||
state.sectionsList = payload.courseStructure?.childInfo?.children || [];
|
||||
state.isCustomRelativeDatesActive = payload.isCustomRelativeDatesActive;
|
||||
},
|
||||
updateOutlineIndexLoadingStatus: (state, { payload }) => {
|
||||
state.loadingStatus = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Row } from '@edx/paragon';
|
||||
import { Col, Row } from '@edx/paragon';
|
||||
import { SortableItem } from '@edx/frontend-lib-content-components';
|
||||
|
||||
const ConditionalSortableElement = ({
|
||||
@@ -24,9 +24,9 @@ const ConditionalSortableElement = ({
|
||||
id={id}
|
||||
componentStyle={style}
|
||||
>
|
||||
<div className="extend-margin">
|
||||
<Col className="extend-margin px-0">
|
||||
{children}
|
||||
</div>
|
||||
</Col>
|
||||
</SortableItem>
|
||||
);
|
||||
}
|
||||
@@ -36,7 +36,9 @@ const ConditionalSortableElement = ({
|
||||
style={style}
|
||||
className="mx-0"
|
||||
>
|
||||
{children}
|
||||
<Col className="px-0">
|
||||
{children}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
.extend-margin {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
.item-children {
|
||||
margin-right: -2.75rem;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
getCurrentItem,
|
||||
getCurrentSection,
|
||||
getCurrentSubsection,
|
||||
getCustomRelativeDatesActiveFlag,
|
||||
} from './data/selectors';
|
||||
import {
|
||||
addNewSectionQuery,
|
||||
@@ -62,6 +63,7 @@ const useCourseOutline = ({ courseId }) => {
|
||||
const currentItem = useSelector(getCurrentItem);
|
||||
const currentSection = useSelector(getCurrentSection);
|
||||
const currentSubsection = useSelector(getCurrentSubsection);
|
||||
const isCustomRelativeDatesActive = useSelector(getCustomRelativeDatesActiveFlag);
|
||||
|
||||
const [isEnableHighlightsModalOpen, openEnableHighlightsModal, closeEnableHighlightsModal] = useToggle(false);
|
||||
const [isSectionsExpanded, setSectionsExpanded] = useState(true);
|
||||
@@ -242,6 +244,7 @@ const useCourseOutline = ({ courseId }) => {
|
||||
courseActions,
|
||||
savingStatus,
|
||||
sectionsList,
|
||||
isCustomRelativeDatesActive,
|
||||
isLoading: outlineIndexLoadingStatus === RequestStatus.IN_PROGRESS,
|
||||
isReIndexShow: Boolean(reindexLink),
|
||||
showSuccessAlert,
|
||||
|
||||
@@ -14,11 +14,14 @@ import CardHeader from '../card-header/CardHeader';
|
||||
import BaseTitleWithStatusBadge from '../card-header/BaseTitleWithStatusBadge';
|
||||
import ConditionalSortableElement from '../drag-helper/ConditionalSortableElement';
|
||||
import TitleButton from '../card-header/TitleButton';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
import messages from './messages';
|
||||
|
||||
const SectionCard = ({
|
||||
section,
|
||||
isSelfPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
children,
|
||||
index,
|
||||
canMoveItem,
|
||||
@@ -60,7 +63,6 @@ const SectionCard = ({
|
||||
highlights,
|
||||
actions: sectionActions,
|
||||
isHeaderVisible = true,
|
||||
explanatoryMessage = '',
|
||||
} = section;
|
||||
|
||||
// re-create actions object for customizations
|
||||
@@ -174,18 +176,24 @@ const SectionCard = ({
|
||||
/>
|
||||
)}
|
||||
<div className="section-card__content" data-testid="section-card__content">
|
||||
{explanatoryMessage && <p className="text-secondary-400 x-small mb-1">{explanatoryMessage}</p>}
|
||||
<div className="outline-section__status">
|
||||
<div className="outline-section__status mb-1">
|
||||
<Button
|
||||
className="section-card__highlights"
|
||||
className="p-0 bg-transparent"
|
||||
data-destid="section-card-highlights-button"
|
||||
variant="tertiary"
|
||||
onClick={handleOpenHighlightsModal}
|
||||
>
|
||||
<Badge className="highlights-badge">{highlights.length}</Badge>
|
||||
<Badge className="mr-1 d-flex justify-content-center align-items-center highlights-badge">
|
||||
{highlights.length}
|
||||
</Badge>
|
||||
<p className="m-0 text-black">{messages.sectionHighlightsBadge.defaultMessage}</p>
|
||||
</Button>
|
||||
</div>
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={section}
|
||||
/>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div
|
||||
@@ -226,7 +234,6 @@ SectionCard.propTypes = {
|
||||
visibilityState: PropTypes.string.isRequired,
|
||||
highlights: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
shouldScroll: PropTypes.bool,
|
||||
explanatoryMessage: PropTypes.string,
|
||||
actions: PropTypes.shape({
|
||||
deletable: PropTypes.bool.isRequired,
|
||||
draggable: PropTypes.bool.isRequired,
|
||||
@@ -235,6 +242,8 @@ SectionCard.propTypes = {
|
||||
}).isRequired,
|
||||
isHeaderVisible: PropTypes.bool,
|
||||
}).isRequired,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node,
|
||||
onOpenHighlightsModal: PropTypes.func.isRequired,
|
||||
onOpenPublishModal: PropTypes.func.isRequired,
|
||||
|
||||
@@ -13,26 +13,15 @@
|
||||
color: $headings-color;
|
||||
}
|
||||
|
||||
.section-card__highlights {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
.highlights-badge {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 1.375rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.highlights-badge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 1.375rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.section-card__content {
|
||||
margin-left: 1.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,15 @@ import CardHeader from '../card-header/CardHeader';
|
||||
import BaseTitleWithStatusBadge from '../card-header/BaseTitleWithStatusBadge';
|
||||
import ConditionalSortableElement from '../drag-helper/ConditionalSortableElement';
|
||||
import TitleButton from '../card-header/TitleButton';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
import messages from './messages';
|
||||
|
||||
const SubsectionCard = ({
|
||||
section,
|
||||
subsection,
|
||||
isSelfPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
children,
|
||||
index,
|
||||
canMoveItem,
|
||||
@@ -140,26 +143,35 @@ const SubsectionCard = ({
|
||||
>
|
||||
<div className="subsection-card" data-testid="subsection-card" ref={currentRef}>
|
||||
{isHeaderVisible && (
|
||||
<CardHeader
|
||||
title={displayName}
|
||||
status={subsectionStatus}
|
||||
hasChanges={hasChanges}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleSubsectionMoveUp}
|
||||
onClickMoveDown={handleSubsectionMoveDown}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
/>
|
||||
<>
|
||||
<CardHeader
|
||||
title={displayName}
|
||||
status={subsectionStatus}
|
||||
hasChanges={hasChanges}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
onClickMoveUp={handleSubsectionMoveUp}
|
||||
onClickMoveDown={handleSubsectionMoveDown}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
isFormOpen={isFormOpen}
|
||||
closeForm={closeForm}
|
||||
onEditSubmit={handleEditSubmit}
|
||||
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
|
||||
onClickDuplicate={onDuplicateSubmit}
|
||||
titleComponent={titleComponent}
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
/>
|
||||
<div className="subsection-card__content item-children" data-testid="subsection-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={subsection}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isExpanded && (
|
||||
<div
|
||||
@@ -215,6 +227,8 @@ SubsectionCard.propTypes = {
|
||||
isHeaderVisible: PropTypes.bool,
|
||||
}).isRequired,
|
||||
children: PropTypes.node,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
||||
onOpenPublishModal: PropTypes.func.isRequired,
|
||||
onEditSubmit: PropTypes.func.isRequired,
|
||||
savingStatus: PropTypes.string.isRequired,
|
||||
|
||||
@@ -16,4 +16,8 @@
|
||||
line-height: $headings-line-height;
|
||||
color: $headings-color;
|
||||
}
|
||||
|
||||
.subsection-card__content {
|
||||
margin-left: 1.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,15 @@ import CardHeader from '../card-header/CardHeader';
|
||||
import BaseTitleWithStatusBadge from '../card-header/BaseTitleWithStatusBadge';
|
||||
import ConditionalSortableElement from '../drag-helper/ConditionalSortableElement';
|
||||
import TitleLink from '../card-header/TitleLink';
|
||||
import XBlockStatus from '../xblock-status/XBlockStatus';
|
||||
import { getItemStatus, getItemStatusBorder, scrollToElement } from '../utils';
|
||||
|
||||
const UnitCard = ({
|
||||
unit,
|
||||
subsection,
|
||||
section,
|
||||
isSelfPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
index,
|
||||
canMoveItem,
|
||||
onOpenPublishModal,
|
||||
@@ -146,6 +149,13 @@ const UnitCard = ({
|
||||
namePrefix={namePrefix}
|
||||
actions={actions}
|
||||
/>
|
||||
<div className="unit-card__content item-children" data-testid="unit-card__content">
|
||||
<XBlockStatus
|
||||
isSelfPaced={isSelfPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
blockData={unit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ConditionalSortableElement>
|
||||
);
|
||||
@@ -193,6 +203,8 @@ UnitCard.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
canMoveItem: PropTypes.func.isRequired,
|
||||
onOrderChange: PropTypes.func.isRequired,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default UnitCard;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
.unit-card {
|
||||
flex-grow: 1;
|
||||
|
||||
.unit-card__content {
|
||||
margin: $spacer;
|
||||
}
|
||||
|
||||
.item-card-header__badge-status {
|
||||
background: $light-100;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ const getItemStatus = ({
|
||||
switch (true) {
|
||||
case visibilityState === VisibilityTypes.STAFF_ONLY:
|
||||
return ITEM_BADGE_STATUS.staffOnly;
|
||||
case visibilityState === VisibilityTypes.GATED:
|
||||
return ITEM_BADGE_STATUS.gated;
|
||||
case visibilityState === VisibilityTypes.LIVE:
|
||||
return ITEM_BADGE_STATUS.live;
|
||||
case published && !hasChanges:
|
||||
@@ -42,6 +44,11 @@ const getItemStatus = ({
|
||||
*/
|
||||
const getItemStatusBadgeContent = (status, messages, intl) => {
|
||||
switch (status) {
|
||||
case ITEM_BADGE_STATUS.gated:
|
||||
return {
|
||||
badgeTitle: intl.formatMessage(messages.statusBadgeGated),
|
||||
badgeIcon: LockIcon,
|
||||
};
|
||||
case ITEM_BADGE_STATUS.live:
|
||||
return {
|
||||
badgeTitle: intl.formatMessage(messages.statusBadgeLive),
|
||||
@@ -92,6 +99,10 @@ const getItemStatusBorder = (status) => {
|
||||
return {
|
||||
borderLeft: '5px solid #0D7D4D',
|
||||
};
|
||||
case ITEM_BADGE_STATUS.gated:
|
||||
return {
|
||||
borderLeft: '5px solid #000000',
|
||||
};
|
||||
case ITEM_BADGE_STATUS.staffOnly:
|
||||
return {
|
||||
borderLeft: '5px solid #000000',
|
||||
|
||||
48
src/course-outline/xblock-status/GradingPolicyAlert.jsx
Normal file
48
src/course-outline/xblock-status/GradingPolicyAlert.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import {
|
||||
WarningFilled as WarningIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const GradingPolicyAlert = ({
|
||||
graded,
|
||||
gradingType,
|
||||
courseGraders,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
let gradingPolicyMismatch = false;
|
||||
if (graded) {
|
||||
if (gradingType) {
|
||||
gradingPolicyMismatch = (
|
||||
courseGraders.filter((cg) => cg.toLowerCase() === gradingType.toLowerCase())
|
||||
).length === 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (gradingPolicyMismatch) {
|
||||
return (
|
||||
<Alert className="mt-2 grading-mismatch-alert" variant="warning" icon={WarningIcon}>
|
||||
{intl.formatMessage(messages.gradingPolicyMismatchText, { gradingType })}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
GradingPolicyAlert.defaultProps = {
|
||||
graded: false,
|
||||
gradingType: '',
|
||||
};
|
||||
|
||||
GradingPolicyAlert.propTypes = {
|
||||
graded: PropTypes.bool,
|
||||
gradingType: PropTypes.string,
|
||||
courseGraders: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
|
||||
};
|
||||
|
||||
export default GradingPolicyAlert;
|
||||
134
src/course-outline/xblock-status/GradingTypeAndDueDate.jsx
Normal file
134
src/course-outline/xblock-status/GradingTypeAndDueDate.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import {
|
||||
Check as CheckIcon,
|
||||
CalendarMonth as CalendarIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const GradingTypeAndDueDate = ({
|
||||
isSelfPaced,
|
||||
isInstructorPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
isTimeLimited,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
graded,
|
||||
gradingType,
|
||||
dueDate,
|
||||
relativeWeeksDue,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const showRelativeWeeks = isSelfPaced && isCustomRelativeDatesActive && relativeWeeksDue;
|
||||
|
||||
let examValue = '';
|
||||
if (isProctoredExam) {
|
||||
if (isOnboardingExam) {
|
||||
examValue = messages.onboardingExam;
|
||||
} else if (isPracticeExam) {
|
||||
examValue = messages.practiceProctoredExam;
|
||||
} else {
|
||||
examValue = messages.proctoredExam;
|
||||
}
|
||||
} else {
|
||||
examValue = messages.timedExam;
|
||||
}
|
||||
|
||||
const gradingTypeDiv = () => (
|
||||
<div className="d-flex align-items-center mr-1" data-testid="grading-type-div">
|
||||
<span className="sr-only status-grading-label">
|
||||
{intl.formatMessage(messages.gradedAsScreenReaderLabel)}
|
||||
</span>
|
||||
<Icon className="mr-1" size="sm" src={CheckIcon} />
|
||||
<span className="status-grading-value">
|
||||
{gradingType || intl.formatMessage(messages.ungradedText)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const dueDateDiv = () => {
|
||||
if (dueDate && isInstructorPaced) {
|
||||
return (
|
||||
<div className="status-grading-date" data-testid="due-date-div">
|
||||
{intl.formatMessage(messages.dueLabel)} {dueDate}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const selfPacedRelativeDueWeeksDiv = () => (
|
||||
<div className="d-flex align-items-center" data-testid="self-paced-relative-due-weeks-div">
|
||||
<Icon className="mr-1" size="sm" src={CalendarIcon} />
|
||||
<span className="status-custom-grading-date">
|
||||
{intl.formatMessage(messages.customDueDateLabel, { relativeWeeksDue })}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isTimeLimited) {
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex align-items-center">
|
||||
{gradingTypeDiv()} -
|
||||
<span className="sr-only">{intl.formatMessage(examValue)}</span>
|
||||
<span className="mx-2" data-testid="exam-value-span">
|
||||
{intl.formatMessage(examValue)}
|
||||
</span>
|
||||
{dueDateDiv()}
|
||||
</div>
|
||||
{showRelativeWeeks && (selfPacedRelativeDueWeeksDiv())}
|
||||
</>
|
||||
);
|
||||
} if ((dueDate && !isSelfPaced) || graded) {
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex align-items-center">
|
||||
{gradingTypeDiv()}
|
||||
{dueDateDiv()}
|
||||
</div>
|
||||
{showRelativeWeeks && (selfPacedRelativeDueWeeksDiv())}
|
||||
</>
|
||||
);
|
||||
} if (showRelativeWeeks) {
|
||||
return (
|
||||
<>
|
||||
{gradingTypeDiv()}
|
||||
{selfPacedRelativeDueWeeksDiv()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
GradingTypeAndDueDate.defaultProps = {
|
||||
isCustomRelativeDatesActive: false,
|
||||
isTimeLimited: false,
|
||||
isProctoredExam: false,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: false,
|
||||
graded: false,
|
||||
gradingType: '',
|
||||
dueDate: '',
|
||||
relativeWeeksDue: null,
|
||||
};
|
||||
|
||||
GradingTypeAndDueDate.propTypes = {
|
||||
isInstructorPaced: PropTypes.bool.isRequired,
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool,
|
||||
isTimeLimited: PropTypes.bool,
|
||||
isProctoredExam: PropTypes.bool,
|
||||
isOnboardingExam: PropTypes.bool,
|
||||
isPracticeExam: PropTypes.bool,
|
||||
graded: PropTypes.bool,
|
||||
gradingType: PropTypes.string,
|
||||
dueDate: PropTypes.string,
|
||||
relativeWeeksDue: PropTypes.number,
|
||||
};
|
||||
|
||||
export default GradingTypeAndDueDate;
|
||||
29
src/course-outline/xblock-status/HideAfterDueMessage.jsx
Normal file
29
src/course-outline/xblock-status/HideAfterDueMessage.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import {
|
||||
VisibilityOff as HideIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const HideAfterDueMessage = ({ isSelfPaced }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className="d-flex align-items-center" data-testid="hide-after-due-message">
|
||||
<Icon className="mr-1" size="sm" src={HideIcon} />
|
||||
<span className="status-hide-after-due-value">
|
||||
{isSelfPaced
|
||||
? intl.formatMessage(messages.hiddenAfterEndDate)
|
||||
: intl.formatMessage(messages.hiddenAfterDueDate)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
HideAfterDueMessage.propTypes = {
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default HideAfterDueMessage;
|
||||
65
src/course-outline/xblock-status/ReleaseStatus.jsx
Normal file
65
src/course-outline/xblock-status/ReleaseStatus.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import {
|
||||
AccessTime as ClockIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ReleaseStatus = ({
|
||||
isInstructorPaced,
|
||||
explanatoryMessage,
|
||||
releaseDate,
|
||||
releasedToStudents,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const explanatoryMessageDiv = () => (
|
||||
<span data-testid="explanatory-message-span">
|
||||
{explanatoryMessage}
|
||||
</span>
|
||||
);
|
||||
|
||||
let releaseLabel = messages.unscheduledLabel;
|
||||
if (releasedToStudents) {
|
||||
releaseLabel = messages.releasedLabel;
|
||||
} else if (releaseDate) {
|
||||
releaseLabel = messages.scheduledLabel;
|
||||
}
|
||||
|
||||
const releaseStatusDiv = () => (
|
||||
<div className="d-flex align-items-center" data-testid="release-status-div">
|
||||
<span className="sr-only status-release-label">
|
||||
{intl.formatMessage(messages.releaseStatusScreenReaderTitle)}
|
||||
</span>
|
||||
<Icon className="mr-1" size="sm" src={ClockIcon} />
|
||||
{intl.formatMessage(releaseLabel)}
|
||||
{releaseDate && releaseDate}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (explanatoryMessage) {
|
||||
return explanatoryMessageDiv();
|
||||
}
|
||||
|
||||
if (isInstructorPaced) {
|
||||
return releaseStatusDiv();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ReleaseStatus.defaultProps = {
|
||||
explanatoryMessage: '',
|
||||
};
|
||||
|
||||
ReleaseStatus.propTypes = {
|
||||
isInstructorPaced: PropTypes.bool.isRequired,
|
||||
explanatoryMessage: PropTypes.string,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
releasedToStudents: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default ReleaseStatus;
|
||||
88
src/course-outline/xblock-status/StatusMessages.jsx
Normal file
88
src/course-outline/xblock-status/StatusMessages.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import {
|
||||
Lock as LockIcon,
|
||||
Groups as GroupsIcon,
|
||||
} from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const StatusMessages = ({
|
||||
isVertical,
|
||||
staffOnlyMessage,
|
||||
prereq,
|
||||
prereqs,
|
||||
userPartitionInfo,
|
||||
hasPartitionGroupComponents,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const statusMessages = [];
|
||||
|
||||
if (prereq) {
|
||||
let prereqDisplayName = '';
|
||||
prereqs.forEach((block) => {
|
||||
if (block.blockUsageKey === prereq) {
|
||||
prereqDisplayName = block.blockDisplayName;
|
||||
}
|
||||
});
|
||||
statusMessages.push({
|
||||
icon: LockIcon,
|
||||
text: intl.formatMessage(messages.prerequisiteLabel, { prereqDisplayName }),
|
||||
});
|
||||
}
|
||||
|
||||
if (!staffOnlyMessage && isVertical) {
|
||||
const { selectedPartitionIndex, selectedGroupsLabel } = userPartitionInfo;
|
||||
if (selectedPartitionIndex !== -1 && !Number.isNaN(selectedPartitionIndex)) {
|
||||
statusMessages.push({
|
||||
icon: GroupsIcon,
|
||||
text: intl.formatMessage(messages.restrictedUnitAccess, { selectedGroupsLabel }),
|
||||
});
|
||||
} else if (hasPartitionGroupComponents) {
|
||||
statusMessages.push({
|
||||
icon: GroupsIcon,
|
||||
text: intl.formatMessage(messages.restrictedUnitAccessToSomeContent),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (statusMessages.length > 0) {
|
||||
return (
|
||||
<div className="border-top border-light mt-2 text-dark" data-testid="status-messages-div">
|
||||
{statusMessages.map(({ icon, text }) => (
|
||||
<div key={text} className="d-flex align-items-center pt-1">
|
||||
<Icon className="mr-1" size="sm" src={icon} />
|
||||
{text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
StatusMessages.defaultProps = {
|
||||
staffOnlyMessage: false,
|
||||
prereq: '',
|
||||
prereqs: [],
|
||||
userPartitionInfo: {},
|
||||
};
|
||||
|
||||
StatusMessages.propTypes = {
|
||||
isVertical: PropTypes.bool.isRequired,
|
||||
staffOnlyMessage: PropTypes.bool,
|
||||
prereq: PropTypes.string,
|
||||
prereqs: PropTypes.arrayOf(PropTypes.shape({
|
||||
blockUsageKey: PropTypes.string.isRequired,
|
||||
blockDisplayName: PropTypes.string.isRequired,
|
||||
})),
|
||||
userPartitionInfo: PropTypes.shape({
|
||||
selectedPartitionIndex: PropTypes.number.isRequired,
|
||||
selectedGroupsLabel: PropTypes.string.isRequired,
|
||||
}),
|
||||
hasPartitionGroupComponents: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default StatusMessages;
|
||||
122
src/course-outline/xblock-status/XBlockStatus.jsx
Normal file
122
src/course-outline/xblock-status/XBlockStatus.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { COURSE_BLOCK_NAMES } from '../constants';
|
||||
import ReleaseStatus from './ReleaseStatus';
|
||||
import GradingPolicyAlert from './GradingPolicyAlert';
|
||||
import GradingTypeAndDueDate from './GradingTypeAndDueDate';
|
||||
import StatusMessages from './StatusMessages';
|
||||
import HideAfterDueMessage from './HideAfterDueMessage';
|
||||
|
||||
const XBlockStatus = ({
|
||||
isSelfPaced,
|
||||
isCustomRelativeDatesActive,
|
||||
blockData,
|
||||
}) => {
|
||||
const {
|
||||
category,
|
||||
explanatoryMessage,
|
||||
releasedToStudents,
|
||||
releaseDate,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
prereq,
|
||||
prereqs,
|
||||
staffOnlyMessage,
|
||||
userPartitionInfo,
|
||||
hasPartitionGroupComponents,
|
||||
format: gradingType,
|
||||
dueDate,
|
||||
relativeWeeksDue,
|
||||
isTimeLimited,
|
||||
graded,
|
||||
courseGraders,
|
||||
hideAfterDue,
|
||||
} = blockData;
|
||||
|
||||
const isInstructorPaced = !isSelfPaced;
|
||||
const isVertical = category === COURSE_BLOCK_NAMES.vertical.id;
|
||||
|
||||
return (
|
||||
<div className="text-secondary-400 x-small mb-1">
|
||||
{!isVertical && (
|
||||
<ReleaseStatus
|
||||
isInstructorPaced={isInstructorPaced}
|
||||
explanatoryMessage={explanatoryMessage}
|
||||
releaseDate={releaseDate}
|
||||
releasedToStudents={releasedToStudents}
|
||||
/>
|
||||
)}
|
||||
{!isVertical && (
|
||||
<GradingTypeAndDueDate
|
||||
isSelfPaced={isSelfPaced}
|
||||
isInstructorPaced={isInstructorPaced}
|
||||
isCustomRelativeDatesActive={isCustomRelativeDatesActive}
|
||||
isTimeLimited={isTimeLimited}
|
||||
isProctoredExam={isProctoredExam}
|
||||
isOnboardingExam={isOnboardingExam}
|
||||
isPracticeExam={isPracticeExam}
|
||||
graded={graded}
|
||||
gradingType={gradingType}
|
||||
dueDate={dueDate}
|
||||
relativeWeeksDue={relativeWeeksDue}
|
||||
/>
|
||||
)}
|
||||
{hideAfterDue && (
|
||||
<HideAfterDueMessage isSelfPaced={isSelfPaced} />
|
||||
)}
|
||||
<StatusMessages
|
||||
isVertical={isVertical}
|
||||
staffOnlyMessage={staffOnlyMessage}
|
||||
prereq={prereq}
|
||||
prereqs={prereqs}
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
hasPartitionGroupComponents={hasPartitionGroupComponents}
|
||||
/>
|
||||
<GradingPolicyAlert
|
||||
graded={graded}
|
||||
gradingType={gradingType}
|
||||
courseGraders={courseGraders}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
XBlockStatus.defaultProps = {
|
||||
isCustomRelativeDatesActive: false,
|
||||
};
|
||||
|
||||
XBlockStatus.propTypes = {
|
||||
isSelfPaced: PropTypes.bool.isRequired,
|
||||
isCustomRelativeDatesActive: PropTypes.bool,
|
||||
blockData: PropTypes.shape({
|
||||
category: PropTypes.string.isRequired,
|
||||
explanatoryMessage: PropTypes.string,
|
||||
releasedToStudents: PropTypes.bool.isRequired,
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
isProctoredExam: PropTypes.bool,
|
||||
isOnboardingExam: PropTypes.bool,
|
||||
isPracticeExam: PropTypes.bool,
|
||||
prereq: PropTypes.string,
|
||||
prereqs: PropTypes.arrayOf(PropTypes.shape({
|
||||
blockUsageKey: PropTypes.string.isRequired,
|
||||
blockDisplayName: PropTypes.string.isRequired,
|
||||
})),
|
||||
staffOnlyMessage: PropTypes.bool,
|
||||
userPartitionInfo: PropTypes.shape({
|
||||
selectedPartitionIndex: PropTypes.number.isRequired,
|
||||
selectedGroupsLabel: PropTypes.string.isRequired,
|
||||
}),
|
||||
hasPartitionGroupComponents: PropTypes.bool.isRequired,
|
||||
format: PropTypes.string,
|
||||
dueDate: PropTypes.string,
|
||||
relativeWeeksDue: PropTypes.number,
|
||||
isTimeLimited: PropTypes.bool,
|
||||
graded: PropTypes.bool,
|
||||
courseGraders: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
|
||||
hideAfterDue: PropTypes.bool,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default XBlockStatus;
|
||||
4
src/course-outline/xblock-status/XBlockStatus.scss
Normal file
4
src/course-outline/xblock-status/XBlockStatus.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.grading-mismatch-alert {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
503
src/course-outline/xblock-status/XBlockStatus.test.jsx
Normal file
503
src/course-outline/xblock-status/XBlockStatus.test.jsx
Normal file
@@ -0,0 +1,503 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import XBlockStatus from './XBlockStatus';
|
||||
import messages from './messages';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
useIntl: () => ({
|
||||
formatMessage: (message) => message.defaultMessage,
|
||||
}),
|
||||
}));
|
||||
|
||||
const section = {
|
||||
id: '123',
|
||||
displayName: 'Section Name',
|
||||
published: true,
|
||||
visibilityState: 'live',
|
||||
hasChanges: false,
|
||||
highlights: ['highlight 1', 'highlight 2'],
|
||||
category: 'chapter',
|
||||
explanatoryMessage: '',
|
||||
releasedToStudents: true,
|
||||
releaseDate: 'Feb 05, 2013 at 01:00 UTC',
|
||||
isProctoredExam: false,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: false,
|
||||
staffOnlyMessage: false,
|
||||
userPartitionInfo: {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
hasPartitionGroupComponents: false,
|
||||
format: 'Homework',
|
||||
dueDate: 'Dec 28, 2023 at 22:00 UTC',
|
||||
isTimeLimited: true,
|
||||
graded: true,
|
||||
courseGraders: ['Homework'],
|
||||
hideAfterDue: true,
|
||||
};
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<XBlockStatus
|
||||
isSelfPaced={false}
|
||||
isCustomRelativeDatesActive={false}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<XBlockStatus /> for Instructor paced Section', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('render XBlockStatus with explanatoryMessage', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...section,
|
||||
explanatoryMessage: 'some explanatory message',
|
||||
},
|
||||
});
|
||||
|
||||
expect(queryByTestId('explanatory-message-span')).toBeInTheDocument();
|
||||
// when explanatory message is displayed, release date should not be visible
|
||||
expect(queryByTestId('release-status-div')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with release status, grading type, due date etc.', () => {
|
||||
const { queryByTestId } = renderComponent({ blockData: section });
|
||||
|
||||
expect(queryByTestId('explanatory-message-span')).not.toBeInTheDocument();
|
||||
// when explanatory message is not displayed, release date should be visible
|
||||
const releaseStatusDiv = queryByTestId('release-status-div');
|
||||
expect(releaseStatusDiv).toBeInTheDocument();
|
||||
expect(releaseStatusDiv).toHaveTextContent(
|
||||
`${messages.releasedLabel.defaultMessage}${section.releaseDate}`,
|
||||
);
|
||||
|
||||
// check grading type
|
||||
const gradingTypeDiv = queryByTestId('grading-type-div');
|
||||
expect(gradingTypeDiv).toBeInTheDocument();
|
||||
expect(gradingTypeDiv).toHaveTextContent(section.format);
|
||||
// check exam value label
|
||||
const examValue = queryByTestId('exam-value-span');
|
||||
expect(examValue).toBeInTheDocument();
|
||||
expect(examValue).toHaveTextContent(messages.timedExam.defaultMessage);
|
||||
// check due date div
|
||||
const dueDateDiv = queryByTestId('due-date-div');
|
||||
expect(dueDateDiv).toBeInTheDocument();
|
||||
expect(dueDateDiv).toHaveTextContent(
|
||||
`${messages.dueLabel.defaultMessage} ${section.dueDate}`,
|
||||
);
|
||||
// self paced weeks should not be visible as
|
||||
// isSelfPaced is false as well as isCustomRelativeDatesActive is false
|
||||
expect(queryByTestId('self-paced-relative-due-weeks-div')).not.toBeInTheDocument();
|
||||
|
||||
// check hide after due date message
|
||||
const hideAfterDueMessage = queryByTestId('hide-after-due-message');
|
||||
expect(hideAfterDueMessage).toBeInTheDocument();
|
||||
expect(hideAfterDueMessage).toHaveTextContent(messages.hiddenAfterDueDate.defaultMessage);
|
||||
|
||||
// check status messages
|
||||
const statusDiv = queryByTestId('status-messages-div');
|
||||
expect(statusDiv).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<XBlockStatus /> for self paced Section', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with grading type, due weeks etc.', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
isSelfPaced: true,
|
||||
isCustomRelativeDatesActive: true,
|
||||
blockData: {
|
||||
...section,
|
||||
relativeWeeksDue: 2,
|
||||
},
|
||||
});
|
||||
|
||||
// both explanatoryMessage & releaseStatusDiv should not be visible
|
||||
expect(queryByTestId('explanatory-message-span')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('release-status-div')).not.toBeInTheDocument();
|
||||
|
||||
// check grading type
|
||||
const gradingTypeDiv = queryByTestId('grading-type-div');
|
||||
expect(gradingTypeDiv).toBeInTheDocument();
|
||||
expect(gradingTypeDiv).toHaveTextContent(section.format);
|
||||
// due date should not be visible for self paced courses.
|
||||
expect(queryByTestId('due-date-div')).not.toBeInTheDocument();
|
||||
// check selfPacedRelativeDueWeeksDiv
|
||||
const selfPacedRelativeDueWeeksDiv = queryByTestId('self-paced-relative-due-weeks-div');
|
||||
expect(selfPacedRelativeDueWeeksDiv).toBeInTheDocument();
|
||||
expect(selfPacedRelativeDueWeeksDiv).toHaveTextContent(
|
||||
messages.customDueDateLabel.defaultMessage,
|
||||
);
|
||||
|
||||
// check hide after due date message
|
||||
const hideAfterDueMessage = queryByTestId('hide-after-due-message');
|
||||
expect(hideAfterDueMessage).toBeInTheDocument();
|
||||
expect(hideAfterDueMessage).toHaveTextContent(messages.hiddenAfterEndDate.defaultMessage);
|
||||
|
||||
// check status messages
|
||||
expect(queryByTestId('status-messages-div')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with grading mismatch alert', () => {
|
||||
const { queryByText } = renderComponent({
|
||||
blockData: {
|
||||
...section,
|
||||
format: 'Fun',
|
||||
},
|
||||
});
|
||||
|
||||
// check alert
|
||||
const alert = queryByText(messages.gradingPolicyMismatchText.defaultMessage);
|
||||
expect(alert).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const subsection = {
|
||||
id: '123',
|
||||
displayName: 'Subsection Name',
|
||||
published: true,
|
||||
visibilityState: 'live',
|
||||
hasChanges: false,
|
||||
highlights: ['highlight 1', 'highlight 2'],
|
||||
category: 'sequential',
|
||||
explanatoryMessage: '',
|
||||
releasedToStudents: false,
|
||||
releaseDate: 'Feb 05, 2025 at 01:00 UTC',
|
||||
isProctoredExam: false,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: false,
|
||||
prereq: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc0',
|
||||
prereqs: [
|
||||
{
|
||||
blockDisplayName: 'Find your study buddy',
|
||||
blockUsageKey: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@dbe8fc0',
|
||||
},
|
||||
{
|
||||
blockDisplayName: 'Something else',
|
||||
blockUsageKey: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@sdafyrb',
|
||||
},
|
||||
],
|
||||
staffOnlyMessage: false,
|
||||
userPartitionInfo: {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
hasPartitionGroupComponents: false,
|
||||
format: 'Homework',
|
||||
dueDate: 'Dec 28, 2023 at 22:00 UTC',
|
||||
isTimeLimited: true,
|
||||
graded: true,
|
||||
courseGraders: ['Homework'],
|
||||
hideAfterDue: true,
|
||||
};
|
||||
|
||||
describe('<XBlockStatus /> for Instructor paced Subsection', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with release status, grading type, due date etc.', () => {
|
||||
const { queryByTestId } = renderComponent({ blockData: subsection });
|
||||
|
||||
expect(queryByTestId('explanatory-message-span')).not.toBeInTheDocument();
|
||||
// when explanatory message is not displayed, release date should be visible
|
||||
const releaseStatusDiv = queryByTestId('release-status-div');
|
||||
expect(releaseStatusDiv).toBeInTheDocument();
|
||||
expect(releaseStatusDiv).toHaveTextContent(
|
||||
`${messages.scheduledLabel.defaultMessage}${subsection.releaseDate}`,
|
||||
);
|
||||
|
||||
// check grading type
|
||||
const gradingTypeDiv = queryByTestId('grading-type-div');
|
||||
expect(gradingTypeDiv).toBeInTheDocument();
|
||||
expect(gradingTypeDiv).toHaveTextContent(subsection.format);
|
||||
// check exam value label
|
||||
const examValue = queryByTestId('exam-value-span');
|
||||
expect(examValue).toBeInTheDocument();
|
||||
expect(examValue).toHaveTextContent(messages.timedExam.defaultMessage);
|
||||
// check due date div
|
||||
const dueDateDiv = queryByTestId('due-date-div');
|
||||
expect(dueDateDiv).toBeInTheDocument();
|
||||
expect(dueDateDiv).toHaveTextContent(
|
||||
`${messages.dueLabel.defaultMessage} ${subsection.dueDate}`,
|
||||
);
|
||||
// self paced weeks should not be visible as
|
||||
// isSelfPaced is false as well as isCustomRelativeDatesActive is false
|
||||
expect(queryByTestId('self-paced-relative-due-weeks-div')).not.toBeInTheDocument();
|
||||
|
||||
// check hide after due date message
|
||||
const hideAfterDueMessage = queryByTestId('hide-after-due-message');
|
||||
expect(hideAfterDueMessage).toBeInTheDocument();
|
||||
expect(hideAfterDueMessage).toHaveTextContent(messages.hiddenAfterDueDate.defaultMessage);
|
||||
|
||||
// check status messages
|
||||
const statusDiv = queryByTestId('status-messages-div');
|
||||
expect(statusDiv).toBeInTheDocument();
|
||||
expect(statusDiv).toHaveTextContent(messages.prerequisiteLabel.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with proctored exam info', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...subsection,
|
||||
isProctoredExam: true,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: false,
|
||||
},
|
||||
});
|
||||
|
||||
// check exam value label
|
||||
const examValue = queryByTestId('exam-value-span');
|
||||
expect(examValue).toBeInTheDocument();
|
||||
expect(examValue).toHaveTextContent(messages.proctoredExam.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with practice proctored exam info', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...subsection,
|
||||
isProctoredExam: true,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: true,
|
||||
},
|
||||
});
|
||||
|
||||
// check exam value label
|
||||
const examValue = queryByTestId('exam-value-span');
|
||||
expect(examValue).toBeInTheDocument();
|
||||
expect(examValue).toHaveTextContent(messages.practiceProctoredExam.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with onboarding exam info', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...subsection,
|
||||
isProctoredExam: true,
|
||||
isOnboardingExam: true,
|
||||
isPracticeExam: false,
|
||||
},
|
||||
});
|
||||
|
||||
// check exam value label
|
||||
const examValue = queryByTestId('exam-value-span');
|
||||
expect(examValue).toBeInTheDocument();
|
||||
expect(examValue).toHaveTextContent(messages.onboardingExam.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders XBlockStatus correctly for graded but not time limited subsection', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...subsection,
|
||||
isTimeLimited: false,
|
||||
graded: true,
|
||||
},
|
||||
});
|
||||
|
||||
// check grading type
|
||||
const gradingTypeDiv = queryByTestId('grading-type-div');
|
||||
expect(gradingTypeDiv).toBeInTheDocument();
|
||||
expect(gradingTypeDiv).toHaveTextContent(subsection.format);
|
||||
// exam value label should not be visible
|
||||
expect(queryByTestId('exam-value-span')).not.toBeInTheDocument();
|
||||
// check due date div
|
||||
const dueDateDiv = queryByTestId('due-date-div');
|
||||
expect(dueDateDiv).toBeInTheDocument();
|
||||
expect(dueDateDiv).toHaveTextContent(
|
||||
`${messages.dueLabel.defaultMessage} ${subsection.dueDate}`,
|
||||
);
|
||||
// self paced weeks should not be visible as
|
||||
// isSelfPaced is false as well as isCustomRelativeDatesActive is false
|
||||
expect(queryByTestId('self-paced-relative-due-weeks-div')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('<XBlockStatus /> for self paced Subsection', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with grading type, due weeks etc.', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
isSelfPaced: true,
|
||||
isCustomRelativeDatesActive: true,
|
||||
blockData: {
|
||||
...subsection,
|
||||
relativeWeeksDue: 2,
|
||||
},
|
||||
});
|
||||
|
||||
// both explanatoryMessage & releaseStatusDiv should not be visible
|
||||
expect(queryByTestId('explanatory-message-span')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('release-status-div')).not.toBeInTheDocument();
|
||||
|
||||
// check grading type
|
||||
const gradingTypeDiv = queryByTestId('grading-type-div');
|
||||
expect(gradingTypeDiv).toBeInTheDocument();
|
||||
expect(gradingTypeDiv).toHaveTextContent(subsection.format);
|
||||
// due date should not be visible for self paced courses.
|
||||
expect(queryByTestId('due-date-div')).not.toBeInTheDocument();
|
||||
// check selfPacedRelativeDueWeeksDiv
|
||||
const selfPacedRelativeDueWeeksDiv = queryByTestId('self-paced-relative-due-weeks-div');
|
||||
expect(selfPacedRelativeDueWeeksDiv).toBeInTheDocument();
|
||||
expect(selfPacedRelativeDueWeeksDiv).toHaveTextContent(
|
||||
messages.customDueDateLabel.defaultMessage,
|
||||
);
|
||||
|
||||
// check hide after due date message
|
||||
const hideAfterDueMessage = queryByTestId('hide-after-due-message');
|
||||
expect(hideAfterDueMessage).toBeInTheDocument();
|
||||
expect(hideAfterDueMessage).toHaveTextContent(messages.hiddenAfterEndDate.defaultMessage);
|
||||
|
||||
// check status messages
|
||||
const statusDiv = queryByTestId('status-messages-div');
|
||||
expect(statusDiv).toBeInTheDocument();
|
||||
expect(statusDiv).toHaveTextContent(messages.prerequisiteLabel.defaultMessage);
|
||||
});
|
||||
});
|
||||
|
||||
const unit = {
|
||||
id: '123',
|
||||
displayName: 'Unit Name',
|
||||
published: true,
|
||||
visibilityState: 'live',
|
||||
hasChanges: false,
|
||||
highlights: ['highlight 1', 'highlight 2'],
|
||||
category: 'vertical',
|
||||
explanatoryMessage: '',
|
||||
releasedToStudents: true,
|
||||
releaseDate: 'Feb 05, 2013 at 01:00 UTC',
|
||||
isProctoredExam: false,
|
||||
isOnboardingExam: false,
|
||||
isPracticeExam: false,
|
||||
staffOnlyMessage: false,
|
||||
userPartitionInfo: {
|
||||
selectedPartitionIndex: 1,
|
||||
selectedGroupsLabel: 'Some label',
|
||||
},
|
||||
hasPartitionGroupComponents: false,
|
||||
format: 'Homework',
|
||||
dueDate: 'Dec 28, 2023 at 22:00 UTC',
|
||||
isTimeLimited: true,
|
||||
graded: true,
|
||||
courseGraders: ['Homework'],
|
||||
};
|
||||
|
||||
describe('<XBlockStatus /> for unit', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with status messages', () => {
|
||||
const { queryByTestId } = renderComponent({ blockData: unit });
|
||||
|
||||
expect(queryByTestId('explanatory-message-span')).not.toBeInTheDocument();
|
||||
expect(queryByTestId('release-status-div')).not.toBeInTheDocument();
|
||||
|
||||
// grading type should not be visible
|
||||
expect(queryByTestId('grading-type-div')).not.toBeInTheDocument();
|
||||
// due date should not be visible
|
||||
expect(queryByTestId('due-date-div')).not.toBeInTheDocument();
|
||||
|
||||
// self paced weeks should not be visible for units
|
||||
expect(queryByTestId('self-paced-relative-due-weeks-div')).not.toBeInTheDocument();
|
||||
|
||||
// check hide after due date message
|
||||
// hide after due date message should not be visible as the flag is set to false
|
||||
expect(queryByTestId('hide-after-due-message')).not.toBeInTheDocument();
|
||||
|
||||
// check status messages for partition info
|
||||
const statusDiv = queryByTestId('status-messages-div');
|
||||
expect(statusDiv).toBeInTheDocument();
|
||||
expect(statusDiv).toHaveTextContent(messages.restrictedUnitAccess.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders XBlockStatus with status messages', () => {
|
||||
const { queryByTestId } = renderComponent({
|
||||
blockData: {
|
||||
...unit,
|
||||
hasPartitionGroupComponents: true,
|
||||
userPartitionInfo: {
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// check status messages for partition info
|
||||
const statusDiv = queryByTestId('status-messages-div');
|
||||
expect(statusDiv).toBeInTheDocument();
|
||||
expect(statusDiv).toHaveTextContent(messages.restrictedUnitAccessToSomeContent.defaultMessage);
|
||||
});
|
||||
});
|
||||
78
src/course-outline/xblock-status/messages.js
Normal file
78
src/course-outline/xblock-status/messages.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
unscheduledLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.unscheduled.label',
|
||||
defaultMessage: 'Unscheduled',
|
||||
},
|
||||
releasedLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.released.label',
|
||||
defaultMessage: 'Released: ',
|
||||
},
|
||||
scheduledLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.scheduled.label',
|
||||
defaultMessage: 'Scheduled: ',
|
||||
},
|
||||
onboardingExam: {
|
||||
id: 'course-authoring.course-outline.xblock-status.onboardingExam.value',
|
||||
defaultMessage: 'Onboarding Exam',
|
||||
},
|
||||
practiceProctoredExam: {
|
||||
id: 'course-authoring.course-outline.xblock-status.practiceProctoredExam.value',
|
||||
defaultMessage: 'Practice proctored Exam',
|
||||
},
|
||||
proctoredExam: {
|
||||
id: 'course-authoring.course-outline.xblock-status.proctoredExam.value',
|
||||
defaultMessage: 'Proctored Exam',
|
||||
},
|
||||
timedExam: {
|
||||
id: 'course-authoring.course-outline.xblock-status.timedExam.value',
|
||||
defaultMessage: 'Timed Exam',
|
||||
},
|
||||
releaseStatusScreenReaderTitle: {
|
||||
id: 'course-authoring.course-outline.xblock-status.releaseStatusScreenReader.title',
|
||||
defaultMessage: 'Release Status: ',
|
||||
},
|
||||
gradedAsScreenReaderLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.gradedAsScreenReader.label',
|
||||
defaultMessage: 'Graded as: ',
|
||||
},
|
||||
ungradedText: {
|
||||
id: 'course-authoring.course-outline.xblock-status.ungraded.text',
|
||||
defaultMessage: 'Ungraded',
|
||||
},
|
||||
dueLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.due.label',
|
||||
defaultMessage: 'Due:',
|
||||
},
|
||||
customDueDateLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.custom-due-date.label',
|
||||
defaultMessage: 'Custom due date: {relativeWeeksDue, plural, one {# week} other {# weeks}} from enrollment',
|
||||
},
|
||||
prerequisiteLabel: {
|
||||
id: 'course-authoring.course-outline.xblock-status.prerequisite.label',
|
||||
defaultMessage: 'Prerequisite: {prereqDisplayName}',
|
||||
},
|
||||
restrictedUnitAccess: {
|
||||
id: 'course-authoring.course-outline.xblock-status.restrictedUnitAccess.text',
|
||||
defaultMessage: 'Access to this unit is restricted to: {selectedGroupsLabel}',
|
||||
},
|
||||
restrictedUnitAccessToSomeContent: {
|
||||
id: 'course-authoring.course-outline.xblock-status.restrictedUnitAccessToSomeContent.text',
|
||||
defaultMessage: 'Access to some content in this unit is restricted to specific groups of learners',
|
||||
},
|
||||
gradingPolicyMismatchText: {
|
||||
id: 'course-authoring.course-outline.xblock-status.gradingPolicyMismatch.text',
|
||||
defaultMessage: 'This subsection is configured as "{gradingType}", which doesn\'t exist in the current grading policy.',
|
||||
},
|
||||
hiddenAfterEndDate: {
|
||||
id: 'course-authoring.course-outline.xblock-status.hiddenAfterEndDate.text',
|
||||
defaultMessage: 'Subsection is hidden after course end date',
|
||||
},
|
||||
hiddenAfterDueDate: {
|
||||
id: 'course-authoring.course-outline.xblock-status.hiddenAfterDueDate.text',
|
||||
defaultMessage: 'Subsection is hidden after due date',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -45,6 +45,7 @@ export const DivisionSchemes = /** @type {const} */ ({
|
||||
});
|
||||
|
||||
export const VisibilityTypes = /** @type {const} */ ({
|
||||
GATED: 'gated',
|
||||
LIVE: 'live',
|
||||
STAFF_ONLY: 'staff_only',
|
||||
HIDE_AFTER_DUE: 'hide_after_due',
|
||||
|
||||
Reference in New Issue
Block a user