feat: [FC-0044] Unit page - Manage access modal (unit & xblocks) (#901)
* feat: [FC-0044] Unit page - Manage access modal (unit & xblocks) * fix: add message description
This commit is contained in:
276
src/generic/configure-modal/AdvancedTab.jsx
Normal file
276
src/generic/configure-modal/AdvancedTab.jsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { Alert, Form, Hyperlink } from '@openedx/paragon';
|
||||
import {
|
||||
Warning as WarningIcon,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
import PrereqSettings from './PrereqSettings';
|
||||
|
||||
const AdvancedTab = ({
|
||||
values,
|
||||
setFieldValue,
|
||||
prereqs,
|
||||
releasedToStudents,
|
||||
wasExamEverLinkedWithExternal,
|
||||
enableProctoredExams,
|
||||
supportsOnboarding,
|
||||
wasProctoredExam,
|
||||
showReviewRules,
|
||||
onlineProctoringRules,
|
||||
}) => {
|
||||
const {
|
||||
isTimeLimited,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
defaultTimeLimitMinutes,
|
||||
examReviewRules,
|
||||
} = values;
|
||||
let examTypeValue = 'none';
|
||||
|
||||
if (isTimeLimited && isProctoredExam) {
|
||||
if (isOnboardingExam) {
|
||||
examTypeValue = 'onboardingExam';
|
||||
} else if (isPracticeExam) {
|
||||
examTypeValue = 'practiceExam';
|
||||
} else {
|
||||
examTypeValue = 'proctoredExam';
|
||||
}
|
||||
} else if (isTimeLimited) {
|
||||
examTypeValue = 'timed';
|
||||
}
|
||||
|
||||
const formatHour = (hour) => {
|
||||
const hh = Math.floor(hour / 60);
|
||||
const mm = hour % 60;
|
||||
let hhs = `${hh}`;
|
||||
let mms = `${mm}`;
|
||||
if (hh < 10) {
|
||||
hhs = `0${hh}`;
|
||||
}
|
||||
if (mm < 10) {
|
||||
mms = `0${mm}`;
|
||||
}
|
||||
if (Number.isNaN(hh)) {
|
||||
hhs = '00';
|
||||
}
|
||||
if (Number.isNaN(mm)) {
|
||||
mms = '00';
|
||||
}
|
||||
return `${hhs}:${mms}`;
|
||||
};
|
||||
|
||||
const [timeLimit, setTimeLimit] = useState(formatHour(defaultTimeLimitMinutes));
|
||||
const showReviewRulesDiv = showReviewRules && isProctoredExam && !isPracticeExam && !isOnboardingExam;
|
||||
|
||||
const handleChange = (e) => {
|
||||
if (e.target.value === 'timed') {
|
||||
setFieldValue('isTimeLimited', true);
|
||||
setFieldValue('isOnboardingExam', false);
|
||||
setFieldValue('isPracticeExam', false);
|
||||
setFieldValue('isProctoredExam', false);
|
||||
} else if (e.target.value === 'onboardingExam') {
|
||||
setFieldValue('isOnboardingExam', true);
|
||||
setFieldValue('isProctoredExam', true);
|
||||
setFieldValue('isTimeLimited', true);
|
||||
setFieldValue('isPracticeExam', false);
|
||||
} else if (e.target.value === 'practiceExam') {
|
||||
setFieldValue('isPracticeExam', true);
|
||||
setFieldValue('isProctoredExam', true);
|
||||
setFieldValue('isTimeLimited', true);
|
||||
setFieldValue('isOnboardingExam', false);
|
||||
} else if (e.target.value === 'proctoredExam') {
|
||||
setFieldValue('isProctoredExam', true);
|
||||
setFieldValue('isTimeLimited', true);
|
||||
setFieldValue('isOnboardingExam', false);
|
||||
setFieldValue('isPracticeExam', false);
|
||||
} else {
|
||||
setFieldValue('isTimeLimited', false);
|
||||
setFieldValue('isOnboardingExam', false);
|
||||
setFieldValue('isPracticeExam', false);
|
||||
setFieldValue('isProctoredExam', false);
|
||||
}
|
||||
};
|
||||
|
||||
const setCurrentTimeLimit = (event) => {
|
||||
const { validity: { valid } } = event.target;
|
||||
let { value } = event.target;
|
||||
value = value.trim();
|
||||
if (value && valid) {
|
||||
const minutes = moment.duration(value).asMinutes();
|
||||
setFieldValue('defaultTimeLimitMinutes', minutes);
|
||||
}
|
||||
setTimeLimit(value);
|
||||
};
|
||||
|
||||
const renderAlerts = () => {
|
||||
const proctoredExamLockedIn = releasedToStudents && wasExamEverLinkedWithExternal;
|
||||
return (
|
||||
<>
|
||||
{proctoredExamLockedIn && !wasProctoredExam && (
|
||||
<Alert variant="warning" icon={WarningIcon}>
|
||||
<FormattedMessage {...messages.proctoredExamLockedAndisNotProctoredExamAlert} />
|
||||
</Alert>
|
||||
)}
|
||||
{proctoredExamLockedIn && wasProctoredExam && (
|
||||
<Alert variant="warning" icon={WarningIcon}>
|
||||
<FormattedMessage {...messages.proctoredExamLockedAndisProctoredExamAlert} />
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.setSpecialExam} /></h5>
|
||||
<hr />
|
||||
<Form.RadioSet
|
||||
name="specialExam"
|
||||
onChange={handleChange}
|
||||
value={examTypeValue}
|
||||
>
|
||||
{renderAlerts()}
|
||||
<Form.Radio value="none">
|
||||
<FormattedMessage {...messages.none} />
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
value="timed"
|
||||
description={<FormattedMessage {...messages.timedDescription} />}
|
||||
controlClassName="mw-1-25rem"
|
||||
>
|
||||
<FormattedMessage {...messages.timed} />
|
||||
</Form.Radio>
|
||||
{enableProctoredExams && (
|
||||
<>
|
||||
<Form.Radio
|
||||
value="proctoredExam"
|
||||
description={<FormattedMessage {...messages.proctoredExamDescription} />}
|
||||
controlClassName="mw-1-25rem"
|
||||
>
|
||||
<FormattedMessage {...messages.proctoredExam} />
|
||||
</Form.Radio>
|
||||
{supportsOnboarding ? (
|
||||
<Form.Radio
|
||||
description={<FormattedMessage {...messages.onboardingExamDescription} />}
|
||||
value="onboardingExam"
|
||||
controlClassName="mw-1-25rem"
|
||||
>
|
||||
<FormattedMessage {...messages.onboardingExam} />
|
||||
</Form.Radio>
|
||||
) : (
|
||||
<Form.Radio
|
||||
value="practiceExam"
|
||||
controlClassName="mw-1-25rem"
|
||||
description={<FormattedMessage {...messages.practiceExamDescription} />}
|
||||
>
|
||||
<FormattedMessage {...messages.practiceExam} />
|
||||
</Form.Radio>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form.RadioSet>
|
||||
{ isTimeLimited && (
|
||||
<div className="mt-3" data-testid="advanced-tab-hours-picker-wrapper">
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage {...messages.timeAllotted} />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
onChange={setCurrentTimeLimit}
|
||||
value={timeLimit}
|
||||
placeholder="HH:MM"
|
||||
pattern="^[0-9][0-9]:[0-5][0-9]$"
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Text><FormattedMessage {...messages.timeLimitDescription} /></Form.Text>
|
||||
</div>
|
||||
)}
|
||||
{ showReviewRulesDiv && (
|
||||
<div className="mt-3">
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<FormattedMessage {...messages.reviewRulesLabel} />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
onChange={(e) => setFieldValue('examReviewRules', e.target.value)}
|
||||
value={examReviewRules}
|
||||
as="textarea"
|
||||
rows="3"
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Text>
|
||||
{ onlineProctoringRules ? (
|
||||
<FormattedMessage
|
||||
{...messages.reviewRulesDescriptionWithLink}
|
||||
values={{
|
||||
hyperlink: (
|
||||
<Hyperlink
|
||||
destination={onlineProctoringRules}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<FormattedMessage
|
||||
{...messages.reviewRulesDescriptionLinkText}
|
||||
/>
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage {...messages.reviewRulesDescription} />
|
||||
)}
|
||||
</Form.Text>
|
||||
</div>
|
||||
)}
|
||||
<PrereqSettings
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
prereqs={prereqs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AdvancedTab.defaultProps = {
|
||||
prereqs: [],
|
||||
wasExamEverLinkedWithExternal: false,
|
||||
enableProctoredExams: false,
|
||||
supportsOnboarding: false,
|
||||
wasProctoredExam: false,
|
||||
showReviewRules: false,
|
||||
onlineProctoringRules: '',
|
||||
};
|
||||
|
||||
AdvancedTab.propTypes = {
|
||||
values: PropTypes.shape({
|
||||
isTimeLimited: PropTypes.bool.isRequired,
|
||||
defaultTimeLimitMinutes: PropTypes.number,
|
||||
isPrereq: PropTypes.bool,
|
||||
prereqUsageKey: PropTypes.string,
|
||||
prereqMinScore: PropTypes.number,
|
||||
prereqMinCompletion: PropTypes.number,
|
||||
isProctoredExam: PropTypes.bool,
|
||||
isPracticeExam: PropTypes.bool,
|
||||
isOnboardingExam: PropTypes.bool,
|
||||
examReviewRules: PropTypes.string,
|
||||
}).isRequired,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
prereqs: PropTypes.arrayOf(PropTypes.shape({
|
||||
blockUsageKey: PropTypes.string.isRequired,
|
||||
blockDisplayName: PropTypes.string.isRequired,
|
||||
})),
|
||||
releasedToStudents: PropTypes.bool.isRequired,
|
||||
wasExamEverLinkedWithExternal: PropTypes.bool,
|
||||
enableProctoredExams: PropTypes.bool,
|
||||
supportsOnboarding: PropTypes.bool,
|
||||
wasProctoredExam: PropTypes.bool,
|
||||
showReviewRules: PropTypes.bool,
|
||||
onlineProctoringRules: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(AdvancedTab);
|
||||
106
src/generic/configure-modal/BasicTab.jsx
Normal file
106
src/generic/configure-modal/BasicTab.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Stack, Form } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { DatepickerControl, DATEPICKER_TYPES } from '../datepicker-control';
|
||||
import messages from './messages';
|
||||
|
||||
const BasicTab = ({
|
||||
values,
|
||||
setFieldValue,
|
||||
courseGraders,
|
||||
isSubsection,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const {
|
||||
releaseDate,
|
||||
graderType,
|
||||
dueDate,
|
||||
} = values;
|
||||
|
||||
const onChangeGraderType = (e) => setFieldValue('graderType', e.target.value);
|
||||
|
||||
const createOptions = () => courseGraders.map((option) => (
|
||||
<option key={option} value={option}> {option} </option>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.releaseDateAndTime} /></h5>
|
||||
<hr />
|
||||
<div data-testid="release-date-stack">
|
||||
<Stack className="mt-3" direction="horizontal" gap={5}>
|
||||
<DatepickerControl
|
||||
type={DATEPICKER_TYPES.date}
|
||||
value={releaseDate}
|
||||
label={intl.formatMessage(messages.releaseDate)}
|
||||
controlName="state-date"
|
||||
onChange={(val) => setFieldValue('releaseDate', val)}
|
||||
/>
|
||||
<DatepickerControl
|
||||
type={DATEPICKER_TYPES.time}
|
||||
value={releaseDate}
|
||||
label={intl.formatMessage(messages.releaseTimeUTC)}
|
||||
controlName="start-time"
|
||||
onChange={(val) => setFieldValue('releaseDate', val)}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
{
|
||||
isSubsection && (
|
||||
<div>
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.grading} /></h5>
|
||||
<hr />
|
||||
<Form.Group>
|
||||
<Form.Label><FormattedMessage {...messages.gradeAs} /></Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue={graderType}
|
||||
onChange={onChangeGraderType}
|
||||
data-testid="grader-type-select"
|
||||
>
|
||||
<option key="notgraded" value="notgraded">
|
||||
{intl.formatMessage(messages.notGradedTypeOption)}
|
||||
</option>
|
||||
{createOptions()}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<div data-testid="due-date-stack">
|
||||
<Stack className="mt-3" direction="horizontal" gap={5}>
|
||||
<DatepickerControl
|
||||
type={DATEPICKER_TYPES.date}
|
||||
value={dueDate}
|
||||
label={intl.formatMessage(messages.dueDate)}
|
||||
controlName="state-date"
|
||||
onChange={(val) => setFieldValue('dueDate', val)}
|
||||
data-testid="due-date-picker"
|
||||
/>
|
||||
<DatepickerControl
|
||||
type={DATEPICKER_TYPES.time}
|
||||
value={dueDate}
|
||||
label={intl.formatMessage(messages.dueTimeUTC)}
|
||||
controlName="start-time"
|
||||
onChange={(val) => setFieldValue('dueDate', val)}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BasicTab.propTypes = {
|
||||
isSubsection: PropTypes.bool.isRequired,
|
||||
values: PropTypes.shape({
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
graderType: PropTypes.string.isRequired,
|
||||
dueDate: PropTypes.string,
|
||||
}).isRequired,
|
||||
courseGraders: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(BasicTab);
|
||||
365
src/generic/configure-modal/ConfigureModal.jsx
Normal file
365
src/generic/configure-modal/ConfigureModal.jsx
Normal file
@@ -0,0 +1,365 @@
|
||||
/* eslint-disable import/named */
|
||||
import React from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ModalDialog,
|
||||
Button,
|
||||
ActionRow,
|
||||
Form,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from '@openedx/paragon';
|
||||
import { Formik } from 'formik';
|
||||
|
||||
import { VisibilityTypes } from '../../data/constants';
|
||||
import { COURSE_BLOCK_NAMES } from '../../constants';
|
||||
import messages from './messages';
|
||||
import BasicTab from './BasicTab';
|
||||
import VisibilityTab from './VisibilityTab';
|
||||
import AdvancedTab from './AdvancedTab';
|
||||
import UnitTab from './UnitTab';
|
||||
|
||||
const ConfigureModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfigureSubmit,
|
||||
currentItemData,
|
||||
enableProctoredExams,
|
||||
isXBlockComponent,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
displayName,
|
||||
start: sectionStartDate,
|
||||
visibilityState,
|
||||
due,
|
||||
isTimeLimited,
|
||||
defaultTimeLimitMinutes,
|
||||
hideAfterDue,
|
||||
showCorrectness,
|
||||
courseGraders,
|
||||
category,
|
||||
format,
|
||||
userPartitionInfo,
|
||||
ancestorHasStaffLock,
|
||||
isPrereq,
|
||||
prereqs,
|
||||
prereq,
|
||||
prereqMinScore,
|
||||
prereqMinCompletion,
|
||||
releasedToStudents,
|
||||
wasExamEverLinkedWithExternal,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
examReviewRules,
|
||||
supportsOnboarding,
|
||||
showReviewRules,
|
||||
onlineProctoringRules,
|
||||
} = currentItemData;
|
||||
|
||||
const getSelectedGroups = () => {
|
||||
if (userPartitionInfo?.selectedPartitionIndex >= 0) {
|
||||
return userPartitionInfo?.selectablePartitions[userPartitionInfo?.selectedPartitionIndex]
|
||||
?.groups
|
||||
.filter(({ selected }) => selected)
|
||||
.map(({ id }) => `${id}`)
|
||||
|| [];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const defaultPrereqScore = (val) => {
|
||||
if (val === null || val === undefined) {
|
||||
return 100;
|
||||
}
|
||||
return parseFloat(val);
|
||||
};
|
||||
|
||||
const initialValues = {
|
||||
releaseDate: sectionStartDate,
|
||||
isVisibleToStaffOnly: visibilityState === VisibilityTypes.STAFF_ONLY,
|
||||
graderType: format == null ? 'notgraded' : format,
|
||||
dueDate: due == null ? '' : due,
|
||||
isTimeLimited,
|
||||
isProctoredExam,
|
||||
isOnboardingExam,
|
||||
isPracticeExam,
|
||||
examReviewRules,
|
||||
defaultTimeLimitMinutes,
|
||||
hideAfterDue: hideAfterDue === undefined ? false : hideAfterDue,
|
||||
showCorrectness,
|
||||
isPrereq,
|
||||
prereqUsageKey: prereq,
|
||||
prereqMinScore: defaultPrereqScore(prereqMinScore),
|
||||
prereqMinCompletion: defaultPrereqScore(prereqMinCompletion),
|
||||
// by default it is -1 i.e. accessible to all learners & staff
|
||||
selectedPartitionIndex: userPartitionInfo?.selectedPartitionIndex,
|
||||
selectedGroups: getSelectedGroups(),
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
isTimeLimited: Yup.boolean(),
|
||||
isProctoredExam: Yup.boolean(),
|
||||
isPracticeExam: Yup.boolean(),
|
||||
isOnboardingExam: Yup.boolean(),
|
||||
examReviewRules: Yup.string(),
|
||||
defaultTimeLimitMinutes: Yup.number().nullable(true),
|
||||
hideAfterDueState: Yup.boolean(),
|
||||
showCorrectness: Yup.string().required(),
|
||||
isPrereq: Yup.boolean(),
|
||||
prereqUsageKey: Yup.string().nullable(true),
|
||||
prereqMinScore: Yup.number().min(
|
||||
0,
|
||||
intl.formatMessage(messages.minScoreError),
|
||||
).max(
|
||||
100,
|
||||
intl.formatMessage(messages.minScoreError),
|
||||
).nullable(true),
|
||||
prereqMinCompletion: Yup.number().min(
|
||||
0,
|
||||
intl.formatMessage(messages.minScoreError),
|
||||
).max(
|
||||
100,
|
||||
intl.formatMessage(messages.minScoreError),
|
||||
).nullable(true),
|
||||
selectedPartitionIndex: Yup.number().integer(),
|
||||
selectedGroups: Yup.array().of(Yup.string()),
|
||||
});
|
||||
|
||||
const isSubsection = category === COURSE_BLOCK_NAMES.sequential.id;
|
||||
|
||||
const dialogTitle = isXBlockComponent
|
||||
? intl.formatMessage(messages.componentTitle, { title: displayName })
|
||||
: intl.formatMessage(messages.title, { title: displayName });
|
||||
|
||||
const handleSave = (data) => {
|
||||
const groupAccess = {};
|
||||
switch (category) {
|
||||
case COURSE_BLOCK_NAMES.chapter.id:
|
||||
onConfigureSubmit(data.isVisibleToStaffOnly, data.releaseDate);
|
||||
break;
|
||||
case COURSE_BLOCK_NAMES.sequential.id:
|
||||
onConfigureSubmit(
|
||||
data.isVisibleToStaffOnly,
|
||||
data.releaseDate,
|
||||
data.graderType,
|
||||
data.dueDate,
|
||||
data.isTimeLimited,
|
||||
data.isProctoredExam,
|
||||
data.isOnboardingExam,
|
||||
data.isPracticeExam,
|
||||
data.examReviewRules,
|
||||
data.isTimeLimited ? data.defaultTimeLimitMinutes : 0,
|
||||
data.hideAfterDue,
|
||||
data.showCorrectness,
|
||||
data.isPrereq,
|
||||
data.prereqUsageKey,
|
||||
data.prereqMinScore,
|
||||
data.prereqMinCompletion,
|
||||
);
|
||||
break;
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
case COURSE_BLOCK_NAMES.component.id:
|
||||
// groupAccess should be {partitionId: [group1, group2]} or {} if selectedPartitionIndex === -1
|
||||
if (data.selectedPartitionIndex >= 0) {
|
||||
const partitionId = userPartitionInfo.selectablePartitions[data.selectedPartitionIndex].id;
|
||||
groupAccess[partitionId] = data.selectedGroups.map(g => parseInt(g, 10));
|
||||
}
|
||||
onConfigureSubmit(data.isVisibleToStaffOnly, groupAccess);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const renderModalBody = (values, setFieldValue) => {
|
||||
switch (category) {
|
||||
case COURSE_BLOCK_NAMES.chapter.id:
|
||||
return (
|
||||
<Tabs>
|
||||
<Tab eventKey="basic" title={intl.formatMessage(messages.basicTabTitle)}>
|
||||
<BasicTab
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
isSubsection={isSubsection}
|
||||
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
|
||||
<VisibilityTab
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
category={category}
|
||||
isSubsection={isSubsection}
|
||||
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
case COURSE_BLOCK_NAMES.sequential.id:
|
||||
return (
|
||||
<Tabs>
|
||||
<Tab eventKey="basic" title={intl.formatMessage(messages.basicTabTitle)}>
|
||||
<BasicTab
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
isSubsection={isSubsection}
|
||||
courseGraders={courseGraders === 'undefined' ? [] : courseGraders}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
|
||||
<VisibilityTab
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
category={category}
|
||||
isSubsection={isSubsection}
|
||||
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab eventKey="advanced" title={intl.formatMessage(messages.advancedTabTitle)}>
|
||||
<AdvancedTab
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
prereqs={prereqs}
|
||||
releasedToStudents={releasedToStudents}
|
||||
wasExamEverLinkedWithExternal={wasExamEverLinkedWithExternal}
|
||||
enableProctoredExams={enableProctoredExams}
|
||||
supportsOnboarding={supportsOnboarding}
|
||||
showReviewRules={showReviewRules}
|
||||
wasProctoredExam={isProctoredExam}
|
||||
onlineProctoringRules={onlineProctoringRules}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
case COURSE_BLOCK_NAMES.vertical.id:
|
||||
case COURSE_BLOCK_NAMES.component.id:
|
||||
return (
|
||||
<UnitTab
|
||||
isXBlockComponent={COURSE_BLOCK_NAMES.component.id === category}
|
||||
values={values}
|
||||
setFieldValue={setFieldValue}
|
||||
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY && !ancestorHasStaffLock}
|
||||
userPartitionInfo={userPartitionInfo}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
className="configure-modal"
|
||||
size="lg"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<div data-testid="configure-modal">
|
||||
<ModalDialog.Header className="configure-modal__header">
|
||||
<ModalDialog.Title>
|
||||
{dialogTitle}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSave}
|
||||
validationSchema={validationSchema}
|
||||
validateOnBlur
|
||||
validateOnChange
|
||||
>
|
||||
{({
|
||||
values, handleSubmit, setFieldValue,
|
||||
}) => (
|
||||
<>
|
||||
<ModalDialog.Body className="configure-modal__body">
|
||||
<Form.Group size="sm" className="form-field">
|
||||
{renderModalBody(values, setFieldValue)}
|
||||
</Form.Group>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="pt-1">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.cancelButton)}
|
||||
</ModalDialog.CloseButton>
|
||||
<Button
|
||||
data-testid="configure-save-button"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{intl.formatMessage(messages.saveButton)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigureModal.defaultProps = {
|
||||
isXBlockComponent: false,
|
||||
enableProctoredExams: false,
|
||||
};
|
||||
|
||||
ConfigureModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onConfigureSubmit: PropTypes.func.isRequired,
|
||||
enableProctoredExams: PropTypes.bool,
|
||||
currentItemData: PropTypes.shape({
|
||||
displayName: PropTypes.string,
|
||||
start: PropTypes.string,
|
||||
visibilityState: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
due: PropTypes.string,
|
||||
isTimeLimited: PropTypes.bool,
|
||||
defaultTimeLimitMinutes: PropTypes.number,
|
||||
hideAfterDue: PropTypes.bool,
|
||||
showCorrectness: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
|
||||
courseGraders: PropTypes.arrayOf(PropTypes.string),
|
||||
category: PropTypes.string,
|
||||
format: PropTypes.string,
|
||||
userPartitionInfo: PropTypes.shape({
|
||||
selectablePartitions: PropTypes.arrayOf(PropTypes.shape({
|
||||
groups: PropTypes.arrayOf(PropTypes.shape({
|
||||
deleted: PropTypes.bool,
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
selected: PropTypes.bool,
|
||||
})),
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
scheme: PropTypes.string,
|
||||
})),
|
||||
selectedPartitionIndex: PropTypes.number,
|
||||
selectedGroupsLabel: PropTypes.string,
|
||||
}),
|
||||
ancestorHasStaffLock: PropTypes.bool,
|
||||
isPrereq: PropTypes.bool,
|
||||
prereqs: PropTypes.arrayOf({
|
||||
blockDisplayName: PropTypes.string,
|
||||
blockUsageKey: PropTypes.string,
|
||||
}),
|
||||
prereq: PropTypes.number,
|
||||
prereqMinScore: PropTypes.number,
|
||||
prereqMinCompletion: PropTypes.number,
|
||||
releasedToStudents: PropTypes.bool,
|
||||
wasExamEverLinkedWithExternal: PropTypes.bool,
|
||||
isProctoredExam: PropTypes.bool,
|
||||
isOnboardingExam: PropTypes.bool,
|
||||
isPracticeExam: PropTypes.bool,
|
||||
examReviewRules: PropTypes.string,
|
||||
supportsOnboarding: PropTypes.bool,
|
||||
showReviewRules: PropTypes.bool,
|
||||
onlineProctoringRules: PropTypes.string,
|
||||
}).isRequired,
|
||||
isXBlockComponent: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ConfigureModal;
|
||||
14
src/generic/configure-modal/ConfigureModal.scss
Normal file
14
src/generic/configure-modal/ConfigureModal.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
.configure-modal {
|
||||
.configure-modal__header {
|
||||
padding-top: 1.5rem;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.w-7rem {
|
||||
width: 7.2rem;
|
||||
}
|
||||
|
||||
.mw-1-25rem {
|
||||
min-width: 1.25rem;
|
||||
}
|
||||
}
|
||||
278
src/generic/configure-modal/ConfigureModal.test.jsx
Normal file
278
src/generic/configure-modal/ConfigureModal.test.jsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { 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 initializeStore from '../../store';
|
||||
import ConfigureModal from './ConfigureModal';
|
||||
import {
|
||||
currentSectionMock,
|
||||
currentSubsectionMock,
|
||||
currentUnitMock,
|
||||
currentXBlockMock,
|
||||
} from './__mocks__';
|
||||
import messages from './messages';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let axiosMock;
|
||||
let store;
|
||||
const mockPathname = '/foo-bar';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: () => ({
|
||||
pathname: mockPathname,
|
||||
}),
|
||||
}));
|
||||
|
||||
const onCloseMock = jest.fn();
|
||||
const onConfigureSubmitMock = jest.fn();
|
||||
|
||||
const renderComponent = () => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<ConfigureModal
|
||||
isOpen
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
currentItemData={currentSectionMock}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<ConfigureModal /> for Section', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders ConfigureModal component correctly', () => {
|
||||
const { getByText, getByRole } = renderComponent();
|
||||
expect(getByText(`${currentSectionMock.displayName} settings`)).toBeInTheDocument();
|
||||
expect(getByText(messages.basicTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.visibilityTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.releaseDate.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.releaseTimeUTC.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches to the Visibility tab and renders correctly', () => {
|
||||
const { getByRole, getByText } = renderComponent();
|
||||
|
||||
const visibilityTab = getByRole('tab', { name: messages.visibilityTabTitle.defaultMessage });
|
||||
userEvent.click(visibilityTab);
|
||||
expect(getByText('Section visibility')).toBeInTheDocument();
|
||||
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const renderSubsectionComponent = () => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<ConfigureModal
|
||||
isOpen
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
currentItemData={currentSubsectionMock}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<ConfigureModal /> for Subsection', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders subsection ConfigureModal component correctly', () => {
|
||||
const { getByText, getByRole } = renderSubsectionComponent();
|
||||
expect(getByText(`${currentSubsectionMock.displayName} settings`)).toBeInTheDocument();
|
||||
expect(getByText(messages.basicTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.visibilityTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.advancedTabTitle.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.releaseDate.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.releaseTimeUTC.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.grading.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.gradeAs.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.dueDate.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.dueTimeUTC.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches to the subsection Visibility tab and renders correctly', () => {
|
||||
const { getByRole, getByText } = renderSubsectionComponent();
|
||||
|
||||
const visibilityTab = getByRole('tab', { name: messages.visibilityTabTitle.defaultMessage });
|
||||
userEvent.click(visibilityTab);
|
||||
expect(getByText('Subsection visibility')).toBeInTheDocument();
|
||||
expect(getByText(messages.showEntireSubsection.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.showEntireSubsectionDescription.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.hideContentAfterDue.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.hideContentAfterDueDescription.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.hideEntireSubsection.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.hideEntireSubsectionDescription.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.assessmentResultsVisibility.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.alwaysShowAssessmentResults.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.alwaysShowAssessmentResultsDescription.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.neverShowAssessmentResults.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.neverShowAssessmentResultsDescription.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.showAssessmentResultsPastDue.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.showAssessmentResultsPastDueDescription.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches to the subsection Advanced tab and renders correctly', () => {
|
||||
const { getByRole, getByText } = renderSubsectionComponent();
|
||||
|
||||
const advancedTab = getByRole('tab', { name: messages.advancedTabTitle.defaultMessage });
|
||||
userEvent.click(advancedTab);
|
||||
expect(getByText(messages.setSpecialExam.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.none.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.timed.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.timedDescription.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const renderUnitComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<ConfigureModal
|
||||
isOpen
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
currentItemData={currentUnitMock}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<ConfigureModal /> for Unit', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders unit ConfigureModal component correctly', () => {
|
||||
const {
|
||||
getByText, queryByText, getByRole, getByTestId,
|
||||
} = renderUnitComponent();
|
||||
expect(getByText(`${currentUnitMock.displayName} settings`)).toBeInTheDocument();
|
||||
expect(getByText(messages.unitVisibility.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.unitSelectGroupType.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(queryByText(messages.unitSelectGroup.defaultMessage)).not.toBeInTheDocument();
|
||||
const input = getByTestId('group-type-select');
|
||||
|
||||
['0', '1'].forEach(groupeTypeIndex => {
|
||||
userEvent.selectOptions(input, groupeTypeIndex);
|
||||
|
||||
expect(getByText(messages.unitSelectGroup.defaultMessage)).toBeInTheDocument();
|
||||
currentUnitMock
|
||||
.userPartitionInfo
|
||||
.selectablePartitions[groupeTypeIndex].groups
|
||||
.forEach(g => expect(getByText(g.name)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const renderXBlockComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<ConfigureModal
|
||||
isOpen
|
||||
isXBlockComponent
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
currentItemData={currentXBlockMock}
|
||||
{...props}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<ConfigureModal /> for XBlock', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
});
|
||||
|
||||
it('renders unit ConfigureModal component correctly', () => {
|
||||
const {
|
||||
getByText, queryByText, getByRole, getByTestId,
|
||||
} = renderXBlockComponent();
|
||||
expect(getByText(`Editing access for: ${currentUnitMock.displayName}`)).toBeInTheDocument();
|
||||
expect(queryByText(messages.unitVisibility.defaultMessage)).not.toBeInTheDocument();
|
||||
expect(queryByText(messages.hideFromLearners.defaultMessage)).not.toBeInTheDocument();
|
||||
expect(getByText(messages.restrictAccessTo.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.unitSelectGroupType.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
expect(queryByText(messages.unitSelectGroup.defaultMessage)).not.toBeInTheDocument();
|
||||
const input = getByTestId('group-type-select');
|
||||
|
||||
['0', '1'].forEach(groupeTypeIndex => {
|
||||
userEvent.selectOptions(input, groupeTypeIndex);
|
||||
|
||||
expect(getByText(messages.unitSelectGroup.defaultMessage)).toBeInTheDocument();
|
||||
currentUnitMock
|
||||
.userPartitionInfo
|
||||
.selectablePartitions[groupeTypeIndex].groups
|
||||
.forEach(g => expect(getByText(g.name)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
expect(getByRole('button', { name: messages.cancelButton.defaultMessage })).toBeInTheDocument();
|
||||
expect(getByRole('button', { name: messages.saveButton.defaultMessage })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
117
src/generic/configure-modal/PrereqSettings.jsx
Normal file
117
src/generic/configure-modal/PrereqSettings.jsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
import FormikControl from '../FormikControl';
|
||||
|
||||
const PrereqSettings = ({
|
||||
values,
|
||||
setFieldValue,
|
||||
prereqs,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
isPrereq,
|
||||
prereqUsageKey,
|
||||
prereqMinScore,
|
||||
prereqMinCompletion,
|
||||
} = values;
|
||||
|
||||
if (isPrereq === null || isPrereq === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSelectChange = (e) => {
|
||||
setFieldValue('prereqUsageKey', e.target.value);
|
||||
};
|
||||
|
||||
const prereqSelectionForm = () => (
|
||||
<>
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.limitAccessTitle} /></h5>
|
||||
<hr />
|
||||
<Form>
|
||||
<Form.Text><FormattedMessage {...messages.limitAccessDescription} /></Form.Text>
|
||||
<Form.Group controlId="prereqForm.select">
|
||||
<Form.Label>
|
||||
{intl.formatMessage(messages.prerequisiteSelectLabel)}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
defaultValue={prereqUsageKey}
|
||||
onChange={handleSelectChange}
|
||||
role="combobox"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.noPrerequisiteOption)}
|
||||
</option>
|
||||
{prereqs.map((prereqOption) => (
|
||||
<option
|
||||
key={prereqOption.blockUsageKey}
|
||||
value={prereqOption.blockUsageKey}
|
||||
>
|
||||
{prereqOption.blockDisplayName}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
{prereqUsageKey && (
|
||||
<>
|
||||
<FormikControl
|
||||
name="prereqMinScore"
|
||||
value={prereqMinScore}
|
||||
label={<Form.Label>{intl.formatMessage(messages.minScoreLabel)}</Form.Label>}
|
||||
controlClassName="text-right"
|
||||
controlClasses="w-7rem"
|
||||
type="number"
|
||||
trailingElement="%"
|
||||
/>
|
||||
<FormikControl
|
||||
name="prereqMinCompletion"
|
||||
value={prereqMinCompletion}
|
||||
label={<Form.Label>{intl.formatMessage(messages.minCompletionLabel)}</Form.Label>}
|
||||
controlClassName="text-right"
|
||||
controlClasses="w-7rem"
|
||||
type="number"
|
||||
trailingElement="%"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
|
||||
const handleCheckboxChange = e => setFieldValue('isPrereq', e.target.checked);
|
||||
|
||||
return (
|
||||
<>
|
||||
{prereqs.length > 0 && prereqSelectionForm()}
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.prereqTitle} /></h5>
|
||||
<hr />
|
||||
<Form.Checkbox checked={isPrereq} onChange={handleCheckboxChange}>
|
||||
<FormattedMessage {...messages.prereqCheckboxLabel} />
|
||||
</Form.Checkbox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
PrereqSettings.defaultProps = {
|
||||
prereqs: [],
|
||||
};
|
||||
|
||||
PrereqSettings.propTypes = {
|
||||
values: PropTypes.shape({
|
||||
isPrereq: PropTypes.bool,
|
||||
prereqUsageKey: PropTypes.string,
|
||||
prereqMinScore: PropTypes.number,
|
||||
prereqMinCompletion: PropTypes.number,
|
||||
}).isRequired,
|
||||
prereqs: PropTypes.arrayOf(PropTypes.shape({
|
||||
blockUsageKey: PropTypes.string.isRequired,
|
||||
blockDisplayName: PropTypes.string.isRequired,
|
||||
})),
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(PrereqSettings);
|
||||
165
src/generic/configure-modal/UnitTab.jsx
Normal file
165
src/generic/configure-modal/UnitTab.jsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Form } from '@openedx/paragon';
|
||||
import {
|
||||
FormattedMessage, injectIntl, useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Field } from 'formik';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const UnitTab = ({
|
||||
isXBlockComponent,
|
||||
values,
|
||||
setFieldValue,
|
||||
showWarning,
|
||||
userPartitionInfo,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
isVisibleToStaffOnly,
|
||||
selectedPartitionIndex,
|
||||
selectedGroups,
|
||||
} = values;
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFieldValue('isVisibleToStaffOnly', e.target.checked);
|
||||
};
|
||||
|
||||
const handleSelect = (e) => {
|
||||
setFieldValue('selectedPartitionIndex', parseInt(e.target.value, 10));
|
||||
setFieldValue('selectedGroups', []);
|
||||
};
|
||||
|
||||
const checkIsDeletedGroup = (group) => {
|
||||
const isGroupSelected = selectedGroups.includes(group.id.toString());
|
||||
|
||||
return group.deleted && isGroupSelected;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isXBlockComponent && (
|
||||
<>
|
||||
<h3 className="mt-3"><FormattedMessage {...messages.unitVisibility} /></h3>
|
||||
<hr />
|
||||
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleChange} data-testid="unit-visibility-checkbox">
|
||||
<FormattedMessage {...messages.hideFromLearners} />
|
||||
</Form.Checkbox>
|
||||
{showWarning && (
|
||||
<Alert className="mt-2" variant="warning">
|
||||
<FormattedMessage {...messages.unitVisibilityWarning} />
|
||||
</Alert>
|
||||
)}
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
<Form.Group controlId="groupSelect">
|
||||
<Form.Label as="legend" className="font-weight-bold">
|
||||
<FormattedMessage {...messages.restrictAccessTo} />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
name="groupSelect"
|
||||
value={selectedPartitionIndex}
|
||||
onChange={handleSelect}
|
||||
data-testid="group-type-select"
|
||||
>
|
||||
<option value="-1" key="-1">
|
||||
{userPartitionInfo.selectedPartitionIndex === -1
|
||||
? intl.formatMessage(messages.unitSelectGroupType)
|
||||
: intl.formatMessage(messages.unitAllLearnersAndStaff)}
|
||||
</option>
|
||||
{userPartitionInfo.selectablePartitions.map((partition, index) => (
|
||||
<option
|
||||
key={partition.id}
|
||||
value={index}
|
||||
>
|
||||
{partition.name}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
|
||||
{selectedPartitionIndex >= 0 && userPartitionInfo.selectablePartitions.length && (
|
||||
<Form.Group controlId="select-groups-checkboxes">
|
||||
<Form.Label><FormattedMessage {...messages.unitSelectGroup} /></Form.Label>
|
||||
<div
|
||||
role="group"
|
||||
className="d-flex flex-column"
|
||||
data-testid="group-checkboxes"
|
||||
aria-labelledby="select-groups-checkboxes"
|
||||
>
|
||||
{userPartitionInfo.selectablePartitions[selectedPartitionIndex].groups.map((group) => (
|
||||
<Form.Group
|
||||
key={group.id}
|
||||
className="pgn__form-checkbox"
|
||||
>
|
||||
<Field
|
||||
as={Form.Control}
|
||||
className="flex-grow-0 mr-1"
|
||||
controlClassName="pgn__form-checkbox-input mr-1"
|
||||
type="checkbox"
|
||||
value={`${group.id}`}
|
||||
name="selectedGroups"
|
||||
/>
|
||||
<div>
|
||||
<Form.Label
|
||||
className={classNames({ 'text-danger': checkIsDeletedGroup(group) })}
|
||||
isInline
|
||||
>
|
||||
{group.name}
|
||||
</Form.Label>
|
||||
{group.deleted && (
|
||||
<Form.Control.Feedback type="invalid" hasIcon={false}>
|
||||
{intl.formatMessage(messages.unitSelectDeletedGroupErrorMessage)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</div>
|
||||
</Form.Group>
|
||||
))}
|
||||
</div>
|
||||
</Form.Group>
|
||||
)}
|
||||
</Form.Group>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
UnitTab.defaultProps = {
|
||||
isXBlockComponent: false,
|
||||
};
|
||||
|
||||
UnitTab.propTypes = {
|
||||
isXBlockComponent: PropTypes.bool,
|
||||
values: PropTypes.shape({
|
||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
selectedPartitionIndex: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]).isRequired,
|
||||
selectedGroups: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
]),
|
||||
}).isRequired,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
showWarning: PropTypes.bool.isRequired,
|
||||
userPartitionInfo: PropTypes.shape({
|
||||
selectablePartitions: PropTypes.arrayOf(PropTypes.shape({
|
||||
groups: PropTypes.arrayOf(PropTypes.shape({
|
||||
deleted: PropTypes.bool.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
selected: PropTypes.bool.isRequired,
|
||||
}).isRequired).isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
scheme: PropTypes.string.isRequired,
|
||||
}).isRequired).isRequired,
|
||||
selectedGroupsLabel: PropTypes.string,
|
||||
selectedPartitionIndex: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(UnitTab);
|
||||
135
src/generic/configure-modal/VisibilityTab.jsx
Normal file
135
src/generic/configure-modal/VisibilityTab.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Form } from '@openedx/paragon';
|
||||
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import { COURSE_BLOCK_NAMES } from '../../constants';
|
||||
|
||||
const VisibilityTab = ({
|
||||
values,
|
||||
setFieldValue,
|
||||
category,
|
||||
showWarning,
|
||||
isSubsection,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const visibilityTitle = COURSE_BLOCK_NAMES[category]?.name;
|
||||
|
||||
const {
|
||||
isVisibleToStaffOnly,
|
||||
hideAfterDue,
|
||||
showCorrectness,
|
||||
} = values;
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFieldValue('isVisibleToStaffOnly', e.target.checked);
|
||||
};
|
||||
|
||||
const getVisibilityValue = () => {
|
||||
if (isVisibleToStaffOnly) {
|
||||
return 'hide';
|
||||
}
|
||||
if (hideAfterDue) {
|
||||
return 'hideDue';
|
||||
}
|
||||
return 'show';
|
||||
};
|
||||
|
||||
const visibilityChanged = (e) => {
|
||||
const selected = e.target.value;
|
||||
if (selected === 'hide') {
|
||||
setFieldValue('isVisibleToStaffOnly', true);
|
||||
setFieldValue('hideAfterDue', false);
|
||||
} else if (selected === 'hideDue') {
|
||||
setFieldValue('isVisibleToStaffOnly', false);
|
||||
setFieldValue('hideAfterDue', true);
|
||||
} else {
|
||||
setFieldValue('isVisibleToStaffOnly', false);
|
||||
setFieldValue('hideAfterDue', false);
|
||||
}
|
||||
};
|
||||
|
||||
const correctnessChanged = (e) => {
|
||||
setFieldValue('showCorrectness', e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5 className="mt-4 text-gray-700">
|
||||
{intl.formatMessage(messages.visibilitySectionTitle, { visibilityTitle })}
|
||||
</h5>
|
||||
<hr />
|
||||
{
|
||||
isSubsection ? (
|
||||
<>
|
||||
<Form.RadioSet
|
||||
name="subsectionVisibility"
|
||||
onChange={visibilityChanged}
|
||||
value={getVisibilityValue()}
|
||||
>
|
||||
<Form.Radio value="show">
|
||||
<FormattedMessage {...messages.showEntireSubsection} />
|
||||
</Form.Radio>
|
||||
<Form.Text><FormattedMessage {...messages.showEntireSubsectionDescription} /></Form.Text>
|
||||
<Form.Radio value="hideDue">
|
||||
<FormattedMessage {...messages.hideContentAfterDue} />
|
||||
</Form.Radio>
|
||||
<Form.Text><FormattedMessage {...messages.hideContentAfterDueDescription} /></Form.Text>
|
||||
<Form.Radio value="hide">
|
||||
<FormattedMessage {...messages.hideEntireSubsection} />
|
||||
</Form.Radio>
|
||||
<Form.Text><FormattedMessage {...messages.hideEntireSubsectionDescription} /></Form.Text>
|
||||
</Form.RadioSet>
|
||||
{showWarning && (
|
||||
<Alert className="mt-2" variant="warning">
|
||||
<FormattedMessage {...messages.subsectionVisibilityWarning} />
|
||||
</Alert>
|
||||
)}
|
||||
<h5 className="mt-4 text-gray-700"><FormattedMessage {...messages.assessmentResultsVisibility} /></h5>
|
||||
<Form.RadioSet
|
||||
name="assessmentResultsVisibility"
|
||||
onChange={correctnessChanged}
|
||||
value={showCorrectness}
|
||||
>
|
||||
<Form.Radio value="always">
|
||||
<FormattedMessage {...messages.alwaysShowAssessmentResults} />
|
||||
</Form.Radio>
|
||||
<Form.Text><FormattedMessage {...messages.alwaysShowAssessmentResultsDescription} /></Form.Text>
|
||||
<Form.Radio value="never">
|
||||
<FormattedMessage {...messages.neverShowAssessmentResults} />
|
||||
</Form.Radio>
|
||||
<Form.Text><FormattedMessage {...messages.neverShowAssessmentResultsDescription} /></Form.Text>
|
||||
<Form.Radio value="past_due">
|
||||
<FormattedMessage {...messages.showAssessmentResultsPastDue} />
|
||||
</Form.Radio>
|
||||
<Form.Text><FormattedMessage {...messages.showAssessmentResultsPastDueDescription} /></Form.Text>
|
||||
</Form.RadioSet>
|
||||
</>
|
||||
) : (
|
||||
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleChange} data-testid="visibility-checkbox">
|
||||
<FormattedMessage {...messages.hideFromLearners} />
|
||||
</Form.Checkbox>
|
||||
)
|
||||
}
|
||||
{showWarning && !isSubsection && (
|
||||
<Alert className="mt-2" variant="warning">
|
||||
<FormattedMessage {...messages.sectionVisibilityWarning} />
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
VisibilityTab.propTypes = {
|
||||
values: PropTypes.shape({
|
||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
hideAfterDue: PropTypes.bool.isRequired,
|
||||
showCorrectness: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
setFieldValue: PropTypes.func.isRequired,
|
||||
category: PropTypes.string.isRequired,
|
||||
showWarning: PropTypes.bool.isRequired,
|
||||
isSubsection: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(VisibilityTab);
|
||||
199
src/generic/configure-modal/__mocks__/index.js
Normal file
199
src/generic/configure-modal/__mocks__/index.js
Normal file
@@ -0,0 +1,199 @@
|
||||
export const currentSectionMock = {
|
||||
displayName: 'Section1',
|
||||
category: 'chapter',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
format: 'Not Graded',
|
||||
childInfo: {
|
||||
displayName: 'Subsection',
|
||||
children: [
|
||||
{
|
||||
displayName: 'Subsection 1',
|
||||
id: 1,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
displayName: 'Subsection_1 Unit 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subsection 2',
|
||||
id: 2,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
displayName: 'Subsection_2 Unit 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subsection 3',
|
||||
id: 3,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const currentSubsectionMock = {
|
||||
displayName: 'Subsection 1',
|
||||
id: 1,
|
||||
category: 'sequential',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
format: 'Homework',
|
||||
courseGraders: ['Homework', 'Exam'],
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
displayName: 'Subsection_1 Unit 1',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
displayName: 'Subsection_1 Unit 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const currentUnitMock = {
|
||||
displayName: 'Unit 1',
|
||||
id: 1,
|
||||
category: 'vertical',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
userPartitionInfo: {
|
||||
selectablePartitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 6,
|
||||
name: 'Honor',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1508065533,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
id: 1224170703,
|
||||
name: 'Content Group 1',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const currentXBlockMock = {
|
||||
displayName: 'Unit 1',
|
||||
id: 1,
|
||||
category: 'component',
|
||||
due: '',
|
||||
start: '2025-08-10T10:00:00Z',
|
||||
visibilityState: true,
|
||||
defaultTimeLimitMinutes: null,
|
||||
hideAfterDue: false,
|
||||
showCorrectness: false,
|
||||
userPartitionInfo: {
|
||||
selectablePartitions: [
|
||||
{
|
||||
id: 50,
|
||||
name: 'Enrollment Track Groups',
|
||||
scheme: 'enrollment_track',
|
||||
groups: [
|
||||
{
|
||||
id: 6,
|
||||
name: 'Honor',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Verified',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1508065533,
|
||||
name: 'Content Groups',
|
||||
scheme: 'cohort',
|
||||
groups: [
|
||||
{
|
||||
id: 1224170703,
|
||||
name: 'Content Group 1',
|
||||
selected: false,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selectedPartitionIndex: -1,
|
||||
selectedGroupsLabel: '',
|
||||
},
|
||||
};
|
||||
280
src/generic/configure-modal/messages.js
Normal file
280
src/generic/configure-modal/messages.js
Normal file
@@ -0,0 +1,280 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'course-authoring.course-outline.configure-modal.title',
|
||||
defaultMessage: '{title} settings',
|
||||
},
|
||||
componentTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.component.title',
|
||||
defaultMessage: 'Editing access for: {title}',
|
||||
description: 'The visibility modal title for unit',
|
||||
},
|
||||
basicTabTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.title',
|
||||
defaultMessage: 'Basic',
|
||||
},
|
||||
notGradedTypeOption: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.notGradedTypeOption',
|
||||
defaultMessage: 'Not Graded',
|
||||
},
|
||||
releaseDateAndTime: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.release-date-and-time',
|
||||
defaultMessage: 'Release date and time',
|
||||
},
|
||||
releaseDate: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.release-date',
|
||||
defaultMessage: 'Release date:',
|
||||
},
|
||||
releaseTimeUTC: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.release-time-UTC',
|
||||
defaultMessage: 'Release time in UTC:',
|
||||
},
|
||||
visibilityTabTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.title',
|
||||
defaultMessage: 'Visibility',
|
||||
},
|
||||
visibilitySectionTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.section-visibility',
|
||||
defaultMessage: '{visibilityTitle} visibility',
|
||||
},
|
||||
unitVisibility: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.unit-visibility',
|
||||
defaultMessage: 'Unit visibility',
|
||||
},
|
||||
hideFromLearners: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility.hide-from-learners',
|
||||
defaultMessage: 'Hide from learners',
|
||||
},
|
||||
restrictAccessTo: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility.restrict-access-to',
|
||||
defaultMessage: 'Restrict access to',
|
||||
},
|
||||
sectionVisibilityWarning: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.section-visibility-warning',
|
||||
defaultMessage: 'If you make this section visible to learners, learners will be able to see its content after the release date has passed and you have published the section. Only units that are explicitly hidden from learners will remain hidden after you clear this option for the section.',
|
||||
},
|
||||
unitVisibilityWarning: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-visibility-warning',
|
||||
defaultMessage: 'If the unit was previously published and released to learners, any changes you made to the unit when it was hidden will now be visible to learners.',
|
||||
},
|
||||
subsectionVisibilityWarning: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.subsection-visibility-warning',
|
||||
defaultMessage: 'If you select an option other than "Hide entire subsection", published units in this subsection will become available to learners unless they are explicitly hidden.',
|
||||
},
|
||||
unitSelectGroup: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-select-group',
|
||||
defaultMessage: 'Select one or more groups:',
|
||||
},
|
||||
unitSelectGroupType: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-select-group-type',
|
||||
defaultMessage: 'Select a group type',
|
||||
},
|
||||
unitSelectDeletedGroupErrorMessage: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-select-group-deleted-error-message',
|
||||
defaultMessage: 'This group no longer exists. Choose another group or remove the access restriction.',
|
||||
description: 'The alert text of no longer available group',
|
||||
},
|
||||
unitAllLearnersAndStaff: {
|
||||
id: 'course-authoring.course-outline.configure-modal.unit-tab.unit-all-learners-staff',
|
||||
defaultMessage: 'All Learners and Staff',
|
||||
},
|
||||
cancelButton: {
|
||||
id: 'course-authoring.course-outline.configure-modal.button.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
saveButton: {
|
||||
id: 'course-authoring.course-outline.configure-modal.button.label',
|
||||
defaultMessage: 'Save',
|
||||
},
|
||||
grading: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.grading',
|
||||
defaultMessage: 'Grading',
|
||||
},
|
||||
gradeAs: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.grade-as',
|
||||
defaultMessage: 'Grade as:',
|
||||
},
|
||||
dueDate: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.due-date',
|
||||
defaultMessage: 'Due date:',
|
||||
},
|
||||
dueTimeUTC: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.due-time-UTC',
|
||||
defaultMessage: 'Due time in UTC:',
|
||||
},
|
||||
subsectionVisibility: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.subsection-visibility',
|
||||
defaultMessage: 'Subsection visibility',
|
||||
},
|
||||
showEntireSubsection: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.show-entire-subsection',
|
||||
defaultMessage: 'Show entire subsection',
|
||||
},
|
||||
showEntireSubsectionDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.show-entire-subsection-description',
|
||||
defaultMessage: 'Learners see the published subsection and can access its content',
|
||||
},
|
||||
hideContentAfterDue: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.hide-content-after-due',
|
||||
defaultMessage: 'Hide content after due date',
|
||||
},
|
||||
hideContentAfterDueDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.hide-content-after-due-description',
|
||||
defaultMessage: 'After the subsection\'s due date has passed, learners can no longer access its content. The subsection is not included in grade calculations.',
|
||||
},
|
||||
hideEntireSubsection: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.hide-entire-subsection',
|
||||
defaultMessage: 'Hide entire subsection',
|
||||
},
|
||||
hideEntireSubsectionDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.hide-entire-subsection-description',
|
||||
defaultMessage: 'Learners do not see the subsection in the course outline. The subsection is not included in grade calculations.',
|
||||
},
|
||||
assessmentResultsVisibility: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.assessment-results-visibility',
|
||||
defaultMessage: 'Assessment Results Visibility',
|
||||
},
|
||||
alwaysShowAssessmentResults: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.always-show-assessment-results',
|
||||
defaultMessage: 'Always show assessment results',
|
||||
},
|
||||
alwaysShowAssessmentResultsDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.always-show-assessment-results-description',
|
||||
defaultMessage: 'When learners submit an answer to an assessment, they immediately see whether the answer is correct or incorrect, and the score received.',
|
||||
},
|
||||
neverShowAssessmentResults: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.never-show-assessment-results',
|
||||
defaultMessage: 'Never show assessment results',
|
||||
},
|
||||
neverShowAssessmentResultsDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.never-show-assessment-results-description',
|
||||
defaultMessage: 'Learners never see whether their answers to assessments are correct or incorrect, nor the score received.',
|
||||
},
|
||||
showAssessmentResultsPastDue: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.show-assessment-results-past-due',
|
||||
defaultMessage: 'Show assessment results when subsection is past due',
|
||||
},
|
||||
showAssessmentResultsPastDueDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.show-assessment-results-past-due-description',
|
||||
defaultMessage: 'Learners do not see whether their answer to assessments were correct or incorrect, nor the score received, until after the due date for the subsection has passed. If the subsection does not have a due date, learners always see their scores when they submit answers to assessments.',
|
||||
},
|
||||
setSpecialExam: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.set-special-exam',
|
||||
defaultMessage: 'Set as a special exam',
|
||||
},
|
||||
none: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.none',
|
||||
defaultMessage: 'None',
|
||||
},
|
||||
timed: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed',
|
||||
defaultMessage: 'Timed',
|
||||
},
|
||||
timedDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
|
||||
defaultMessage: 'Use a timed exam to limit the time learners can spend on problems in this subsection. Learners must submit answers before the time expires. You can allow additional time for individual learners through the instructor Dashboard.',
|
||||
},
|
||||
proctoredExam: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.proctoredExam',
|
||||
defaultMessage: 'Proctored',
|
||||
},
|
||||
proctoredExamDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
|
||||
defaultMessage: 'Proctored exams are timed and they record video of each learner taking the exam. The videos are then reviewed to ensure that learners follow all examination rules. Please note that setting this exam as proctored will change the visibility settings to "Hide content after due date."',
|
||||
},
|
||||
onboardingExam: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.onboardingExam',
|
||||
defaultMessage: 'Onboarding',
|
||||
},
|
||||
onboardingExamDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
|
||||
defaultMessage: 'Use Onboarding to introduce learners to proctoring, verify their identity, and create an onboarding profile. Learners must complete the onboarding profile step prior to taking a proctored exam. Profile reviews take 2+ business days.',
|
||||
},
|
||||
practiceExam: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.practiceExam',
|
||||
defaultMessage: 'Practice proctored',
|
||||
},
|
||||
practiceExamDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.timed-description',
|
||||
defaultMessage: 'Use a practice proctored exam to introduce learners to the proctoring tools and processes. Results of a practice exam do not affect a learner\'s grade.',
|
||||
},
|
||||
advancedTabTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.title',
|
||||
defaultMessage: 'Advanced',
|
||||
},
|
||||
timeAllotted: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-allotted',
|
||||
defaultMessage: 'Time allotted (HH:MM):',
|
||||
},
|
||||
timeLimitDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.time-limit-description',
|
||||
defaultMessage: 'Select a time allotment for the exam. If it is over 24 hours, type in the amount of time. You can grant individual learners extra time to complete the exam through the Instructor Dashboard.',
|
||||
},
|
||||
prereqTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.prereqTitle',
|
||||
defaultMessage: 'Use as a Prerequisite',
|
||||
},
|
||||
prereqCheckboxLabel: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.prereqCheckboxLabel',
|
||||
defaultMessage: 'Make this subsection available as a prerequisite to other content',
|
||||
},
|
||||
limitAccessTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.limitAccessTitle',
|
||||
defaultMessage: 'Limit access',
|
||||
},
|
||||
limitAccessDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.limitAccessDescription',
|
||||
defaultMessage: 'Select a prerequisite subsection and enter a minimum score percentage and minimum completion percentage to limit access to this subsection. Allowed values are 0-100',
|
||||
},
|
||||
noPrerequisiteOption: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.noPrerequisiteOption',
|
||||
defaultMessage: 'No prerequisite',
|
||||
},
|
||||
prerequisiteSelectLabel: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.prerequisiteSelectLabel',
|
||||
defaultMessage: 'Prerequisite:',
|
||||
},
|
||||
minScoreLabel: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minScoreLabel',
|
||||
defaultMessage: 'Minimum score:',
|
||||
},
|
||||
minCompletionLabel: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minCompletionLabel',
|
||||
defaultMessage: 'Minimum completion:',
|
||||
},
|
||||
minScoreError: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minScoreError',
|
||||
defaultMessage: 'The minimum score percentage must be a whole number between 0 and 100.',
|
||||
},
|
||||
minCompletionError: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.minCompletionError',
|
||||
defaultMessage: 'The minimum completion percentage must be a whole number between 0 and 100.',
|
||||
},
|
||||
proctoredExamLockedAndisNotProctoredExamAlert: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.proctoredExamLockedAndisNotProctoredExamAlert',
|
||||
defaultMessage: 'This subsection was released to learners as a proctored exam, but was reverted back to a basic or timed exam. You may not configure it as a proctored exam now. Contact edX Support for assistance.',
|
||||
},
|
||||
proctoredExamLockedAndisProctoredExamAlert: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.proctoredExamLockedAndisProctoredExamAlert',
|
||||
defaultMessage: 'This proctored exam has been released to learners. You may not convert it to another type of special exam. You may revert this subsection back to being a basic exam by selecting \'None\', or a timed exam, but you will NOT be able to configure it as a proctored exam in the future.',
|
||||
},
|
||||
reviewRulesLabel: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesLabel',
|
||||
defaultMessage: 'Review rules',
|
||||
},
|
||||
reviewRulesDescription: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesDescription',
|
||||
defaultMessage: 'Specify any rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed. These specified rules are visible to learners before the learners start the exam.',
|
||||
},
|
||||
reviewRulesDescriptionWithLink: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesDescriptionWithLink',
|
||||
defaultMessage: 'Specify any rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed. These specified rules are visible to learners before the learners start the exam, along with the {hyperlink}.',
|
||||
},
|
||||
reviewRulesDescriptionLinkText: {
|
||||
id: 'course-authoring.course-outline.configure-modal.advanced-tab.reviewRulesDescriptionLinkText',
|
||||
defaultMessage: 'general proctored exam rules',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -8,3 +8,4 @@
|
||||
@import "./course-stepper/CouseStepper";
|
||||
@import "./tag-count/TagCount";
|
||||
@import "./modal-dropzone/ModalDropzone";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
|
||||
Reference in New Issue
Block a user