feat: add checklist page (#870)
* feat: add checklist page * fix: failing tests * fix: styling bugs * fix: lint errors * feat: add test fro CourseChecklist * fix: lint errors * feat: add ChecklistSection tests * fix: lint error * fix: missing api reply status
This commit is contained in:
1
.env
1
.env
@@ -40,3 +40,4 @@ HOTJAR_VERSION=6
|
||||
HOTJAR_DEBUG=false
|
||||
INVITE_STUDENTS_EMAIL_TO=''
|
||||
AI_TRANSLATIONS_BASE_URL=''
|
||||
ENABLE_CHECKLIST_QUALITY=''
|
||||
|
||||
@@ -42,3 +42,4 @@ HOTJAR_VERSION=6
|
||||
HOTJAR_DEBUG=true
|
||||
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
AI_TRANSLATIONS_BASE_URL='http://localhost:18760'
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
|
||||
@@ -34,3 +34,4 @@ ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN=true
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES=true
|
||||
BBB_LEARN_MORE_URL=''
|
||||
INVITE_STUDENTS_EMAIL_TO="someone@domain.com"
|
||||
ENABLE_CHECKLIST_QUALITY=true
|
||||
|
||||
1
course-ingestion-tool
Submodule
1
course-ingestion-tool
Submodule
Submodule course-ingestion-tool added at f8ec84aa4a
@@ -20,6 +20,7 @@ import { CourseUnit } from './course-unit';
|
||||
import CourseExportPage from './export-page/CourseExportPage';
|
||||
import CourseImportPage from './import-page/CourseImportPage';
|
||||
import { DECODED_ROUTES } from './constants';
|
||||
import CourseChecklist from './course-checklist';
|
||||
|
||||
/**
|
||||
* As of this writing, these routes are mounted at a path prefixed with the following:
|
||||
@@ -110,6 +111,10 @@ const CourseAuthoringRoutes = () => {
|
||||
path="export"
|
||||
element={<PageWrap><CourseExportPage courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
<Route
|
||||
path="checklists"
|
||||
element={<PageWrap><CourseChecklist courseId={courseId} /></PageWrap>}
|
||||
/>
|
||||
</Routes>
|
||||
</CourseAuthoringPage>
|
||||
);
|
||||
|
||||
36
src/course-checklist/AriaLiveRegion.jsx
Normal file
36
src/course-checklist/AriaLiveRegion.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AriaLiveRegion = ({
|
||||
isCourseLaunchChecklistLoading,
|
||||
isCourseBestPracticeChecklistLoading,
|
||||
enableQuality,
|
||||
}) => {
|
||||
const courseLaunchLoadingMessage = isCourseLaunchChecklistLoading
|
||||
? <FormattedMessage {...messages.launchChecklistLoadingLabel} />
|
||||
: <FormattedMessage {...messages.launchChecklistDoneLoadingLabel} />;
|
||||
|
||||
const courseBestPracticesLoadingMessage = isCourseBestPracticeChecklistLoading
|
||||
? <FormattedMessage {...messages.bestPracticesChecklistLoadingLabel} />
|
||||
: <FormattedMessage {...messages.bestPracticesChecklistDoneLoadingLabel} />;
|
||||
|
||||
return (
|
||||
<div className="sr-only" aria-live="polite" role="status">
|
||||
<div>
|
||||
{courseLaunchLoadingMessage}
|
||||
</div>
|
||||
{enableQuality ? <div>{courseBestPracticesLoadingMessage}</div> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AriaLiveRegion.propTypes = {
|
||||
isCourseLaunchChecklistLoading: PropTypes.bool.isRequired,
|
||||
isCourseBestPracticeChecklistLoading: PropTypes.bool.isRequired,
|
||||
enableQuality: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AriaLiveRegion);
|
||||
70
src/course-checklist/ChecklistSection/ChecklistItemBody.jsx
Normal file
70
src/course-checklist/ChecklistSection/ChecklistItemBody.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Button,
|
||||
Hyperlink,
|
||||
Icon,
|
||||
} from '@openedx/paragon';
|
||||
import { CheckCircle, RadioButtonUnchecked } from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
|
||||
const ChecklistItemBody = ({
|
||||
checkId,
|
||||
isCompleted,
|
||||
updateLink,
|
||||
// injected
|
||||
intl,
|
||||
}) => (
|
||||
<ActionRow>
|
||||
<div className="mr-3" id={`icon-${checkId}`} data-testid={`icon-${checkId}`}>
|
||||
{isCompleted ? (
|
||||
<Icon
|
||||
data-testid="completed-icon"
|
||||
src={CheckCircle}
|
||||
className="text-success"
|
||||
style={{ height: '32px', width: '32px' }}
|
||||
screenReaderText={intl.formatMessage(messages.completedItemLabel)}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
data-testid="uncompleted-icon"
|
||||
src={RadioButtonUnchecked}
|
||||
style={{ height: '32px', width: '32px' }}
|
||||
screenReaderText={intl.formatMessage(messages.uncompletedItemLabel)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<FormattedMessage {...messages[`${checkId}ShortDescription`]} />
|
||||
</div>
|
||||
<div className="small">
|
||||
<FormattedMessage {...messages[`${checkId}LongDescription`]} />
|
||||
</div>
|
||||
</div>
|
||||
<ActionRow.Spacer />
|
||||
{updateLink && (
|
||||
<Hyperlink destination={updateLink} data-testid="update-hyperlink">
|
||||
<Button size="sm">
|
||||
<FormattedMessage {...messages.updateLinkLabel} />
|
||||
</Button>
|
||||
</Hyperlink>
|
||||
)}
|
||||
</ActionRow>
|
||||
);
|
||||
|
||||
ChecklistItemBody.defaultProps = {
|
||||
updateLink: null,
|
||||
};
|
||||
|
||||
ChecklistItemBody.propTypes = {
|
||||
checkId: PropTypes.string.isRequired,
|
||||
isCompleted: PropTypes.bool.isRequired,
|
||||
updateLink: PropTypes.string,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ChecklistItemBody);
|
||||
123
src/course-checklist/ChecklistSection/ChecklistItemComment.jsx
Normal file
123
src/course-checklist/ChecklistSection/ChecklistItemComment.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl, FormattedMessage, FormattedNumber } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Icon } from '@openedx/paragon';
|
||||
import { ModeComment } from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
|
||||
const ChecklistItemComment = ({
|
||||
checkId,
|
||||
outlineUrl,
|
||||
data,
|
||||
}) => {
|
||||
const commentWrapper = (comment) => (
|
||||
<div className="row m-0 mt-3 pt-3 border-top align-items-center" data-identifier="comment">
|
||||
<div className="mr-4">
|
||||
<Icon src={ModeComment} size="lg" style={{ height: '32px', width: '32px' }} />
|
||||
</div>
|
||||
<div className="small">
|
||||
{comment}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (checkId === 'gradingPolicy') {
|
||||
const sumOfWeights = data?.grades.sumOfWeights || 0;
|
||||
const showGradingCommentSection = Object.keys(data).length > 0 && sumOfWeights !== 1;
|
||||
|
||||
const weightSumPercentage = (sumOfWeights * 100).toFixed(2);
|
||||
const comment = (
|
||||
<FormattedMessage
|
||||
{...messages.gradingPolicyComment}
|
||||
values={{
|
||||
percent: (
|
||||
<FormattedNumber
|
||||
maximumFractionDigits={2}
|
||||
minimumFractionDigits={2}
|
||||
value={weightSumPercentage}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (showGradingCommentSection ? (
|
||||
commentWrapper(comment)
|
||||
) : null);
|
||||
}
|
||||
|
||||
if (checkId === 'assignmentDeadlines') {
|
||||
const showDeadlinesCommentSection = Object.keys(data).length > 0
|
||||
&& (
|
||||
data.assignments.assignmentsWithDatesBeforeStart.length > 0
|
||||
|| data?.assignments.assignmentsWithDatesAfterEnd.length > 0
|
||||
|| data?.assignments.assignmentsWithOraDatesBeforeStart.length > 0
|
||||
|| data?.assignments.assignmentsWithOraDatesAfterEnd.length > 0
|
||||
);
|
||||
|
||||
const allGradedAssignmentsOutsideDateRange = [].concat(
|
||||
data?.assignments.assignmentsWithDatesBeforeStart,
|
||||
data?.assignments.assignmentsWithDatesAfterEnd,
|
||||
data?.assignments.assignmentsWithOraDatesBeforeStart,
|
||||
data?.assignments.assignmentsWithOraDatesAfterEnd,
|
||||
);
|
||||
|
||||
// de-dupe in case one assignment has multiple violations
|
||||
const assignmentsMap = new Map();
|
||||
allGradedAssignmentsOutsideDateRange.forEach(
|
||||
(assignment) => { assignmentsMap.set(assignment.id, assignment); },
|
||||
);
|
||||
const gradedAssignmentsOutsideDateRange = [];
|
||||
assignmentsMap.forEach(
|
||||
(value) => {
|
||||
gradedAssignmentsOutsideDateRange.push(value);
|
||||
},
|
||||
);
|
||||
|
||||
const comment = (
|
||||
<>
|
||||
<FormattedMessage {...messages.assignmentDeadlinesComment} />
|
||||
<ul className="assignment-list">
|
||||
{gradedAssignmentsOutsideDateRange.map(assignment => (
|
||||
<li className="assignment-list-item" key={assignment.id}>
|
||||
<Hyperlink
|
||||
content={assignment.displayName}
|
||||
destination={`${outlineUrl}#${assignment.id}`}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
return (showDeadlinesCommentSection ? (
|
||||
commentWrapper(comment)
|
||||
) : null);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ChecklistItemComment.propTypes = {
|
||||
checkId: PropTypes.string.isRequired,
|
||||
outlineUrl: PropTypes.string.isRequired,
|
||||
data: PropTypes.oneOfType([
|
||||
PropTypes.shape({
|
||||
grades: PropTypes.shape({
|
||||
sumOfWeights: PropTypes.number,
|
||||
}),
|
||||
}).isRequired,
|
||||
PropTypes.shape({
|
||||
assignments: PropTypes.shape({
|
||||
totalNumber: PropTypes.number,
|
||||
totalVisible: PropTypes.number,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
assignmentsWithDatesBeforeStart: PropTypes.array,
|
||||
assignmentsWithDatesAfterEnd: PropTypes.array,
|
||||
assignmentsWithOraDatesBeforeStart: PropTypes.array,
|
||||
assignmentsWithOraDatesAfterEnd: PropTypes.array,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
}),
|
||||
}).isRequired,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ChecklistItemComment);
|
||||
142
src/course-checklist/ChecklistSection/ChecklistSection.jsx
Normal file
142
src/course-checklist/ChecklistSection/ChecklistSection.jsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Container, Stack } from '@openedx/paragon';
|
||||
|
||||
import { LoadingSpinner } from '../../generic/Loading';
|
||||
import { getCompletionCount, useChecklistState } from './hooks';
|
||||
import ChecklistItemBody from './ChecklistItemBody';
|
||||
import ChecklistItemComment from './ChecklistItemComment';
|
||||
import { checklistItems } from './utils/courseChecklistData';
|
||||
|
||||
const ChecklistSection = ({
|
||||
dataHeading,
|
||||
data,
|
||||
idPrefix,
|
||||
isLoading,
|
||||
updateLinks,
|
||||
}) => {
|
||||
const dataList = checklistItems[idPrefix];
|
||||
const getCompletionCountID = () => (`${idPrefix}-completion-count`);
|
||||
const { checklistState } = useChecklistState({ data, dataList });
|
||||
const { checks, totalCompletedChecks, values } = checklistState;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<h3 aria-describedby={getCompletionCountID()} className="lead">{dataHeading}</h3>
|
||||
{isLoading ? (
|
||||
<div className="row justify-content-center" data-testid="loading-spinner">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div data-testid="completion-subheader">
|
||||
{getCompletionCount(checks, totalCompletedChecks)}
|
||||
</div>
|
||||
<Stack gap={3} className="mt-3">
|
||||
{checks.map(check => {
|
||||
const checkId = check.id;
|
||||
const isCompleted = values[checkId];
|
||||
const updateLink = updateLinks?.[checkId];
|
||||
const outlineUrl = updateLinks.outline;
|
||||
return (
|
||||
<div
|
||||
className={`bg-white border py-3 px-4 ${isCompleted && 'checklist-item-complete'}`}
|
||||
id={`checklist-item-${checkId}`}
|
||||
data-testid={`checklist-item-${checkId}`}
|
||||
key={checkId}
|
||||
>
|
||||
<ChecklistItemBody {...{ checkId, isCompleted, updateLink }} />
|
||||
<div data-testid={`comment-section-${checkId}`}>
|
||||
<ChecklistItemComment {...{ checkId, outlineUrl, data }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
ChecklistSection.defaultProps = {
|
||||
updateLinks: {},
|
||||
data: {},
|
||||
};
|
||||
|
||||
ChecklistSection.propTypes = {
|
||||
dataHeading: PropTypes.string.isRequired,
|
||||
data: PropTypes.oneOfType([
|
||||
PropTypes.shape({
|
||||
assignments: PropTypes.shape({
|
||||
totalNumber: PropTypes.number,
|
||||
totalVisible: PropTypes.number,
|
||||
numWithDatesBeforeEnd: PropTypes.number,
|
||||
numWithDates: PropTypes.number,
|
||||
numWithDatesAfterStart: PropTypes.number,
|
||||
}),
|
||||
dates: PropTypes.shape({
|
||||
hasStartDate: PropTypes.bool,
|
||||
hasEndDate: PropTypes.bool,
|
||||
}),
|
||||
updates: PropTypes.shape({
|
||||
hasUpdate: PropTypes.bool,
|
||||
}),
|
||||
certificates: PropTypes.shape({
|
||||
isEnabled: PropTypes.bool,
|
||||
isActivated: PropTypes.bool,
|
||||
hasCertificate: PropTypes.bool,
|
||||
}),
|
||||
grades: PropTypes.shape({
|
||||
sumOfWeights: PropTypes.number,
|
||||
}),
|
||||
is_self_paced: PropTypes.bool,
|
||||
}).isRequired,
|
||||
PropTypes.shape({
|
||||
assignments: PropTypes.shape({
|
||||
totalNumber: PropTypes.number,
|
||||
totalVisible: PropTypes.number,
|
||||
/* eslint-disable react/forbid-prop-types */
|
||||
assignmentsWithDatesBeforeStart: PropTypes.array,
|
||||
assignmentsWithDatesAfterEnd: PropTypes.array,
|
||||
assignmentsWithOraDatesBeforeStart: PropTypes.array,
|
||||
assignmentsWithOraDatesAfterEnd: PropTypes.array,
|
||||
/* eslint-enable react/forbid-prop-types */
|
||||
}),
|
||||
dates: PropTypes.shape({
|
||||
hasStartDate: PropTypes.bool,
|
||||
hasEndDate: PropTypes.bool,
|
||||
}),
|
||||
updates: PropTypes.shape({
|
||||
hasUpdate: PropTypes.bool,
|
||||
}),
|
||||
certificates: PropTypes.shape({
|
||||
isEnabled: PropTypes.bool,
|
||||
isActivated: PropTypes.bool,
|
||||
hasCertificate: PropTypes.bool,
|
||||
}),
|
||||
grades: PropTypes.shape({
|
||||
hasGradingPolicy: PropTypes.bool,
|
||||
sumOfWeights: PropTypes.number,
|
||||
}),
|
||||
proctoring: PropTypes.shape({
|
||||
needsProctoringEscalationEmail: PropTypes.bool,
|
||||
hasProctoringEscalation_email: PropTypes.bool,
|
||||
}),
|
||||
isSelfPaced: PropTypes.bool,
|
||||
}).isRequired,
|
||||
]),
|
||||
idPrefix: PropTypes.string.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
updateLinks: PropTypes.shape({
|
||||
welcomeMessage: PropTypes.string,
|
||||
gradingPolicy: PropTypes.string,
|
||||
certificate: PropTypes.string,
|
||||
courseDates: PropTypes.string,
|
||||
proctoringEmail: PropTypes.string,
|
||||
outline: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
export default injectIntl(ChecklistSection);
|
||||
22
src/course-checklist/ChecklistSection/ChecklistSection.scss
Normal file
22
src/course-checklist/ChecklistSection/ChecklistSection.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.assignment-list-item {
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
|
||||
&::after {
|
||||
content: ",";
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&::after { content: ""; }
|
||||
}
|
||||
}
|
||||
|
||||
.assignment-list {
|
||||
display: inline;
|
||||
padding-inline-start: map-get($spacers, 1);
|
||||
}
|
||||
|
||||
//complete checklist item style
|
||||
.checklist-item-complete {
|
||||
box-shadow: -5px 0 0 0 $success-500;
|
||||
}
|
||||
255
src/course-checklist/ChecklistSection/ChecklistSection.test.jsx
Normal file
255
src/course-checklist/ChecklistSection/ChecklistSection.test.jsx
Normal file
@@ -0,0 +1,255 @@
|
||||
/* eslint-disable */
|
||||
import {
|
||||
render,
|
||||
within,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { camelCaseObject, initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import initializeStore from '../../store';
|
||||
import { initialState,generateCourseLaunchData } from '../factories/mockApiResponses';
|
||||
import messages from './messages';
|
||||
import ChecklistSection from './index';
|
||||
import { checklistItems } from './utils/courseChecklistData';
|
||||
import getUpdateLinks from '../utils';
|
||||
|
||||
const testData = camelCaseObject(generateCourseLaunchData());
|
||||
|
||||
|
||||
const defaultProps = {
|
||||
data: testData,
|
||||
dataHeading: 'Test checklist',
|
||||
idPrefix: 'launchChecklist',
|
||||
updateLinks: getUpdateLinks('courseId'),
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const testChecklistData = checklistItems[defaultProps.idPrefix];
|
||||
|
||||
const completedItemIds = ['welcomeMessage', 'courseDates']
|
||||
|
||||
const renderComponent = (props) => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<ChecklistSection {...props} />
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
let store;
|
||||
|
||||
describe('ChecklistSection', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
});
|
||||
|
||||
it('a heading using the dataHeading prop', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
expect(screen.getByText(defaultProps.dataHeading)).toBeVisible();
|
||||
});
|
||||
|
||||
it('completion count text', () => {
|
||||
renderComponent(defaultProps);
|
||||
const completionText = `${completedItemIds.length}/6 completed`;
|
||||
expect(screen.getByTestId('completion-subheader').textContent).toEqual(completionText);
|
||||
});
|
||||
|
||||
it('a loading spinner when isLoading prop is true', () => {
|
||||
renderComponent({ ...defaultProps, isLoading: true });
|
||||
|
||||
const completionSubheader = screen.queryByTestId('completion-subheader');
|
||||
expect(completionSubheader).toBeNull();
|
||||
|
||||
const loadingSpinner = screen.getByTestId('loading-spinner');
|
||||
expect(loadingSpinner).toBeVisible();
|
||||
});
|
||||
|
||||
it('the correct number of checks', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
const listItems = screen.getAllByTestId('checklist-item', { exact: false });
|
||||
expect(listItems).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('welcomeMessage comment section should be null', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
const comment = screen.getByTestId('comment-section-welcomeMessage');
|
||||
expect(comment.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('certificate comment section should be null', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
const comment = screen.getByTestId('comment-section-certificate');
|
||||
expect(comment.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('courseDates comment section should be null', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
const comment = screen.getByTestId('comment-section-courseDates');
|
||||
expect(comment.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('proctoringEmail comment section should be null', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
const comment = screen.getByTestId('comment-section-proctoringEmail');
|
||||
expect(comment.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
describe('gradingPolicy comment section', () => {
|
||||
it('should be null if sum of weights is equal to 1', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
grades: {
|
||||
...defaultProps.data.grades,
|
||||
sumOfWeights: 1,
|
||||
}
|
||||
},
|
||||
};
|
||||
renderComponent(props);
|
||||
|
||||
const comment = screen.getByTestId('comment-section-gradingPolicy');
|
||||
expect(comment.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should have comment section', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
const comment = screen.getByTestId('comment-section-gradingPolicy');
|
||||
expect(comment.children).toHaveLength(1);
|
||||
|
||||
expect(screen.getByText(
|
||||
'Your current grading policy adds up to',
|
||||
{ exact: false },
|
||||
)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('assignmentDeadlines comment section', () => {
|
||||
it('should be null if assignments with dates before start and after end are empty', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
data: {
|
||||
...defaultProps.data,
|
||||
assignments: {
|
||||
...defaultProps.data.assignments,
|
||||
assignmentsWithDatesAfterEnd: [],
|
||||
assignmentsWithOraDatesBeforeStart: [],
|
||||
}
|
||||
},
|
||||
};
|
||||
renderComponent(props);
|
||||
|
||||
const comment = screen.getByTestId('comment-section-assignmentDeadlines');
|
||||
expect(comment.children).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should have comment section', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
const comment = screen.getByTestId('comment-section-assignmentDeadlines');
|
||||
const assigmentLinks = within(comment).getAllByRole('link');
|
||||
|
||||
expect(comment.children).toHaveLength(1);
|
||||
|
||||
expect(screen.getByText(
|
||||
messages.assignmentDeadlinesComment.defaultMessage,
|
||||
{ exact: false },
|
||||
)).toBeVisible();
|
||||
|
||||
expect(assigmentLinks).toHaveLength(2);
|
||||
|
||||
expect(assigmentLinks[0].textContent).toEqual('Subsection');
|
||||
|
||||
expect(assigmentLinks[1].textContent).toEqual('ORA subsection');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
testChecklistData.forEach((check) => {
|
||||
describe(`check with id '${check.id}'`, () => {
|
||||
let checkItem;
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
renderComponent(defaultProps);
|
||||
checkItem = screen.getAllByTestId(`checklist-item-${check.id}`);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(checkItem).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('has correct icon', () => {
|
||||
const icon = screen.getAllByTestId(`icon-${check.id}`)
|
||||
|
||||
expect(icon).toHaveLength(1);
|
||||
|
||||
const { queryByTestId } = within(icon[0]);
|
||||
if (completedItemIds.includes(check.id)) {
|
||||
expect(queryByTestId('completed-icon')).not.toBeNull();
|
||||
} else {
|
||||
expect(queryByTestId('uncompleted-icon')).not.toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('has correct short description', () => {
|
||||
const { getByText } = within(checkItem[0]);
|
||||
const shortDescription = messages[`${check.id}ShortDescription`].defaultMessage;
|
||||
expect(getByText(shortDescription)).toBeVisible();
|
||||
});
|
||||
|
||||
it('has correct long description', () => {
|
||||
const { getByText } = within(checkItem[0]);
|
||||
const longDescription = messages[`${check.id}LongDescription`].defaultMessage;
|
||||
expect(getByText(longDescription)).toBeVisible();
|
||||
});
|
||||
|
||||
describe('has correct link', () => {
|
||||
const links = getUpdateLinks('courseId')
|
||||
const shouldShowLink = Object.keys(links).includes(check.id);
|
||||
|
||||
if (shouldShowLink) {
|
||||
it('with a Hyperlink', () => {
|
||||
const { getByRole, getByText } = within(checkItem[0]);
|
||||
|
||||
expect(getByText('Update')).toBeVisible();
|
||||
|
||||
expect(getByRole('link').href).toMatch(links[check.id]);
|
||||
});
|
||||
} else {
|
||||
it('without a Hyperlink', () => {
|
||||
const { queryByText } = within(checkItem[0]);
|
||||
|
||||
expect(queryByText('Update')).toBeNull();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
71
src/course-checklist/ChecklistSection/hooks.jsx
Normal file
71
src/course-checklist/ChecklistSection/hooks.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import getFilteredChecklist from './utils/getFilteredChecklist';
|
||||
import getValidatedValue from './utils/getValidatedValue';
|
||||
|
||||
export const useChecklistState = ({ data, dataList }) => {
|
||||
const [checklistState, setChecklistState] = useState({
|
||||
checks: [],
|
||||
totalCompletedChecks: 0,
|
||||
values: {},
|
||||
});
|
||||
|
||||
const updateChecklistState = () => {
|
||||
if (Object.keys(data).length > 0) {
|
||||
const { isSelfPaced } = data;
|
||||
const hasCertificatesEnabled = data.certificates && data.certificates.isEnabled;
|
||||
const hasHighlightsEnabled = data.sections && data.sections.highlightsEnabled;
|
||||
const needsProctoringEscalationEmail = (
|
||||
data.proctoring && data.proctoring.needsProctoringEscalationEmail
|
||||
);
|
||||
const checks = getFilteredChecklist(
|
||||
dataList,
|
||||
isSelfPaced,
|
||||
hasCertificatesEnabled,
|
||||
hasHighlightsEnabled,
|
||||
needsProctoringEscalationEmail,
|
||||
);
|
||||
|
||||
const values = {};
|
||||
let totalCompletedChecks = 0;
|
||||
|
||||
checks.forEach((check) => {
|
||||
const value = getValidatedValue(data, check.id);
|
||||
|
||||
if (value) {
|
||||
totalCompletedChecks += 1;
|
||||
}
|
||||
|
||||
values[check.id] = value;
|
||||
});
|
||||
|
||||
setChecklistState({
|
||||
checks,
|
||||
totalCompletedChecks,
|
||||
values,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateChecklistState();
|
||||
}, [data]);
|
||||
|
||||
return {
|
||||
checklistState,
|
||||
setChecklistState,
|
||||
};
|
||||
};
|
||||
|
||||
export const getCompletionCount = (checks, totalCompletedChecks) => {
|
||||
const totalChecks = Object.values(checks).length;
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
{...messages.completionCountLabel}
|
||||
values={{ completed: totalCompletedChecks, total: totalChecks }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
3
src/course-checklist/ChecklistSection/index.js
Normal file
3
src/course-checklist/ChecklistSection/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import ChecklistSection from './ChecklistSection';
|
||||
|
||||
export default ChecklistSection;
|
||||
146
src/course-checklist/ChecklistSection/messages.js
Normal file
146
src/course-checklist/ChecklistSection/messages.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
welcomeMessageShortDescription: {
|
||||
id: 'welcomeMessageShortDescription',
|
||||
defaultMessage: 'Add a welcome message',
|
||||
description: 'Label for a section that describes a welcome message for a course',
|
||||
},
|
||||
welcomeMessageLongDescription: {
|
||||
id: 'welcomeMessageLongDescription',
|
||||
defaultMessage: 'Personally welcome learners into your course and prepare learners for a positive course experience.',
|
||||
description: 'Description for a section that prompts a user to enter a welcome message for a course',
|
||||
},
|
||||
gradingPolicyShortDescription: {
|
||||
id: 'gradingPolicyShortDescription',
|
||||
defaultMessage: 'Create your course grading policy',
|
||||
description: 'Label for a section that describes a grading policy for a course',
|
||||
},
|
||||
gradingPolicyLongDescription: {
|
||||
id: 'gradingPolicyLongDescription',
|
||||
defaultMessage: 'Establish your grading policy, including assignment types and passing score. All assignments add up to 100%.',
|
||||
description: 'Description for a section that prompts a user to enter a grading policy for a course',
|
||||
},
|
||||
gradingPolicyComment: {
|
||||
id: 'gradingPolicyComment',
|
||||
defaultMessage: 'Your current grading policy adds up to {percent}%.',
|
||||
description: 'Description for a section that displays a course\'s grading policy total',
|
||||
},
|
||||
certificateShortDescription: {
|
||||
id: 'certificateShortDescription',
|
||||
defaultMessage: 'Enable your certificate',
|
||||
description: 'Label for a section that describes a certificate for completing a course',
|
||||
},
|
||||
certificateLongDescription: {
|
||||
id: 'certificateLongDescription',
|
||||
defaultMessage: 'Make sure that all text is correct, signatures have been uploaded, and the certificate has been activated.',
|
||||
description: 'Description for a section that prompts a user to create a course completion certificate',
|
||||
},
|
||||
courseDatesShortDescription: {
|
||||
id: 'courseDatesShortDescription',
|
||||
defaultMessage: 'Set important course dates',
|
||||
description: 'Label for a section that describes a certificate for completing a course',
|
||||
},
|
||||
courseDatesLongDescription: {
|
||||
id: 'courseDatesLongDescription',
|
||||
defaultMessage: 'Establish your course schedule, including when the course starts and ends.',
|
||||
description: 'Description for a section that prompts a user to set up a course schedule',
|
||||
},
|
||||
assignmentDeadlinesShortDescription: {
|
||||
id: 'assignmentDeadlinesShortDescription',
|
||||
defaultMessage: 'Validate assignment deadlines',
|
||||
description: 'Label for a section that describes course assignment deadlines',
|
||||
},
|
||||
assignmentDeadlinesLongDescription: {
|
||||
id: 'assignmentDeadlinesLongDescription',
|
||||
defaultMessage: 'Ensure all assignment deadlines are between course start and end dates.',
|
||||
description: 'Description for a section that prompts a user to enter course assignment deadlines',
|
||||
},
|
||||
assignmentDeadlinesComment: {
|
||||
id: 'assignmentDeadlinesComment',
|
||||
defaultMessage: 'The following assignments have deadlines that do not fall between course start and end date:',
|
||||
description: 'Description for a section that displays which assignments are outside of a course\'s start and end date',
|
||||
},
|
||||
videoDurationShortDescription: {
|
||||
id: 'videoDurationShortDescription',
|
||||
defaultMessage: 'Check video duration',
|
||||
description: 'Label for a section that describes video durations',
|
||||
},
|
||||
videoDurationLongDescription: {
|
||||
id: 'videoDurationLongDescription',
|
||||
defaultMessage: 'Learners engage best with short videos followed by opportunities to practice. Ensure that 80% or more of course videos are less than 10 minutes long.',
|
||||
description: 'Description for a section that prompts a user to follow best practices for video length',
|
||||
},
|
||||
mobileFriendlyVideoShortDescription: {
|
||||
id: 'mobileFriendlyVideoShortDescription',
|
||||
defaultMessage: 'Create mobile-friendly video',
|
||||
description: 'Label for a section that describes mobile friendly videos',
|
||||
},
|
||||
mobileFriendlyVideoLongDescription: {
|
||||
id: 'mobileFriendlyVideoLongDescription',
|
||||
defaultMessage: 'Mobile-friendly videos can be viewed across all supported devices. Ensure that at least 90% of course videos are mobile friendly by uploading course videos to the edX video pipeline.',
|
||||
description: 'Description for a section that prompts a user to follow best practices for mobile friendly videos',
|
||||
},
|
||||
diverseSequencesShortDescription: {
|
||||
id: 'diverseSequencesShortDescription',
|
||||
defaultMessage: 'Build diverse learning sequences',
|
||||
description: 'Label for a section that describes diverse sequences of educational content',
|
||||
},
|
||||
diverseSequencesLongDescription: {
|
||||
id: 'diverseSequencesLongDescription',
|
||||
defaultMessage: 'Research shows that a diverse content experience drives learner engagement. We recommend that 80% or more of your learning sequences or subsections include multiple content types (such as video, discussion, or problem).',
|
||||
description: 'Description for a section that prompts a user to follow best practices diverse sequences of educational content',
|
||||
},
|
||||
weeklyHighlightsShortDescription: {
|
||||
id: 'weeklyHighlightsShortDescription',
|
||||
defaultMessage: 'Set weekly highlights',
|
||||
description: 'Label for a section that describes weekly highlights',
|
||||
},
|
||||
weeklyHighlightsLongDescription: {
|
||||
id: 'weeklyHighlightsLongDescription',
|
||||
defaultMessage: 'Enable and specify weekly highlights to keep learners engaged and on track in your course.',
|
||||
description: 'Description for a section that prompts a user to follow best practices for course weekly highlights',
|
||||
},
|
||||
unitDepthShortDescription: {
|
||||
id: 'unitDepthShortDescription',
|
||||
defaultMessage: 'Manage unit depth',
|
||||
description: 'Label for a section that describes course unit depth',
|
||||
},
|
||||
unitDepthLongDescription: {
|
||||
id: 'unitDepthLongDescription',
|
||||
defaultMessage: 'Breaking up course content into manageable pieces promotes learner engagement. We recommend units contain no more than three components.',
|
||||
description: 'Description for a section that prompts a user to follow best practices for course unit depth',
|
||||
},
|
||||
proctoringEmailShortDescription: {
|
||||
id: 'proctoringEmailShortDescription',
|
||||
defaultMessage: 'Add a proctortrack escalation email',
|
||||
description: 'Label for a section that describes proctoring escalation email',
|
||||
},
|
||||
proctoringEmailLongDescription: {
|
||||
id: 'proctoringEmailLongDescription',
|
||||
defaultMessage: 'Courses using Proctortrack require an escalation email. Ensure learners and Support can contact your course team regarding proctoring issues (e.g. appeals, exam resets, etc).',
|
||||
description: 'Description for a section that prompts the user to add a Proctortrack escalation email for the course',
|
||||
},
|
||||
updateLinkLabel: {
|
||||
id: 'updateLinkLabel',
|
||||
defaultMessage: 'Update',
|
||||
description: 'Label for a link that takes the user to a page where they can update settings',
|
||||
},
|
||||
completionCountLabel: {
|
||||
id: 'completionCountLabel',
|
||||
defaultMessage: '{completed}/{total} completed',
|
||||
description: 'Label that describes how many tasks have been completed out of a total number of tasks',
|
||||
},
|
||||
completedItemLabel: {
|
||||
id: 'completedItemLabel',
|
||||
defaultMessage: 'completed',
|
||||
description: 'Label that describes a completed task',
|
||||
},
|
||||
uncompletedItemLabel: {
|
||||
id: 'uncompletedItemLabel',
|
||||
defaultMessage: 'uncompleted',
|
||||
description: 'Label that describes an uncompleted task',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,56 @@
|
||||
export const filters = {
|
||||
ALL: 'ALL',
|
||||
SELF_PACED: 'SELF_PACED',
|
||||
INSTRUCTOR_PACED: 'INSTRUCTOR_PACED',
|
||||
};
|
||||
|
||||
export const checklistItems = {
|
||||
launchChecklist: [
|
||||
{
|
||||
id: 'welcomeMessage',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'gradingPolicy',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'certificate',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'courseDates',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'assignmentDeadlines',
|
||||
pacingTypeFilter: filters.INSTRUCTOR_PACED,
|
||||
},
|
||||
{
|
||||
id: 'proctoringEmail',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
],
|
||||
bestPracticesChecklist: [
|
||||
{
|
||||
id: 'videoDuration',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'mobileFriendlyVideo',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'diverseSequences',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'weeklyHighlights',
|
||||
pacingTypeFilter: filters.SELF_PACED,
|
||||
},
|
||||
{
|
||||
id: 'unitDepth',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
export const hasWelcomeMessage = updates => (
|
||||
updates.hasUpdate
|
||||
);
|
||||
|
||||
export const hasGradingPolicy = grades => (
|
||||
grades.hasGradingPolicy
|
||||
&& parseFloat(grades.sumOfWeights.toPrecision(2), 10) === 1.0
|
||||
);
|
||||
|
||||
export const hasCertificate = certificates => (
|
||||
certificates.isActivated && certificates.hasCertificate
|
||||
);
|
||||
|
||||
export const hasDates = dates => (
|
||||
dates.hasStartDate && dates.hasEndDate
|
||||
);
|
||||
|
||||
export const hasAssignmentDeadlines = (assignments, dates) => {
|
||||
if (!hasDates(dates)) {
|
||||
return false;
|
||||
} if (assignments.totalNumber === 0) {
|
||||
return false;
|
||||
} if (assignments.assignmentsWithDatesBeforeStart.length > 0) {
|
||||
return false;
|
||||
} if (assignments.assignmentsWithDatesAfterEnd.length > 0) {
|
||||
return false;
|
||||
} if (assignments.assignmentsWithOraDatesBeforeStart.length > 0) {
|
||||
return false;
|
||||
} if (assignments.assignmentsWithOraDatesAfterEnd.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const hasShortVideoDuration = (videos) => {
|
||||
if (videos.totalNumber === 0) {
|
||||
return true;
|
||||
} if (videos.totalNumber > 0 && videos.durations.median <= 600) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const hasMobileFriendlyVideos = (videos) => {
|
||||
if (videos.totalNumber === 0) {
|
||||
return true;
|
||||
} if (videos.totalNumber > 0 && (videos.numMobileEncoded / videos.totalNumber) >= 0.9) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const hasDiverseSequences = (subsections) => {
|
||||
if (subsections.totalVisible === 0) {
|
||||
return false;
|
||||
} if (subsections.totalVisible > 0) {
|
||||
return ((subsections.numWithOneBlockType / subsections.totalVisible) < 0.2);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const hasWeeklyHighlights = sections => (
|
||||
sections.highlightsActiveForCourse && sections.highlightsEnabled
|
||||
);
|
||||
|
||||
export const hasShortUnitDepth = units => (
|
||||
units.numBlocks.median <= 3
|
||||
);
|
||||
|
||||
export const hasProctoringEscalationEmail = proctoring => (
|
||||
proctoring.hasProctoringEscalationEmail
|
||||
);
|
||||
@@ -0,0 +1,297 @@
|
||||
import * as validators from './courseChecklistValidators';
|
||||
|
||||
describe('courseCheckValidators utility functions', () => {
|
||||
describe('hasWelcomeMessage', () => {
|
||||
it('returns true when course run has an update', () => {
|
||||
expect(validators.hasWelcomeMessage({ hasUpdate: true })).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when course run does not have an update', () => {
|
||||
expect(validators.hasWelcomeMessage({ hasUpdate: false })).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasGradingPolicy', () => {
|
||||
it('returns true when sum of weights is 1', () => {
|
||||
expect(validators.hasGradingPolicy(
|
||||
{ hasGradingPolicy: true, sumOfWeights: 1 },
|
||||
)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true when sum of weights is not 1 due to floating point approximation (1.00004)', () => {
|
||||
expect(validators.hasGradingPolicy(
|
||||
{ hasGradingPolicy: true, sumOfWeights: 1.00004 },
|
||||
)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when sum of weights is not 1', () => {
|
||||
expect(validators.hasGradingPolicy(
|
||||
{ hasGradingPolicy: true, sumOfWeights: 2 },
|
||||
)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when hasGradingPolicy is true', () => {
|
||||
expect(validators.hasGradingPolicy(
|
||||
{ hasGradingPolicy: true, sumOfWeights: 1 },
|
||||
)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when hasGradingPolicy is false', () => {
|
||||
expect(validators.hasGradingPolicy(
|
||||
{ hasGradingPolicy: false, sumOfWeights: 1 },
|
||||
)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCertificate', () => {
|
||||
it('returns true when certificates are activated and course run has a certificate', () => {
|
||||
expect(validators.hasCertificate({ isActivated: true, hasCertificate: true }))
|
||||
.toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when certificates are not activated and course run has a certificate', () => {
|
||||
expect(validators.hasCertificate({ isActivated: false, hasCertificate: true }))
|
||||
.toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when certificates are activated and course run does not have a certificate', () => {
|
||||
expect(validators.hasCertificate({ isActivated: true, hasCertificate: false }))
|
||||
.toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when certificates are not activated and course run does not have a certificate', () => {
|
||||
expect(validators.hasCertificate({ isActivated: false, hasCertificate: false }))
|
||||
.toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDates', () => {
|
||||
it('returns true when course run has start date and end date', () => {
|
||||
expect(validators.hasDates({ hasStartDate: true, hasEndDate: true })).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when course run has no start date and end date', () => {
|
||||
expect(validators.hasDates({ hasStartDate: false, hasEndDate: true })).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when course run has start date and no end date', () => {
|
||||
expect(validators.hasDates({ hasStartDate: true, hasEndDate: false })).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when course run has no start date and no end date', () => {
|
||||
expect(validators.hasDates({ hasStartDate: false, hasEndDate: false })).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasAssignmentDeadlines', () => {
|
||||
it('returns true when a course run has start and end date and all assignments are within range', () => {
|
||||
expect(validators.hasAssignmentDeadlines(
|
||||
{
|
||||
assignmentsWithDatesBeforeStart: 0,
|
||||
assignmentsWithDatesAfterEnd: 0,
|
||||
assignmentsWithOraDatesAfterEnd: 0,
|
||||
assignmentsWithOraDatesBeforeStart: 0,
|
||||
},
|
||||
{
|
||||
hasStartDate: true,
|
||||
hasEndDate: true,
|
||||
},
|
||||
)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when a course run has no start and no end date', () => {
|
||||
expect(validators.hasAssignmentDeadlines(
|
||||
{},
|
||||
{
|
||||
hasStartDate: false,
|
||||
hasEndDate: false,
|
||||
},
|
||||
)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when a course has start and end date and no assignments', () => {
|
||||
expect(validators.hasAssignmentDeadlines(
|
||||
{
|
||||
totalNumber: 0,
|
||||
},
|
||||
{
|
||||
hasStartDate: true,
|
||||
hasEndDate: true,
|
||||
},
|
||||
)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when a course run has start and end date and assignments before start', () => {
|
||||
expect(validators.hasAssignmentDeadlines(
|
||||
{
|
||||
assignmentsWithDatesBeforeStart: ['test'],
|
||||
assignmentsWithDatesAfterEnd: 0,
|
||||
assignmentsWithOraDatesAfterEnd: 0,
|
||||
assignmentsWithOraDatesBeforeStart: 0,
|
||||
},
|
||||
{
|
||||
hasStartDate: true,
|
||||
hasEndDate: true,
|
||||
},
|
||||
)).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false when a course run has start and end date and assignments after end', () => {
|
||||
expect(validators.hasAssignmentDeadlines(
|
||||
{
|
||||
assignmentsWithDatesBeforeStart: 0,
|
||||
assignmentsWithDatesAfterEnd: ['test'],
|
||||
assignmentsWithOraDatesAfterEnd: 0,
|
||||
assignmentsWithOraDatesBeforeStart: 0,
|
||||
},
|
||||
{
|
||||
hasStartDate: true,
|
||||
hasEndDate: true,
|
||||
},
|
||||
)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
'returns false when a course run has start and end date and an ora with a date before start',
|
||||
() => {
|
||||
expect(validators.hasAssignmentDeadlines(
|
||||
{
|
||||
assignmentsWithDatesBeforeStart: 0,
|
||||
assignmentsWithDatesAfterEnd: 0,
|
||||
assignmentsWithOraDatesAfterEnd: 0,
|
||||
assignmentsWithOraDatesBeforeStart: ['test'],
|
||||
},
|
||||
{
|
||||
hasStartDate: true,
|
||||
hasEndDate: true,
|
||||
},
|
||||
)).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
'returns false when a course run has start and end date and an ora with a date after end',
|
||||
() => {
|
||||
expect(validators.hasAssignmentDeadlines(
|
||||
{
|
||||
assignmentsWithDatesBeforeStart: 0,
|
||||
assignmentsWithDatesAfterEnd: 0,
|
||||
assignmentsWithOraDatesAfterEnd: ['test'],
|
||||
assignmentsWithOraDatesBeforeStart: 0,
|
||||
},
|
||||
{
|
||||
hasStartDate: true,
|
||||
hasEndDate: true,
|
||||
},
|
||||
)).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
describe('hasShortVideoDuration', () => {
|
||||
it('returns true if course run has no videos', () => {
|
||||
expect(validators.hasShortVideoDuration({ totalNumber: 0 })).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true if course run videos have a median duration <= to 600', () => {
|
||||
expect(validators.hasShortVideoDuration({ totalNumber: 1, durations: { median: 100 } }))
|
||||
.toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true if course run videos have a median duration > to 600', () => {
|
||||
expect(validators.hasShortVideoDuration({ totalNumber: 10, durations: { median: 700 } }))
|
||||
.toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasMobileFriendlyVideos', () => {
|
||||
it('returns true if course run has no videos', () => {
|
||||
expect(validators.hasMobileFriendlyVideos({ totalNumber: 0 })).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true if course run videos are >= 90% mobile friendly', () => {
|
||||
expect(validators.hasMobileFriendlyVideos({ totalNumber: 10, numMobileEncoded: 9 }))
|
||||
.toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true if course run videos are < 90% mobile friendly', () => {
|
||||
expect(validators.hasMobileFriendlyVideos({ totalNumber: 10, numMobileEncoded: 8 }))
|
||||
.toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDiverseSequences', () => {
|
||||
it('returns true if < 20% of visible subsections have more than one block type', () => {
|
||||
expect(validators.hasDiverseSequences({ totalVisible: 10, numWithOneBlockType: 1 }))
|
||||
.toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false if no visible subsections', () => {
|
||||
expect(validators.hasDiverseSequences({ totalVisible: 0 })).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns false if >= 20% of visible subsections have more than one block type', () => {
|
||||
expect(validators.hasDiverseSequences({ totalVisible: 10, numWithOneBlockType: 3 }))
|
||||
.toEqual(false);
|
||||
});
|
||||
|
||||
it('return false if < 0 visible subsections', () => {
|
||||
expect(validators.hasDiverseSequences({ totalVisible: -1, numWithOneBlockType: 1 }))
|
||||
.toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasWeeklyHighlights', () => {
|
||||
it('returns true when course run has highlights enabled', () => {
|
||||
const data = { highlightsActiveForCourse: true, highlightsEnabled: true };
|
||||
expect(validators.hasWeeklyHighlights(data)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when course run has highlights enabled', () => {
|
||||
const data = { highlightsActiveForCourse: false, highlightsEnabled: false };
|
||||
expect(validators.hasWeeklyHighlights(data)).toEqual(false);
|
||||
|
||||
data.highlightsEnabled = true;
|
||||
data.highlightsActiveForCourse = false;
|
||||
expect(validators.hasWeeklyHighlights(data)).toEqual(false);
|
||||
|
||||
data.highlightsEnabled = false;
|
||||
data.highlightsActiveForCourse = true;
|
||||
expect(validators.hasWeeklyHighlights(data)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasShortUnitDepth', () => {
|
||||
it('returns true when course run has median number of blocks <= 3', () => {
|
||||
const units = {
|
||||
numBlocks: {
|
||||
median: 3,
|
||||
},
|
||||
};
|
||||
|
||||
expect(validators.hasShortUnitDepth(units)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when course run has median number of blocks > 3', () => {
|
||||
const units = {
|
||||
numBlocks: {
|
||||
median: 4,
|
||||
},
|
||||
};
|
||||
|
||||
expect(validators.hasShortUnitDepth(units)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasProctoringEscalationEmail', () => {
|
||||
it('returns true when the course has a proctoring escalation email', () => {
|
||||
const proctoring = { hasProctoringEscalationEmail: true };
|
||||
expect(validators.hasProctoringEscalationEmail(proctoring)).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns false when the course does not have a proctoring escalation email', () => {
|
||||
const proctoring = { hasProctoringEscalationEmail: false };
|
||||
expect(validators.hasProctoringEscalationEmail(proctoring)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { filters } from './courseChecklistData';
|
||||
|
||||
const getFilteredChecklist = (
|
||||
checklist,
|
||||
isSelfPaced,
|
||||
hasCertificatesEnabled,
|
||||
hasHighlightsEnabled,
|
||||
needsProctoringEscalationEmail,
|
||||
) => {
|
||||
let filteredCheckList;
|
||||
|
||||
if (isSelfPaced) {
|
||||
filteredCheckList = checklist.filter(data => data.pacingTypeFilter === filters.ALL
|
||||
|| data.pacingTypeFilter === filters.SELF_PACED);
|
||||
} else {
|
||||
filteredCheckList = checklist.filter(data => data.pacingTypeFilter === filters.ALL
|
||||
|| data.pacingTypeFilter === filters.INSTRUCTOR_PACED);
|
||||
}
|
||||
|
||||
filteredCheckList = filteredCheckList.filter(data => data.id !== 'certificate'
|
||||
|| hasCertificatesEnabled);
|
||||
|
||||
filteredCheckList = filteredCheckList.filter(data => data.id !== 'weeklyHighlights'
|
||||
|| hasHighlightsEnabled);
|
||||
|
||||
filteredCheckList = filteredCheckList.filter(data => data.id !== 'proctoringEmail'
|
||||
|| needsProctoringEscalationEmail);
|
||||
|
||||
return filteredCheckList;
|
||||
};
|
||||
|
||||
export default getFilteredChecklist;
|
||||
@@ -0,0 +1,149 @@
|
||||
import { filters } from './courseChecklistData';
|
||||
import getFilteredChecklist from './getFilteredChecklist';
|
||||
|
||||
const checklist = [
|
||||
{
|
||||
id: 'welcomeMessage',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'gradingPolicy',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'certificate',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'courseDates',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
{
|
||||
id: 'assignmentDeadlines',
|
||||
pacingTypeFilter: filters.INSTRUCTOR_PACED,
|
||||
},
|
||||
{
|
||||
id: 'weeklyHighlights',
|
||||
pacingTypeFilter: filters.SELF_PACED,
|
||||
},
|
||||
{
|
||||
id: 'proctoringEmail',
|
||||
pacingTypeFilter: filters.ALL,
|
||||
},
|
||||
];
|
||||
|
||||
let courseData;
|
||||
describe('getFilteredChecklist utility function', () => {
|
||||
beforeEach(() => {
|
||||
courseData = {
|
||||
isSelfPaced: true,
|
||||
hasCertificatesEnabled: true,
|
||||
hasHighlightsEnabled: true,
|
||||
needsProctoringEscalationEmail: true,
|
||||
};
|
||||
});
|
||||
it('returns only checklist items with filters ALL and SELF_PACED when isSelfPaced is true', () => {
|
||||
const filteredChecklist = getFilteredChecklist(
|
||||
checklist,
|
||||
courseData.isSelfPaced,
|
||||
courseData.hasCertificatesEnabled,
|
||||
courseData.hasHighlightsEnabled,
|
||||
courseData.needsProctoringEscalationEmail,
|
||||
);
|
||||
|
||||
filteredChecklist.forEach(((
|
||||
item => expect(item.pacingTypeFilter === filters.ALL
|
||||
|| item.pacingTypeFilter === filters.SELF_PACED)
|
||||
)));
|
||||
|
||||
expect(filteredChecklist.filter(item => item.pacingTypeFilter === filters.ALL).length)
|
||||
.toEqual(checklist.filter(item => item.pacingTypeFilter === filters.ALL).length);
|
||||
expect(filteredChecklist.filter(item => item.pacingTypeFilter === filters.SELF_PACED).length)
|
||||
.toEqual(checklist.filter(item => item.pacingTypeFilter === filters.SELF_PACED).length);
|
||||
});
|
||||
|
||||
it('returns only checklist items with filters ALL and INSTRUCTOR_PACED when isSelfPaced is false', () => {
|
||||
courseData.isSelfPaced = false;
|
||||
const filteredChecklist = getFilteredChecklist(
|
||||
checklist,
|
||||
courseData.isSelfPaced,
|
||||
courseData.hasCertificatesEnabled,
|
||||
courseData.hasHighlightsEnabled,
|
||||
courseData.needsProctoringEscalationEmail,
|
||||
);
|
||||
|
||||
filteredChecklist.forEach(((
|
||||
item => expect(item.pacingTypeFilter === filters.ALL
|
||||
|| item.pacingTypeFilter === filters.INSTRUCTOR_PACED)
|
||||
)));
|
||||
|
||||
expect(filteredChecklist.filter(item => item.pacingTypeFilter === filters.ALL).length)
|
||||
.toEqual(checklist.filter(item => item.pacingTypeFilter === filters.ALL).length);
|
||||
expect(filteredChecklist
|
||||
.filter(item => item.pacingTypeFilter === filters.INSTRUCTOR_PACED).length)
|
||||
.toEqual(checklist.filter(item => item.pacingTypeFilter === filters.INSTRUCTOR_PACED).length);
|
||||
});
|
||||
|
||||
it('excludes certificates when they are disabled', () => {
|
||||
let filteredChecklist = getFilteredChecklist(
|
||||
checklist,
|
||||
courseData.isSelfPaced,
|
||||
courseData.hasCertificatesEnabled,
|
||||
courseData.hasHighlightsEnabled,
|
||||
courseData.needsProctoringEscalationEmail,
|
||||
);
|
||||
expect(checklist.filter(item => item.id === 'certificate').length).toEqual(1);
|
||||
|
||||
courseData.hasCertificatesEnabled = false;
|
||||
filteredChecklist = getFilteredChecklist(
|
||||
checklist,
|
||||
courseData.isSelfPaced,
|
||||
courseData.hasCertificatesEnabled,
|
||||
courseData.hasHighlightsEnabled,
|
||||
courseData.needsProctoringEscalationEmail,
|
||||
);
|
||||
expect(filteredChecklist.filter(item => item.id === 'certificate').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('excludes weekly highlights when they are disabled', () => {
|
||||
let filteredChecklist = getFilteredChecklist(
|
||||
checklist,
|
||||
courseData.isSelfPaced,
|
||||
courseData.hasCertificatesEnabled,
|
||||
courseData.hasHighlightsEnabled,
|
||||
courseData.needsProctoringEscalationEmail,
|
||||
);
|
||||
expect(filteredChecklist.filter(item => item.id === 'weeklyHighlights').length).toEqual(1);
|
||||
|
||||
courseData.hasHighlightsEnabled = false;
|
||||
filteredChecklist = getFilteredChecklist(
|
||||
checklist,
|
||||
courseData.isSelfPaced,
|
||||
courseData.hasCertificatesEnabled,
|
||||
courseData.hasHighlightsEnabled,
|
||||
courseData.needsProctoringEscalationEmail,
|
||||
);
|
||||
expect(filteredChecklist.filter(item => item.id === 'weeklyHighlights').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('excludes proctoring escalation email when not needed', () => {
|
||||
let filteredChecklist = getFilteredChecklist(
|
||||
checklist,
|
||||
courseData.isSelfPaced,
|
||||
courseData.hasCertificatesEnabled,
|
||||
courseData.hasHighlightsEnabled,
|
||||
courseData.needsProctoringEscalationEmail,
|
||||
);
|
||||
expect(filteredChecklist.filter(item => item.id === 'proctoringEmail').length).toEqual(1);
|
||||
|
||||
courseData.needsProctoringEscalationEmail = false;
|
||||
filteredChecklist = getFilteredChecklist(
|
||||
checklist,
|
||||
courseData.isSelfPaced,
|
||||
courseData.hasCertificatesEnabled,
|
||||
courseData.hasHighlightsEnabled,
|
||||
courseData.needsProctoringEscalationEmail,
|
||||
);
|
||||
expect(filteredChecklist.filter(item => item.id === 'proctoringEmail').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import * as healthValidators from './courseChecklistValidators';
|
||||
|
||||
const getValidatedValue = (data, id) => {
|
||||
switch (id) {
|
||||
case 'welcomeMessage':
|
||||
return healthValidators.hasWelcomeMessage(data.updates);
|
||||
case 'gradingPolicy':
|
||||
return healthValidators.hasGradingPolicy(data.grades);
|
||||
case 'certificate':
|
||||
return healthValidators.hasCertificate(data.certificates);
|
||||
case 'courseDates':
|
||||
return healthValidators.hasDates(data.dates);
|
||||
case 'assignmentDeadlines':
|
||||
return healthValidators.hasAssignmentDeadlines(data.assignments, data.dates);
|
||||
case 'videoDuration':
|
||||
return healthValidators.hasShortVideoDuration(data.videos);
|
||||
case 'mobileFriendlyVideo':
|
||||
return healthValidators.hasMobileFriendlyVideos(data.videos);
|
||||
case 'diverseSequences':
|
||||
return healthValidators.hasDiverseSequences(data.subsections);
|
||||
case 'weeklyHighlights':
|
||||
return healthValidators.hasWeeklyHighlights(data.sections);
|
||||
case 'unitDepth':
|
||||
return healthValidators.hasShortUnitDepth(data.units);
|
||||
case 'proctoringEmail':
|
||||
return healthValidators.hasProctoringEscalationEmail(data.proctoring);
|
||||
default:
|
||||
throw new Error(`Unknown validator ${id}.`);
|
||||
}
|
||||
};
|
||||
|
||||
export default getValidatedValue;
|
||||
@@ -0,0 +1,166 @@
|
||||
import * as validators from './courseChecklistValidators';
|
||||
import getValidatedValue from './getValidatedValue';
|
||||
|
||||
describe('getValidatedValue utility function', () => {
|
||||
const localValidators = validators;
|
||||
it('welcome message', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasWelcomeMessage = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
updates: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'welcomeMessage');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('grading policy', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasGradingPolicy = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
grades: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'gradingPolicy');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('certificate', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasCertificate = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
certificates: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'certificate');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('course dates', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasDates = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
dates: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'courseDates');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('assignment deadlines', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasAssignmentDeadlines = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
assignments: {},
|
||||
dates: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'assignmentDeadlines');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('video duration', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasShortVideoDuration = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
videos: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'videoDuration');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('mobile friendly video', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasMobileFriendlyVideos = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
videos: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'mobileFriendlyVideo');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('diverse sequences', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasDiverseSequences = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
subsections: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'diverseSequences');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('weekly highlights', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasWeeklyHighlights = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
sections: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'weeklyHighlights');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('unit depth', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasShortUnitDepth = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
units: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'unitDepth');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('proctoring email', () => {
|
||||
const spy = jest.fn();
|
||||
localValidators.hasProctoringEscalationEmail = spy;
|
||||
|
||||
const props = {
|
||||
data: {
|
||||
proctoring: {},
|
||||
},
|
||||
};
|
||||
|
||||
getValidatedValue(props, 'proctoringEmail');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('other', () => {
|
||||
const sampleID = 'edX';
|
||||
expect(() => getValidatedValue({}, sampleID)).toThrow(Error);
|
||||
expect(() => getValidatedValue({}, sampleID)).toThrow(`Unknown validator ${sampleID}`);
|
||||
});
|
||||
});
|
||||
96
src/course-checklist/CourseChecklist.jsx
Normal file
96
src/course-checklist/CourseChecklist.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Container, Stack } from '@openedx/paragon';
|
||||
|
||||
import { useModel } from '../generic/model-store';
|
||||
import SubHeader from '../generic/sub-header/SubHeader';
|
||||
import messages from './messages';
|
||||
import AriaLiveRegion from './AriaLiveRegion';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import ChecklistSection from './ChecklistSection';
|
||||
import { fetchCourseLaunchQuery, fetchCourseBestPracticesQuery } from './data/thunks';
|
||||
import getUpdateLinks from './utils';
|
||||
|
||||
const CourseChecklist = ({
|
||||
courseId,
|
||||
// injected,
|
||||
intl,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const courseDetails = useModel('courseDetails', courseId);
|
||||
const enableQuality = getConfig().ENABLE_CHECKLIST_QUALITY === 'true';
|
||||
const updateLinks = getUpdateLinks(courseId);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseLaunchQuery({ courseId }));
|
||||
dispatch(fetchCourseBestPracticesQuery({ courseId }));
|
||||
}, [courseId]);
|
||||
|
||||
const {
|
||||
loadingStatus,
|
||||
launchData,
|
||||
bestPracticeData,
|
||||
} = useSelector(state => state.courseChecklist);
|
||||
|
||||
const { bestPracticeChecklistLoadingStatus, launchChecklistLoadingStatus } = loadingStatus;
|
||||
|
||||
const isCourseLaunchChecklistLoading = bestPracticeChecklistLoadingStatus === RequestStatus.IN_PROGRESS;
|
||||
const isCourseBestPracticeChecklistLoading = launchChecklistLoadingStatus === RequestStatus.IN_PROGRESS;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(messages.pageTitle, {
|
||||
headingTitle: intl.formatMessage(messages.headingTitle),
|
||||
courseName: courseDetails?.name,
|
||||
siteName: process.env.SITE_NAME,
|
||||
})}
|
||||
</title>
|
||||
</Helmet>
|
||||
<Container size="xl" className="p-4 pt-4.5">
|
||||
<SubHeader
|
||||
title={intl.formatMessage(messages.headingTitle)}
|
||||
subtitle={intl.formatMessage(messages.headingSubtitle)}
|
||||
/>
|
||||
<AriaLiveRegion
|
||||
{...{
|
||||
isCourseLaunchChecklistLoading,
|
||||
isCourseBestPracticeChecklistLoading,
|
||||
enableQuality,
|
||||
}}
|
||||
/>
|
||||
<Stack gap={4}>
|
||||
<ChecklistSection
|
||||
dataHeading={intl.formatMessage(messages.launchChecklistLabel)}
|
||||
data={launchData}
|
||||
idPrefix="launchChecklist"
|
||||
isLoading={isCourseLaunchChecklistLoading}
|
||||
updateLinks={updateLinks}
|
||||
/>
|
||||
{enableQuality && (
|
||||
<ChecklistSection
|
||||
dataHeading={intl.formatMessage(messages.bestPracticesChecklistLabel)}
|
||||
data={bestPracticeData}
|
||||
idPrefix="bestPracticesChecklist"
|
||||
isLoading={isCourseBestPracticeChecklistLoading}
|
||||
updateLinks={updateLinks}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CourseChecklist.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CourseChecklist);
|
||||
1
src/course-checklist/CourseChecklist.scss
Normal file
1
src/course-checklist/CourseChecklist.scss
Normal file
@@ -0,0 +1 @@
|
||||
@import "./ChecklistSection/ChecklistSection";
|
||||
153
src/course-checklist/CourseChecklist.test.jsx
Normal file
153
src/course-checklist/CourseChecklist.test.jsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import {
|
||||
render,
|
||||
waitFor,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { getConfig, setConfig, initializeMockApp } from '@edx/frontend-platform';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import initializeStore from '../store';
|
||||
import { RequestStatus } from '../data/constants';
|
||||
import { executeThunk } from '../utils';
|
||||
import { getCourseLaunchApiUrl, getCourseBestPracticesApiUrl } from './data/api';
|
||||
import { fetchCourseLaunchQuery, fetchCourseBestPracticesQuery } from './data/thunks';
|
||||
import {
|
||||
courseId,
|
||||
initialState,
|
||||
generateCourseLaunchData,
|
||||
generateCourseBestPracticesData,
|
||||
} from './factories/mockApiResponses';
|
||||
import messages from './messages';
|
||||
import CourseChecklist from './index';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
|
||||
const renderComponent = () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AppProvider store={store}>
|
||||
<CourseChecklist {...{ courseId }} />
|
||||
</AppProvider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
const mockStore = async (status) => {
|
||||
axiosMock.onGet(getCourseLaunchApiUrl(courseId)).reply(status, generateCourseLaunchData());
|
||||
axiosMock.onGet(getCourseBestPracticesApiUrl(courseId)).reply(status, generateCourseBestPracticesData());
|
||||
|
||||
await executeThunk(fetchCourseLaunchQuery(courseId), store.dispatch);
|
||||
await executeThunk(fetchCourseBestPracticesQuery(courseId), store.dispatch);
|
||||
};
|
||||
|
||||
describe('CourseChecklistPage', () => {
|
||||
beforeEach(async () => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: false,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
store = initializeStore(initialState);
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
describe('renders', () => {
|
||||
describe('if enable_quality prop is true', () => {
|
||||
it('two checklist components ', () => {
|
||||
renderComponent();
|
||||
mockStore(200);
|
||||
|
||||
expect(screen.getByText(messages.launchChecklistLabel.defaultMessage)).toBeVisible();
|
||||
|
||||
expect(screen.getByText(messages.bestPracticesChecklistLabel.defaultMessage)).toBeVisible();
|
||||
});
|
||||
|
||||
describe('an aria-live region with', () => {
|
||||
it('an aria-live region', () => {
|
||||
renderComponent();
|
||||
const ariaLiveRegion = screen.getByRole('status');
|
||||
|
||||
expect(ariaLiveRegion).toBeDefined();
|
||||
|
||||
expect(ariaLiveRegion.classList.contains('sr-only')).toBe(true);
|
||||
});
|
||||
|
||||
it('correct content when the launch checklist has loaded', async () => {
|
||||
renderComponent();
|
||||
mockStore(404);
|
||||
await waitFor(() => {
|
||||
const { launchChecklistStatus } = store.getState().courseChecklist.loadingStatus;
|
||||
|
||||
expect(launchChecklistStatus).not.toEqual(RequestStatus.SUCCESSFUL);
|
||||
|
||||
expect(screen.getByText(messages.launchChecklistDoneLoadingLabel.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('correct content when the best practices checklist is loading', async () => {
|
||||
renderComponent();
|
||||
mockStore(404);
|
||||
await waitFor(() => {
|
||||
const { bestPracticeChecklistStatus } = store.getState().courseChecklist.loadingStatus;
|
||||
|
||||
expect(bestPracticeChecklistStatus).not.toEqual(RequestStatus.IN_PROGRESS);
|
||||
|
||||
expect(
|
||||
screen.getByText(messages.bestPracticesChecklistDoneLoadingLabel.defaultMessage),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('if enable_quality prop is false', () => {
|
||||
beforeEach(() => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_CHECKLIST_QUALITY: 'false',
|
||||
});
|
||||
});
|
||||
|
||||
it('one checklist components ', () => {
|
||||
renderComponent();
|
||||
mockStore(200);
|
||||
|
||||
expect(screen.getByText(messages.launchChecklistLabel.defaultMessage)).toBeVisible();
|
||||
|
||||
expect(screen.queryByText(messages.bestPracticesChecklistLabel.defaultMessage)).toBeNull();
|
||||
});
|
||||
|
||||
describe('an aria-live region with', () => {
|
||||
it('correct content when the launch checklist has loaded', async () => {
|
||||
renderComponent();
|
||||
mockStore(404);
|
||||
await waitFor(() => {
|
||||
const { launchChecklistStatus } = store.getState().courseChecklist.loadingStatus;
|
||||
|
||||
expect(launchChecklistStatus).not.toEqual(RequestStatus.SUCCESSFUL);
|
||||
|
||||
expect(screen.getByText(messages.launchChecklistDoneLoadingLabel.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('correct content when the best practices checklist is loading', async () => {
|
||||
renderComponent();
|
||||
mockStore(404);
|
||||
await waitFor(() => {
|
||||
const { bestPracticeChecklistStatus } = store.getState().courseChecklist.loadingStatus;
|
||||
|
||||
expect(bestPracticeChecklistStatus).not.toEqual(RequestStatus.IN_PROGRESS);
|
||||
|
||||
expect(screen.queryByText(messages.bestPracticesChecklistDoneLoadingLabel.defaultMessage)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
64
src/course-checklist/data/api.js
Normal file
64
src/course-checklist/data/api.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// @ts-check
|
||||
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
|
||||
|
||||
export const getCourseBestPracticesApiUrl = ({
|
||||
courseId,
|
||||
excludeGraded,
|
||||
all,
|
||||
}) => `${getApiBaseUrl()}/api/courses/v1/quality/${courseId}/?exclude_graded=${excludeGraded}&all=${all}`;
|
||||
|
||||
export const getCourseLaunchApiUrl = ({
|
||||
courseId,
|
||||
gradedOnly,
|
||||
validateOras,
|
||||
all,
|
||||
}) => `${getApiBaseUrl()}/api/courses/v1/validation/${courseId}/?graded_only=${gradedOnly}&validate_oras=${validateOras}&all=${all}`;
|
||||
|
||||
/**
|
||||
* Get course best practices.
|
||||
* @param {{courseId: string, excludeGraded: boolean, all: boolean}} options
|
||||
* @returns {Promise<{isSelfPaced: boolean, sections: any, subsection: any, units: any, videos: any }>}
|
||||
*/
|
||||
export async function getCourseBestPractices({
|
||||
courseId,
|
||||
excludeGraded,
|
||||
all,
|
||||
}) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getCourseBestPracticesApiUrl({ courseId, excludeGraded, all }));
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
|
||||
/** @typedef {object} courseLaunchData
|
||||
* @property {boolean} isSelfPaced
|
||||
* @property {object} dates
|
||||
* @property {object} assignments
|
||||
* @property {object} grades
|
||||
* @property {number} grades.sum_of_weights
|
||||
* @property {object} certificates
|
||||
* @property {object} updates
|
||||
* @property {object} proctoring
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get course launch.
|
||||
* @param {{courseId: string, gradedOnly: boolean, validateOras: boolean, all: boolean}} options
|
||||
* @returns {Promise<courseLaunchData>}
|
||||
*/
|
||||
export async function getCourseLaunch({
|
||||
courseId,
|
||||
gradedOnly,
|
||||
validateOras,
|
||||
all,
|
||||
}) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(getCourseLaunchApiUrl({
|
||||
courseId, gradedOnly, validateOras, all,
|
||||
}));
|
||||
|
||||
return camelCaseObject(data);
|
||||
}
|
||||
41
src/course-checklist/data/slice.js
Normal file
41
src/course-checklist/data/slice.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'courseChecklist',
|
||||
initialState: {
|
||||
loadingStatus: {
|
||||
launchChecklistStatus: RequestStatus.IN_PROGRESS,
|
||||
bestPracticeChecklistStatus: RequestStatus.IN_PROGRESS,
|
||||
},
|
||||
launchData: {},
|
||||
bestPracticeData: {},
|
||||
},
|
||||
reducers: {
|
||||
fetchLaunchChecklistSuccess: (state, { payload }) => {
|
||||
state.launchData = payload.data;
|
||||
},
|
||||
updateLaunchChecklistStatus: (state, { payload }) => {
|
||||
state.loadingStatus.launchChecklistStatus = payload.status;
|
||||
},
|
||||
fetchBestPracticeChecklistSuccess: (state, { payload }) => {
|
||||
state.bestPracticeData = payload.data;
|
||||
},
|
||||
updateBestPracticeChecklisttStatus: (state, { payload }) => {
|
||||
state.loadingStatus.bestPracticeChecklistStatus = payload.status;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
fetchLaunchChecklistSuccess,
|
||||
updateLaunchChecklistStatus,
|
||||
fetchBestPracticeChecklistSuccess,
|
||||
updateBestPracticeChecklisttStatus,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
reducer,
|
||||
} = slice;
|
||||
46
src/course-checklist/data/thunks.js
Normal file
46
src/course-checklist/data/thunks.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import {
|
||||
getCourseBestPractices,
|
||||
getCourseLaunch,
|
||||
} from './api';
|
||||
import {
|
||||
fetchLaunchChecklistSuccess,
|
||||
updateLaunchChecklistStatus,
|
||||
fetchBestPracticeChecklistSuccess,
|
||||
updateBestPracticeChecklisttStatus,
|
||||
} from './slice';
|
||||
|
||||
export function fetchCourseLaunchQuery({
|
||||
courseId,
|
||||
gradedOnly = true,
|
||||
validateOras = true,
|
||||
all = true,
|
||||
}) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const data = await getCourseLaunch({
|
||||
courseId, gradedOnly, validateOras, all,
|
||||
});
|
||||
dispatch(fetchLaunchChecklistSuccess({ data }));
|
||||
dispatch(updateLaunchChecklistStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateLaunchChecklistStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchCourseBestPracticesQuery({
|
||||
courseId,
|
||||
excludeGraded = true,
|
||||
all = true,
|
||||
}) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const data = await getCourseBestPractices({ courseId, excludeGraded, all });
|
||||
dispatch(fetchBestPracticeChecklistSuccess({ data }));
|
||||
dispatch(updateBestPracticeChecklisttStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
} catch (error) {
|
||||
dispatch(updateBestPracticeChecklisttStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
104
src/course-checklist/factories/mockApiResponses.jsx
Normal file
104
src/course-checklist/factories/mockApiResponses.jsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
|
||||
export const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
|
||||
export const initialState = {
|
||||
courseDetail: {
|
||||
courseId,
|
||||
status: 'sucessful',
|
||||
},
|
||||
courseChecklist: {
|
||||
loadingStatus: {
|
||||
launchChecklistStatus: RequestStatus.IN_PROGRESS,
|
||||
bestPracticeChecklistStatus: RequestStatus.IN_PROGRESS,
|
||||
},
|
||||
launchData: {},
|
||||
bestPracticesData: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const generateCourseBestPracticesData = () => ({
|
||||
is_self_paced: false,
|
||||
sections: {
|
||||
total_number: 2,
|
||||
total_visible: 2,
|
||||
number_with_highlights: 0,
|
||||
highlights_active_for_course: false,
|
||||
highlights_enabled: true,
|
||||
},
|
||||
subsections: {
|
||||
total_visible: 1,
|
||||
num_with_one_block_type: 1,
|
||||
num_block_types: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
mean: 1.0,
|
||||
median: 1.0,
|
||||
mode: 1,
|
||||
},
|
||||
},
|
||||
units: {
|
||||
total_visible: 1,
|
||||
num_blocks: {
|
||||
min: 1,
|
||||
max: 1,
|
||||
mean: 1.0,
|
||||
median: 1.0,
|
||||
mode: 1,
|
||||
},
|
||||
},
|
||||
videos: {
|
||||
total_number: 10,
|
||||
num_mobile_encoded: 5,
|
||||
num_with_val_id: 10,
|
||||
durations: {
|
||||
min: 9.409,
|
||||
max: 168.001,
|
||||
mean: 41.0,
|
||||
median: 9.0,
|
||||
mode: 9.409,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const generateCourseLaunchData = () => ({
|
||||
is_self_paced: false,
|
||||
dates: {
|
||||
has_start_date: true,
|
||||
has_end_date: true,
|
||||
},
|
||||
assignments: {
|
||||
total_number: 2,
|
||||
total_visible: 2,
|
||||
assignments_with_dates_before_start: [],
|
||||
assignments_with_dates_after_end: [
|
||||
{
|
||||
id: 'block-v1',
|
||||
display_name: 'Subsection',
|
||||
},
|
||||
],
|
||||
assignments_with_ora_dates_before_start: [
|
||||
{
|
||||
id: 'block-v2',
|
||||
display_name: 'ORA subsection',
|
||||
},
|
||||
],
|
||||
assignments_with_ora_dates_after_end: [],
|
||||
},
|
||||
grades: {
|
||||
has_grading_policy: true,
|
||||
sum_of_weights: 0.9500000000000001,
|
||||
},
|
||||
certificates: {
|
||||
is_activated: false,
|
||||
has_certificate: false,
|
||||
is_enabled: true,
|
||||
},
|
||||
updates: {
|
||||
has_update: true,
|
||||
},
|
||||
proctoring: {
|
||||
needs_proctoring_escalation_email: true,
|
||||
has_proctoring_escalation_email: false,
|
||||
},
|
||||
});
|
||||
3
src/course-checklist/index.js
Normal file
3
src/course-checklist/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import CourseChecklist from './CourseChecklist';
|
||||
|
||||
export default CourseChecklist;
|
||||
49
src/course-checklist/messages.js
Normal file
49
src/course-checklist/messages.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
pageTitle: {
|
||||
id: 'course-authoring.export.page.title',
|
||||
defaultMessage: '{headingTitle} | {courseName} | {siteName}',
|
||||
},
|
||||
headingTitle: {
|
||||
id: 'course-authoring.course-checklist.heading.title',
|
||||
defaultMessage: 'Checklists',
|
||||
description: 'Header text for the Checklist page',
|
||||
},
|
||||
headingSubtitle: {
|
||||
id: 'course-authoring.course-checklist.heading.subtitle',
|
||||
defaultMessage: 'Tools',
|
||||
},
|
||||
launchChecklistLabel: {
|
||||
id: 'launchChecklistLabel',
|
||||
defaultMessage: 'Launch checklist',
|
||||
description: 'Header text for a checklist that describes actions to have completed before a course should launch',
|
||||
},
|
||||
bestPracticesChecklistLabel: {
|
||||
id: 'bestPracticesChecklistLabel',
|
||||
defaultMessage: 'Best practices checklist',
|
||||
description: 'Header text for a checklist that describes best practices for a course',
|
||||
},
|
||||
launchChecklistLoadingLabel: {
|
||||
id: 'doneLoadingChecklistStatusLabel',
|
||||
defaultMessage: 'Launch Checklist data is loading',
|
||||
description: 'Label telling the user that the Launch Checklist is loading',
|
||||
},
|
||||
launchChecklistDoneLoadingLabel: {
|
||||
id: 'launchChecklistDoneLoadingLabel',
|
||||
defaultMessage: 'Launch Checklist data is done loading',
|
||||
description: 'Label telling the user that the Launch Checklist is done loading',
|
||||
},
|
||||
bestPracticesChecklistLoadingLabel: {
|
||||
id: 'bestPracticesChecklistLoadingLabel',
|
||||
defaultMessage: 'Best Practices Checklist data is loading',
|
||||
description: 'Label telling the user that the Best Practices Checklist is loading',
|
||||
},
|
||||
bestPracticesChecklistDoneLoadingLabel: {
|
||||
id: 'bestPracticesChecklistDoneLoadingLabel',
|
||||
defaultMessage: 'Best Practices Checklist data is done loading',
|
||||
description: 'Label telling the user that the Best Practices Checklist is done loading',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
12
src/course-checklist/utils.js
Normal file
12
src/course-checklist/utils.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
const getUpdateLinks = (courseId) => ({
|
||||
welcomeMessage: `${getConfig().STUDIO_BASE_URL}/course_info/${courseId}`,
|
||||
gradingPolicy: `${getConfig().STUDIO_BASE_URL}/settings/grading/${courseId}`,
|
||||
certificate: `${getConfig().STUDIO_BASE_URL}/certificates/${courseId}`,
|
||||
courseDates: `${getConfig().STUDIO_BASE_URL}/settings/details/${courseId}#schedule`,
|
||||
proctoringEmail: 'pages-and-resources/proctoring/settings',
|
||||
outline: `${getConfig().STUDIO_BASE_URL}/course/${courseId}`,
|
||||
});
|
||||
|
||||
export default getUpdateLinks;
|
||||
@@ -123,6 +123,7 @@ initialize({
|
||||
ENABLE_UNIT_PAGE: process.env.ENABLE_UNIT_PAGE || 'false',
|
||||
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN: process.env.ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN || 'false',
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: process.env.ENABLE_TAGGING_TAXONOMY_PAGES || 'false',
|
||||
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
|
||||
}, 'CourseAuthoringConfig');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -22,3 +22,4 @@
|
||||
@import "content-tags-drawer/TagBubble";
|
||||
@import "course-outline/CourseOutline";
|
||||
@import "course-unit/CourseUnit";
|
||||
@import "course-checklist/CourseChecklist";
|
||||
|
||||
@@ -46,6 +46,8 @@ mergeConfig({
|
||||
CALCULATOR_HELP_URL: process.env.CALCULATOR_HELP_URL || null,
|
||||
ENABLE_PROGRESS_GRAPH_SETTINGS: process.env.ENABLE_PROGRESS_GRAPH_SETTINGS || 'false',
|
||||
ENABLE_TEAM_TYPE_SETTING: process.env.ENABLE_TEAM_TYPE_SETTING === 'true',
|
||||
ENABLE_CHECKLIST_QUALITY: process.env.ENABLE_CHECKLIST_QUALITY || 'true',
|
||||
STUDIO_BASE_URL: process.env.STUDIO_BASE_URL || null,
|
||||
}, 'CourseAuthoringConfig');
|
||||
|
||||
class ResizeObserver {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { reducer as courseImportReducer } from './import-page/data/slice';
|
||||
import { reducer as videosReducer } from './files-and-videos/videos-page/data/slice';
|
||||
import { reducer as courseOutlineReducer } from './course-outline/data/slice';
|
||||
import { reducer as courseUnitReducer } from './course-unit/data/slice';
|
||||
import { reducer as courseChecklistReducer } from './course-checklist/data/slice';
|
||||
import { reducer as accessibilityPageReducer } from './accessibility-page/data/slice';
|
||||
|
||||
export default function initializeStore(preloadedState = undefined) {
|
||||
@@ -50,6 +51,7 @@ export default function initializeStore(preloadedState = undefined) {
|
||||
videos: videosReducer,
|
||||
courseOutline: courseOutlineReducer,
|
||||
courseUnit: courseUnitReducer,
|
||||
courseChecklist: courseChecklistReducer,
|
||||
accessibilityPage: accessibilityPageReducer,
|
||||
},
|
||||
preloadedState,
|
||||
|
||||
Reference in New Issue
Block a user