diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx
index b593d29b9..78a14c2d8 100644
--- a/src/course-outline/CourseOutline.jsx
+++ b/src/course-outline/CourseOutline.jsx
@@ -81,7 +81,7 @@ const CourseOutline = ({ courseId }) => {
handleInternetConnectionFailed,
handleOpenHighlightsModal,
handleHighlightsFormSubmit,
- handleConfigureSectionSubmit,
+ handleConfigureSubmit,
handlePublishItemSubmit,
handleEditSubmit,
handleDeleteItemSubmit,
@@ -339,6 +339,7 @@ const CourseOutline = ({ courseId }) => {
onOpenDeleteModal={openDeleteModal}
onEditSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateSubsectionSubmit}
+ onOpenConfigureModal={openConfigureModal}
onNewUnitSubmit={handleNewUnitSubmit}
onOrderChange={updateSubsectionOrderByIndex(
sectionIndex,
@@ -428,7 +429,7 @@ const CourseOutline = ({ courseId }) => {
', () => {
expect(datePicker).toHaveValue('08/10/2025');
});
+ it('check configure subsection when configure subsection query is successful', async () => {
+ const {
+ findAllByTestId,
+ findByText,
+ findAllByRole,
+ findByRole,
+ findByTestId,
+ } = render();
+ const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
+ const subsection = section.childInfo.children[0];
+ const newReleaseDate = '2025-08-10T10:00:00Z';
+ const newGraderType = 'Homework';
+ const newDue = '2025-09-10T10:00:00Z';
+ const isTimeLimited = true;
+ const defaultTimeLimitMinutes = 210;
+
+ axiosMock
+ .onPost(getCourseItemApiUrl(subsection.id), {
+ publish: 'republish',
+ graderType: newGraderType,
+ metadata: {
+ visible_to_staff_only: true,
+ due: newDue,
+ hide_after_due: false,
+ show_correctness: false,
+ is_practice_exam: false,
+ is_time_limited: isTimeLimited,
+ exam_review_rules: '',
+ is_proctored_enabled: false,
+ default_time_limit_minutes: defaultTimeLimitMinutes,
+ is_onboarding_exam: false,
+ start: newReleaseDate,
+ },
+ })
+ .reply(200, { dummy: 'value' });
+
+ axiosMock
+ .onGet(getXBlockApiUrl(section.id))
+ .reply(200, section);
+
+ const [currentSection] = await findAllByTestId('section-card');
+ const [firstSubsection] = await within(currentSection).findAllByTestId('subsection-card');
+ const subsectionDropdownButton = firstSubsection.querySelector('#subsection-card-header__menu');
+ expect(subsectionDropdownButton).toBeInTheDocument();
+
+ subsection.start = newReleaseDate;
+ subsection.due = newDue;
+ subsection.format = newGraderType;
+ subsection.isTimeLimited = isTimeLimited;
+ subsection.defaultTimeLimitMinutes = defaultTimeLimitMinutes;
+ section.childInfo.children[0] = subsection;
+ axiosMock
+ .onGet(getXBlockApiUrl(section.id))
+ .reply(200, section);
+
+ await executeThunk(configureCourseSubsectionQuery(
+ subsection.id,
+ section.id,
+ true,
+ newReleaseDate,
+ newGraderType,
+ newDue,
+ true,
+ defaultTimeLimitMinutes,
+ false,
+ false,
+ ), store.dispatch);
+ fireEvent.click(subsectionDropdownButton);
+ const configureBtn = await within(firstSubsection).findByTestId('subsection-card-header__menu-configure-button');
+ fireEvent.click(configureBtn);
+
+ expect(await findByText(newGraderType)).toBeInTheDocument();
+ const releaseDateStack = await findByTestId('release-date-stack');
+ const releaseDatePicker = await within(releaseDateStack).findByPlaceholderText('MM/DD/YYYY');
+ expect(releaseDatePicker).toHaveValue('08/10/2025');
+ const dueDateStack = await findByTestId('due-date-stack');
+ const dueDatePicker = await within(dueDateStack).findByPlaceholderText('MM/DD/YYYY');
+ expect(dueDatePicker).toHaveValue('09/10/2025');
+
+ const advancedTab = await findByRole('tab', { name: configureModalMessages.advancedTabTitle.defaultMessage });
+ fireEvent.click(advancedTab);
+ const radioButtons = await findAllByRole('radio');
+ expect(radioButtons[0]).toHaveProperty('checked', false);
+ expect(radioButtons[1]).toHaveProperty('checked', true);
+ const hours = await findByTestId('hour-autosuggest');
+ expect(hours).toHaveValue('03:30');
+ });
+
it('check update highlights when update highlights query is successfully', async () => {
const { getByRole } = render();
diff --git a/src/course-outline/configure-modal/AdvancedTab.jsx b/src/course-outline/configure-modal/AdvancedTab.jsx
new file mode 100644
index 000000000..c38b951a9
--- /dev/null
+++ b/src/course-outline/configure-modal/AdvancedTab.jsx
@@ -0,0 +1,112 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { Form } from '@edx/paragon';
+import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
+import messages from './messages';
+
+const timeLimits = Array.from({ length: 48 }, (_, i) => 30 + 30 * i);
+
+const AdvancedTab = ({
+ isTimeLimited,
+ setIsTimeLimited,
+ defaultTimeLimit,
+ setDefaultTimeLimit,
+}) => {
+ const [key, setKey] = useState(0);
+
+ const handleChange = (e) => {
+ if (e.target.value === 'timed') {
+ setIsTimeLimited(true);
+ } else {
+ setDefaultTimeLimit(null);
+ setIsTimeLimited(false);
+ }
+ };
+
+ const setCurrentTimeLimit = (valueString) => {
+ const value = valueString.split(':');
+ setDefaultTimeLimit(Number.parseInt(value[0], 10) * 60 + Number.parseInt(value[1], 10));
+ };
+
+ 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 handleBlur = (e) => {
+ const isValid = /^\d\d:\d\d$/.test(e.target.value);
+ if (isValid) {
+ setCurrentTimeLimit(e.target.value);
+ } else {
+ // Ensure the component re-renders and reset the value to the previous state
+ setKey(key + 1);
+ }
+ };
+
+ const generateTimeLimits = () => timeLimits.map(
+ (hour) => (
+
+ {formatHour(hour)}
+
+ ),
+ );
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ { isTimeLimited && (
+ <>
+
+
+ {generateTimeLimits()}
+
+
+ >
+ )}
+ >
+ );
+};
+
+AdvancedTab.propTypes = {
+ isTimeLimited: PropTypes.bool.isRequired,
+ setIsTimeLimited: PropTypes.func.isRequired,
+ defaultTimeLimit: PropTypes.number.isRequired,
+ setDefaultTimeLimit: PropTypes.func.isRequired,
+};
+
+export default injectIntl(AdvancedTab);
diff --git a/src/course-outline/configure-modal/BasicTab.jsx b/src/course-outline/configure-modal/BasicTab.jsx
index ffdfc81c5..a4f4ac0c0 100644
--- a/src/course-outline/configure-modal/BasicTab.jsx
+++ b/src/course-outline/configure-modal/BasicTab.jsx
@@ -1,36 +1,87 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Stack } from '@edx/paragon';
+import { Stack, Form } from '@edx/paragon';
import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
import { DatepickerControl, DATEPICKER_TYPES } from '../../generic/datepicker-control';
-const BasicTab = ({ releaseDate, setReleaseDate }) => {
+const BasicTab = ({
+ releaseDate,
+ setReleaseDate,
+ isSubsection,
+ graderType,
+ courseGraders,
+ setGraderType,
+ dueDate,
+ setDueDate,
+}) => {
const intl = useIntl();
- const onChange = (value) => {
- setReleaseDate(value);
- };
+
+ const onChangeGraderType = (e) => setGraderType(e.target.value);
+
+ const createOptions = () => courseGraders.map((option) => (
+
+ ));
return (
<>
-
+
-
- onChange(date)}
- />
- onChange(date)}
- />
-
+
+
+
+
+
+
+ {
+ isSubsection && (
+
+
+
+
+
onChangeGraderType(value)}
+ data-testid="grader-type-select"
+ >
+
+ {createOptions()}
+
+
+
+
+
+
+
+
+ )
+ }
>
);
};
@@ -38,6 +89,12 @@ const BasicTab = ({ releaseDate, setReleaseDate }) => {
BasicTab.propTypes = {
releaseDate: PropTypes.string.isRequired,
setReleaseDate: PropTypes.func.isRequired,
+ isSubsection: PropTypes.bool.isRequired,
+ graderType: PropTypes.string.isRequired,
+ setGraderType: PropTypes.func.isRequired,
+ dueDate: PropTypes.string.isRequired,
+ setDueDate: PropTypes.func.isRequired,
+ courseGraders: PropTypes.arrayOf(PropTypes.string).isRequired,
};
export default injectIntl(BasicTab);
diff --git a/src/course-outline/configure-modal/ConfigureModal.jsx b/src/course-outline/configure-modal/ConfigureModal.jsx
index 5490ef894..be26dc3c5 100644
--- a/src/course-outline/configure-modal/ConfigureModal.jsx
+++ b/src/course-outline/configure-modal/ConfigureModal.jsx
@@ -16,6 +16,8 @@ import { getCurrentItem } from '../data/selectors';
import messages from './messages';
import BasicTab from './BasicTab';
import VisibilityTab from './VisibilityTab';
+import AdvancedTab from './AdvancedTab';
+import { COURSE_BLOCK_NAMES } from '../constants';
const ConfigureModal = ({
isOpen,
@@ -23,33 +25,195 @@ const ConfigureModal = ({
onConfigureSubmit,
}) => {
const intl = useIntl();
- const { displayName, start: sectionStartDate, visibilityState } = useSelector(getCurrentItem);
+ const {
+ displayName,
+ start: sectionStartDate,
+ visibilityState,
+ due,
+ isTimeLimited,
+ defaultTimeLimitMinutes,
+ hideAfterDue,
+ showCorrectness,
+ courseGraders,
+ category,
+ format,
+ } = useSelector(getCurrentItem);
const [releaseDate, setReleaseDate] = useState(sectionStartDate);
const [isVisibleToStaffOnly, setIsVisibleToStaffOnly] = useState(visibilityState === VisibilityTypes.STAFF_ONLY);
const [saveButtonDisabled, setSaveButtonDisabled] = useState(true);
+ const [graderType, setGraderType] = useState(format == null ? 'Not Graded' : format);
+ const [dueDateState, setDueDateState] = useState(due == null ? '' : due);
+ const [isTimeLimitedState, setIsTimeLimitedState] = useState(false);
+ const [defaultTimeLimitMin, setDefaultTimeLimitMin] = useState(defaultTimeLimitMinutes);
+ const [hideAfterDueState, setHideAfterDueState] = useState(hideAfterDue === undefined ? false : hideAfterDue);
+ const [showCorrectnessState, setShowCorrectnessState] = useState(false);
+ const isSubsection = category === COURSE_BLOCK_NAMES.sequential.id;
+ /* TODO: The use of these useEffects needs to be updated to use Formik, please see,
+ * https://github.com/open-craft/frontend-app-course-authoring/pull/22#discussion_r1435957797 as reference. */
useEffect(() => {
setReleaseDate(sectionStartDate);
}, [sectionStartDate]);
+ useEffect(() => {
+ setGraderType(format == null ? 'Not Graded' : format);
+ }, [format]);
+
+ useEffect(() => {
+ setDueDateState(due == null ? '' : due);
+ }, [due]);
+
+ useEffect(() => {
+ setIsTimeLimitedState(isTimeLimited);
+ }, [isTimeLimited]);
+
+ useEffect(() => {
+ setDefaultTimeLimitMin(defaultTimeLimitMinutes);
+ }, [defaultTimeLimitMinutes]);
+
+ useEffect(() => {
+ setHideAfterDueState(hideAfterDue === undefined ? false : hideAfterDue);
+ }, [hideAfterDue]);
+
+ useEffect(() => {
+ setShowCorrectnessState(showCorrectness);
+ }, [showCorrectness]);
+
useEffect(() => {
setIsVisibleToStaffOnly(visibilityState === VisibilityTypes.STAFF_ONLY);
}, [visibilityState]);
useEffect(() => {
const visibilityUnchanged = isVisibleToStaffOnly === (visibilityState === VisibilityTypes.STAFF_ONLY);
- setSaveButtonDisabled(visibilityUnchanged && releaseDate === sectionStartDate);
- }, [releaseDate, isVisibleToStaffOnly]);
+ const graderTypeUnchanged = graderType === (format == null ? 'Not Graded' : format);
+ const dueDateUnchanged = dueDateState === (due == null ? '' : due);
+ const hideAfterDueUnchanged = hideAfterDueState === (hideAfterDue === undefined ? false : hideAfterDue);
+ setSaveButtonDisabled(
+ visibilityUnchanged
+ && releaseDate === sectionStartDate
+ && dueDateUnchanged
+ && isTimeLimitedState === isTimeLimited
+ && defaultTimeLimitMin === defaultTimeLimitMinutes
+ && hideAfterDueUnchanged
+ && showCorrectnessState === showCorrectness
+ && graderTypeUnchanged,
+ );
+ }, [
+ releaseDate,
+ isVisibleToStaffOnly,
+ dueDateState,
+ isTimeLimitedState,
+ defaultTimeLimitMin,
+ hideAfterDueState,
+ showCorrectnessState,
+ graderType,
+ ]);
const handleSave = () => {
- onConfigureSubmit(isVisibleToStaffOnly, releaseDate);
+ if (isSubsection) {
+ onConfigureSubmit(
+ isVisibleToStaffOnly,
+ releaseDate,
+ graderType === 'Not Graded' ? 'notgraded' : graderType,
+ dueDateState,
+ isTimeLimitedState,
+ defaultTimeLimitMin,
+ hideAfterDueState,
+ showCorrectnessState,
+ );
+ } else {
+ onConfigureSubmit(isVisibleToStaffOnly, releaseDate);
+ }
+ };
+
+ const handleClose = () => {
+ setReleaseDate(sectionStartDate);
+ setDueDateState(due);
+ setIsTimeLimitedState(isTimeLimited);
+ setDefaultTimeLimitMin(defaultTimeLimitMinutes);
+ setHideAfterDueState(hideAfterDue);
+ setShowCorrectnessState(showCorrectness);
+ setIsVisibleToStaffOnly(visibilityState === VisibilityTypes.STAFF_ONLY);
+ setGraderType(format);
+ onClose();
+ };
+
+ const createTabs = () => {
+ if (isSubsection) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ return (
+
+
+
+
+
+
+
+
+ );
};
return (
@@ -58,19 +222,8 @@ const ConfigureModal = ({
{intl.formatMessage(messages.title, { title: displayName })}
-
-
-
-
-
-
-
-
-
+
+ {createTabs()}
diff --git a/src/course-outline/configure-modal/ConfigureModal.test.jsx b/src/course-outline/configure-modal/ConfigureModal.test.jsx
index b9a331a4e..dbd69917f 100644
--- a/src/course-outline/configure-modal/ConfigureModal.test.jsx
+++ b/src/course-outline/configure-modal/ConfigureModal.test.jsx
@@ -30,12 +30,25 @@ jest.mock('react-router-dom', () => ({
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: [
@@ -49,6 +62,15 @@ const currentSectionMock = {
{
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: [
@@ -62,6 +84,15 @@ const currentSectionMock = {
{
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: [],
},
@@ -117,7 +148,7 @@ describe('', () => {
const visibilityTab = getByRole('tab', { name: messages.visibilityTabTitle.defaultMessage });
fireEvent.click(visibilityTab);
- expect(getByText(messages.sectionVisibility.defaultMessage)).toBeInTheDocument();
+ expect(getByText('Section Visibility')).toBeInTheDocument();
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
});
@@ -137,3 +168,118 @@ describe('', () => {
expect(saveButton).not.toBeDisabled();
});
});
+
+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',
+ },
+ ],
+ },
+};
+
+const renderSubsectionComponent = () => render(
+
+
+
+ ,
+ ,
+);
+
+describe('', () => {
+ beforeEach(() => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+
+ store = initializeStore();
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ useSelector.mockReturnValue(currentSubsectionMock);
+ });
+
+ 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 });
+ fireEvent.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 });
+ fireEvent.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();
+ });
+
+ it('disables the Save button and enables it if there is a change', () => {
+ const { getByRole, getByTestId } = renderSubsectionComponent();
+
+ const saveButton = getByRole('button', { name: messages.saveButton.defaultMessage });
+ expect(saveButton).toBeDisabled();
+
+ const input = getByTestId('grader-type-select');
+ fireEvent.change(input, { target: { value: 'Exam' } });
+ expect(saveButton).not.toBeDisabled();
+ });
+});
diff --git a/src/course-outline/configure-modal/VisibilityTab.jsx b/src/course-outline/configure-modal/VisibilityTab.jsx
index 033f58018..0536e2d35 100644
--- a/src/course-outline/configure-modal/VisibilityTab.jsx
+++ b/src/course-outline/configure-modal/VisibilityTab.jsx
@@ -1,21 +1,108 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Form } from '@edx/paragon';
-import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
+import { FormattedMessage, injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';
+import { COURSE_BLOCK_NAMES } from '../constants';
-const VisibilityTab = ({ isVisibleToStaffOnly, setIsVisibleToStaffOnly, showWarning }) => {
+const VisibilityTab = ({
+ category,
+ isVisibleToStaffOnly,
+ setIsVisibleToStaffOnly,
+ showWarning,
+ isSubsection,
+ hideAfterDue,
+ setHideAfterDue,
+ showCorrectness,
+ setShowCorrectness,
+}) => {
+ const intl = useIntl();
+ const visibilityTitle = COURSE_BLOCK_NAMES[category]?.name;
const handleChange = (e) => {
setIsVisibleToStaffOnly(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') {
+ setIsVisibleToStaffOnly(true);
+ setHideAfterDue(false);
+ } else if (selected === 'hideDue') {
+ setIsVisibleToStaffOnly(false);
+ setHideAfterDue(true);
+ } else {
+ setIsVisibleToStaffOnly(false);
+ setHideAfterDue(false);
+ }
+ };
+
+ const correctnessChanged = (e) => {
+ setShowCorrectness(e.target.value);
+ };
+
return (
<>
-
+
+ {intl.formatMessage(messages.visibilitySectionTitle, { visibilityTitle })}
+
-
-
-
+ {
+ isSubsection ? (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : (
+
+
+
+ )
+ }
{showWarning && (
<>
@@ -30,9 +117,15 @@ const VisibilityTab = ({ isVisibleToStaffOnly, setIsVisibleToStaffOnly, showWarn
};
VisibilityTab.propTypes = {
+ category: PropTypes.string.isRequired,
isVisibleToStaffOnly: PropTypes.bool.isRequired,
showWarning: PropTypes.bool.isRequired,
setIsVisibleToStaffOnly: PropTypes.func.isRequired,
+ isSubsection: PropTypes.bool.isRequired,
+ hideAfterDue: PropTypes.bool.isRequired,
+ setHideAfterDue: PropTypes.func.isRequired,
+ showCorrectness: PropTypes.string.isRequired,
+ setShowCorrectness: PropTypes.func.isRequired,
};
export default injectIntl(VisibilityTab);
diff --git a/src/course-outline/configure-modal/messages.js b/src/course-outline/configure-modal/messages.js
index 3fd9f50bc..bddffaa5a 100644
--- a/src/course-outline/configure-modal/messages.js
+++ b/src/course-outline/configure-modal/messages.js
@@ -25,9 +25,9 @@ const messages = defineMessages({
id: 'course-authoring.course-outline.configure-modal.visibility-tab.title',
defaultMessage: 'Visibility',
},
- sectionVisibility: {
+ visibilitySectionTitle: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.section-visibility',
- defaultMessage: 'Section Visibility',
+ defaultMessage: '{visibilityTitle} Visibility',
},
hideFromLearners: {
id: 'course-authoring.course-outline.configure-modal.visibility-tab.hide-from-learners',
@@ -45,6 +45,106 @@ const messages = defineMessages({
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.',
+ },
+ 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.',
+ },
});
export default messages;
diff --git a/src/course-outline/data/api.js b/src/course-outline/data/api.js
index 4cb083596..46d968d1b 100644
--- a/src/course-outline/data/api.js
+++ b/src/course-outline/data/api.js
@@ -234,6 +234,52 @@ export async function configureCourseSection(sectionId, isVisibleToStaffOnly, st
return data;
}
+/**
+ * Configure course section
+ * @param {string} itemId
+ * @param {string} isVisibleToStaffOnly
+ * @param {string} releaseDate
+ * @param {string} graderType
+ * @param {string} dueDateState
+ * @param {string} isTimeLimitedState
+ * @param {string} defaultTimeLimitMin
+ * @param {string} hideAfterDueState
+ * @param {string} showCorrectnessState
+ * @returns {Promise