feat: add Section Configure
This commit is contained in:
committed by
Kristin Aoki
parent
134b75568a
commit
7286b21f5a
@@ -30,6 +30,7 @@ import SectionCard from './section-card/SectionCard';
|
||||
import HighlightsModal from './highlights-modal/HighlightsModal';
|
||||
import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
|
||||
import PublishModal from './publish-modal/PublishModal';
|
||||
import ConfigureModal from './configure-modal/ConfigureModal';
|
||||
import DeleteModal from './delete-modal/DeleteModal';
|
||||
import { useCourseOutline } from './hooks';
|
||||
import messages from './messages';
|
||||
@@ -52,11 +53,14 @@ const CourseOutline = ({ courseId }) => {
|
||||
isDisabledReindexButton,
|
||||
isHighlightsModalOpen,
|
||||
isPublishModalOpen,
|
||||
isConfigureModalOpen,
|
||||
isDeleteModalOpen,
|
||||
closeHighlightsModal,
|
||||
closePublishModal,
|
||||
closeConfigureModal,
|
||||
closeDeleteModal,
|
||||
openPublishModal,
|
||||
openConfigureModal,
|
||||
openDeleteModal,
|
||||
headerNavigationsActions,
|
||||
openEnableHighlightsModal,
|
||||
@@ -66,6 +70,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
handleOpenHighlightsModal,
|
||||
handleHighlightsFormSubmit,
|
||||
handlePublishSectionSubmit,
|
||||
handleConfigureSectionSubmit,
|
||||
handleEditSectionSubmit,
|
||||
handleDeleteSectionSubmit,
|
||||
handleDuplicateSectionSubmit,
|
||||
@@ -145,6 +150,7 @@ const CourseOutline = ({ courseId }) => {
|
||||
savingStatus={savingStatus}
|
||||
onOpenHighlightsModal={handleOpenHighlightsModal}
|
||||
onOpenPublishModal={openPublishModal}
|
||||
onOpenConfigureModal={openConfigureModal}
|
||||
onOpenDeleteModal={openDeleteModal}
|
||||
onEditSectionSubmit={handleEditSectionSubmit}
|
||||
onDuplicateSubmit={handleDuplicateSectionSubmit}
|
||||
@@ -190,6 +196,11 @@ const CourseOutline = ({ courseId }) => {
|
||||
onClose={closePublishModal}
|
||||
onPublishSubmit={handlePublishSectionSubmit}
|
||||
/>
|
||||
<ConfigureModal
|
||||
isOpen={isConfigureModalOpen}
|
||||
onClose={closeConfigureModal}
|
||||
onConfigureSubmit={handleConfigureSectionSubmit}
|
||||
/>
|
||||
<DeleteModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
close={closeDeleteModal}
|
||||
|
||||
@@ -5,3 +5,4 @@
|
||||
@import "./empty-placeholder/EmptyPlaceholder";
|
||||
@import "./highlights-modal/HighlightsModal";
|
||||
@import "./publish-modal/PublishModal";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
|
||||
@@ -44,6 +44,7 @@ import { executeThunk } from '../utils';
|
||||
import CourseOutline from './CourseOutline';
|
||||
import messages from './messages';
|
||||
import headerMessages from './header-navigations/messages';
|
||||
import cardHeaderMessages from './card-header/messages';
|
||||
|
||||
let axiosMock;
|
||||
let store;
|
||||
@@ -324,6 +325,55 @@ describe('<CourseOutline />', () => {
|
||||
expect(firstSection.querySelector('.section-card-header__badge-status')).toHaveTextContent('Published not live');
|
||||
});
|
||||
|
||||
it('check configure section when configure query is successful', async () => {
|
||||
cleanup();
|
||||
const { getAllByTestId, getByText, getByPlaceholderText } = render(<RootWrapper />);
|
||||
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
|
||||
const newReleaseDate = '2025-08-10T10:00:00Z';
|
||||
axiosMock
|
||||
.onPost(getUpdateCourseSectionApiUrl(section.id), {
|
||||
id: section.id,
|
||||
data: null,
|
||||
metadata: {
|
||||
display_name: section.displayName,
|
||||
start: newReleaseDate,
|
||||
visible_to_staff_only: true,
|
||||
},
|
||||
})
|
||||
.reply(200);
|
||||
|
||||
axiosMock
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
.reply(200, section);
|
||||
|
||||
await executeThunk(fetchCourseSectionQuery(section.id), store.dispatch);
|
||||
|
||||
const firstSection = getAllByTestId('section-card')[0];
|
||||
|
||||
const sectionDropdownButton = firstSection.querySelector('#section-card-header__menu');
|
||||
expect(sectionDropdownButton).toBeInTheDocument();
|
||||
fireEvent.click(sectionDropdownButton);
|
||||
|
||||
const configureBtn = getByText(cardHeaderMessages.menuConfigure.defaultMessage);
|
||||
fireEvent.click(configureBtn);
|
||||
|
||||
const datePicker = getByPlaceholderText('MM/DD/YYYY');
|
||||
fireEvent.change(datePicker, { target: { value: '08/10/2025' } });
|
||||
|
||||
axiosMock
|
||||
.onGet(getXBlockApiUrl(section.id))
|
||||
.reply(200, {
|
||||
...section,
|
||||
start: newReleaseDate,
|
||||
});
|
||||
|
||||
fireEvent.click(getByText('Save'));
|
||||
fireEvent.click(sectionDropdownButton);
|
||||
fireEvent.click(configureBtn);
|
||||
|
||||
expect(datePicker).toHaveValue('08/10/2025');
|
||||
});
|
||||
|
||||
it('check update highlights when update highlights query is successfully', async () => {
|
||||
const { getByRole } = render(<RootWrapper />);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ const CardHeader = ({
|
||||
hasChanges,
|
||||
isExpanded,
|
||||
onClickPublish,
|
||||
onClickConfigure,
|
||||
onClickMenuButton,
|
||||
onClickEdit,
|
||||
onExpand,
|
||||
@@ -136,7 +137,7 @@ const CardHeader = ({
|
||||
>
|
||||
{intl.formatMessage(messages.menuPublish)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item>{intl.formatMessage(messages.menuConfigure)}</Dropdown.Item>
|
||||
<Dropdown.Item onClick={onClickConfigure}>{intl.formatMessage(messages.menuConfigure)}</Dropdown.Item>
|
||||
<Dropdown.Item onClick={onClickDuplicate}>{intl.formatMessage(messages.menuDuplicate)}</Dropdown.Item>
|
||||
<Dropdown.Item onClick={onClickDelete}>{intl.formatMessage(messages.menuDelete)}</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
@@ -153,6 +154,7 @@ CardHeader.propTypes = {
|
||||
isExpanded: PropTypes.bool.isRequired,
|
||||
onExpand: PropTypes.func.isRequired,
|
||||
onClickPublish: PropTypes.func.isRequired,
|
||||
onClickConfigure: PropTypes.func.isRequired,
|
||||
onClickMenuButton: PropTypes.func.isRequired,
|
||||
onClickEdit: PropTypes.func.isRequired,
|
||||
isFormOpen: PropTypes.bool.isRequired,
|
||||
|
||||
43
src/course-outline/configure-modal/BasicTab.jsx
Normal file
43
src/course-outline/configure-modal/BasicTab.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Stack } 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 intl = useIntl();
|
||||
const onChange = (value) => {
|
||||
setReleaseDate(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mt-3"><FormattedMessage {...messages.releaseDateAndTime} /></h3>
|
||||
<hr />
|
||||
<Stack direction="horizontal" gap={5}>
|
||||
<DatepickerControl
|
||||
type={DATEPICKER_TYPES.date}
|
||||
value={releaseDate}
|
||||
label={intl.formatMessage(messages.releaseDate)}
|
||||
controlName="state-date"
|
||||
onChange={(date) => onChange(date)}
|
||||
/>
|
||||
<DatepickerControl
|
||||
type={DATEPICKER_TYPES.time}
|
||||
value={releaseDate}
|
||||
label={intl.formatMessage(messages.releaseTimeUTC)}
|
||||
controlName="start-time"
|
||||
onChange={(date) => onChange(date)}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BasicTab.propTypes = {
|
||||
releaseDate: PropTypes.string.isRequired,
|
||||
setReleaseDate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(BasicTab);
|
||||
95
src/course-outline/configure-modal/ConfigureModal.jsx
Normal file
95
src/course-outline/configure-modal/ConfigureModal.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable import/named */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ModalDialog,
|
||||
Button,
|
||||
ActionRow,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from '@edx/paragon';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { VisibilityTypes } from '../../data/constants';
|
||||
import { getCurrentSection } from '../data/selectors';
|
||||
import messages from './messages';
|
||||
import BasicTab from './BasicTab';
|
||||
import VisibilityTab from './VisibilityTab';
|
||||
|
||||
const ConfigureModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfigureSubmit,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { displayName, start: sectionStartDate, visibilityState } = useSelector(getCurrentSection);
|
||||
const [releaseDate, setReleaseDate] = useState(sectionStartDate);
|
||||
const [isVisibleToStaffOnly, setIsVisibleToStaffOnly] = useState(visibilityState === VisibilityTypes.STAFF_ONLY);
|
||||
const [saveButtonDisabled, setSaveButtonDisabled] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setReleaseDate(sectionStartDate);
|
||||
}, [sectionStartDate]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisibleToStaffOnly(visibilityState === VisibilityTypes.STAFF_ONLY);
|
||||
}, [visibilityState]);
|
||||
|
||||
useEffect(() => {
|
||||
const visibilityUnchanged = isVisibleToStaffOnly === (visibilityState === VisibilityTypes.STAFF_ONLY);
|
||||
setSaveButtonDisabled(visibilityUnchanged && releaseDate === sectionStartDate);
|
||||
}, [releaseDate, isVisibleToStaffOnly]);
|
||||
|
||||
const handleSave = () => {
|
||||
onConfigureSubmit(isVisibleToStaffOnly, releaseDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
className="configure-modal"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<ModalDialog.Header className="configure-modal__header">
|
||||
<ModalDialog.Title>
|
||||
{intl.formatMessage(messages.title, { title: displayName })}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body className="configure-modal__body">
|
||||
<Tabs>
|
||||
<Tab eventKey="basic" title={intl.formatMessage(messages.basicTabTitle)}>
|
||||
<BasicTab releaseDate={releaseDate} setReleaseDate={setReleaseDate} />
|
||||
</Tab>
|
||||
<Tab eventKey="visibility" title={intl.formatMessage(messages.visibilityTabTitle)}>
|
||||
<VisibilityTab
|
||||
isVisibleToStaffOnly={isVisibleToStaffOnly}
|
||||
setIsVisibleToStaffOnly={setIsVisibleToStaffOnly}
|
||||
showWarning={visibilityState === VisibilityTypes.STAFF_ONLY}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer className="pt-1">
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages.cancelButton)}
|
||||
</ModalDialog.CloseButton>
|
||||
<Button onClick={handleSave} disabled={saveButtonDisabled}>
|
||||
{intl.formatMessage(messages.saveButton)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigureModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onConfigureSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ConfigureModal;
|
||||
12
src/course-outline/configure-modal/ConfigureModal.scss
Normal file
12
src/course-outline/configure-modal/ConfigureModal.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.configure-modal {
|
||||
max-width: 33.6875rem;
|
||||
overflow: visible;
|
||||
|
||||
.configure-modal__header {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.configure-modal__body {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
139
src/course-outline/configure-modal/ConfigureModal.test.jsx
Normal file
139
src/course-outline/configure-modal/ConfigureModal.test.jsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
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 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 currentSectionMock = {
|
||||
displayName: 'Section1',
|
||||
childInfo: {
|
||||
displayName: 'Subsection',
|
||||
children: [
|
||||
{
|
||||
displayName: 'Subsection 1',
|
||||
id: 1,
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
displayName: 'Subsection_1 Unit 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subsection 2',
|
||||
id: 2,
|
||||
childInfo: {
|
||||
displayName: 'Unit',
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
displayName: 'Subsection_2 Unit 1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Subsection 3',
|
||||
id: 3,
|
||||
childInfo: {
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const onCloseMock = jest.fn();
|
||||
const onConfigureSubmitMock = jest.fn();
|
||||
|
||||
const renderComponent = () => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
<ConfigureModal
|
||||
isOpen
|
||||
onClose={onCloseMock}
|
||||
onConfigureSubmit={onConfigureSubmitMock}
|
||||
/>
|
||||
</IntlProvider>,
|
||||
</AppProvider>,
|
||||
);
|
||||
|
||||
describe('<ConfigureModal />', () => {
|
||||
beforeEach(() => {
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'abc123',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
|
||||
store = initializeStore();
|
||||
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
useSelector.mockReturnValue(currentSectionMock);
|
||||
});
|
||||
|
||||
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 });
|
||||
fireEvent.click(visibilityTab);
|
||||
expect(getByText(messages.sectionVisibility.defaultMessage)).toBeInTheDocument();
|
||||
expect(getByText(messages.hideFromLearners.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the Save button and enables it if there is a change', () => {
|
||||
const { getByRole, getByPlaceholderText, getByTestId } = renderComponent();
|
||||
|
||||
const saveButton = getByRole('button', { name: messages.saveButton.defaultMessage });
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
const input = getByPlaceholderText('MM/DD/YYYY');
|
||||
fireEvent.change(input, { target: { value: '12/15/2023' } });
|
||||
|
||||
const visibilityTab = getByRole('tab', { name: messages.visibilityTabTitle.defaultMessage });
|
||||
fireEvent.click(visibilityTab);
|
||||
const checkbox = getByTestId('visibility-checkbox');
|
||||
fireEvent.click(checkbox);
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
38
src/course-outline/configure-modal/VisibilityTab.jsx
Normal file
38
src/course-outline/configure-modal/VisibilityTab.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Form } from '@edx/paragon';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
const VisibilityTab = ({ isVisibleToStaffOnly, setIsVisibleToStaffOnly, showWarning }) => {
|
||||
const handleChange = (e) => {
|
||||
setIsVisibleToStaffOnly(e.target.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mt-3"><FormattedMessage {...messages.sectionVisibility} /></h3>
|
||||
<hr />
|
||||
<Form.Checkbox checked={isVisibleToStaffOnly} onChange={handleChange} data-testid="visibility-checkbox">
|
||||
<FormattedMessage {...messages.hideFromLearners} />
|
||||
</Form.Checkbox>
|
||||
{showWarning && (
|
||||
<>
|
||||
<hr />
|
||||
<Alert variant="warning">
|
||||
<FormattedMessage {...messages.visibilityWarning} />
|
||||
</Alert>
|
||||
</>
|
||||
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
VisibilityTab.propTypes = {
|
||||
isVisibleToStaffOnly: PropTypes.bool.isRequired,
|
||||
showWarning: PropTypes.bool.isRequired,
|
||||
setIsVisibleToStaffOnly: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(VisibilityTab);
|
||||
50
src/course-outline/configure-modal/messages.js
Normal file
50
src/course-outline/configure-modal/messages.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'course-authoring.course-outline.configure-modal.title',
|
||||
defaultMessage: '{title} Settings',
|
||||
},
|
||||
basicTabTitle: {
|
||||
id: 'course-authoring.course-outline.configure-modal.basic-tab.title',
|
||||
defaultMessage: 'Basic',
|
||||
},
|
||||
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',
|
||||
},
|
||||
sectionVisibility: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.section-visibility',
|
||||
defaultMessage: 'Section Visibility',
|
||||
},
|
||||
hideFromLearners: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.hide-from-learners',
|
||||
defaultMessage: 'Hide from learners',
|
||||
},
|
||||
visibilityWarning: {
|
||||
id: 'course-authoring.course-outline.configure-modal.visibility-tab.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 unit. Only units that are explicitly hidden from learners will remain hidden after you clear this option for the section.',
|
||||
},
|
||||
cancelButton: {
|
||||
id: 'course-authoring.course-outline.configure-modal.button.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
saveButton: {
|
||||
id: 'course-authoring.course-outline.configure-modal.button.label',
|
||||
defaultMessage: 'Save',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -215,6 +215,25 @@ export async function publishCourseSection(sectionId) {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure course section
|
||||
* @param {string} sectionId
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export async function configureCourseSection(sectionId, isVisibleToStaffOnly, startDatetime) {
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(getUpdateCourseSectionApiUrl(sectionId), {
|
||||
publish: 'republish',
|
||||
metadata: {
|
||||
// The backend expects metadata.visible_to_staff_only to either true or null
|
||||
visible_to_staff_only: isVisibleToStaffOnly ? true : null,
|
||||
start: startDatetime,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit course section
|
||||
* @param {string} sectionId
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getCourseOutlineIndex,
|
||||
getCourseSection,
|
||||
publishCourseSection,
|
||||
configureCourseSection,
|
||||
restartIndexingOnCourse,
|
||||
updateCourseSectionHighlights,
|
||||
} from './api';
|
||||
@@ -177,6 +178,26 @@ export function publishCourseSectionQuery(sectionId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function configureCourseSectionQuery(sectionId, isVisibleToStaffOnly, startDatetime) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
|
||||
|
||||
try {
|
||||
await configureCourseSection(sectionId, isVisibleToStaffOnly, startDatetime).then(async (result) => {
|
||||
if (result) {
|
||||
await dispatch(fetchCourseSectionQuery(sectionId));
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
dispatch(hideProcessingNotification());
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function editCourseSectionQuery(sectionId, displayName) {
|
||||
return async (dispatch) => {
|
||||
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
fetchCourseReindexQuery,
|
||||
publishCourseSectionQuery,
|
||||
updateCourseSectionHighlightsQuery,
|
||||
configureCourseSectionQuery,
|
||||
} from './data/thunk';
|
||||
|
||||
const useCourseOutline = ({ courseId }) => {
|
||||
@@ -46,6 +47,7 @@ const useCourseOutline = ({ courseId }) => {
|
||||
const [showErrorAlert, setShowErrorAlert] = useState(false);
|
||||
const [isHighlightsModalOpen, openHighlightsModal, closeHighlightsModal] = useToggle(false);
|
||||
const [isPublishModalOpen, openPublishModal, closePublishModal] = useToggle(false);
|
||||
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
|
||||
|
||||
const handleNewSectionSubmit = () => {
|
||||
@@ -96,6 +98,12 @@ const useCourseOutline = ({ courseId }) => {
|
||||
closePublishModal();
|
||||
};
|
||||
|
||||
const handleConfigureSectionSubmit = (isVisibleToStaffOnly, startDatetime) => {
|
||||
dispatch(configureCourseSectionQuery(currentSection.id, isVisibleToStaffOnly, startDatetime));
|
||||
|
||||
closeConfigureModal();
|
||||
};
|
||||
|
||||
const handleEditSectionSubmit = (sectionId, displayName) => {
|
||||
dispatch(editCourseSectionQuery(sectionId, displayName));
|
||||
};
|
||||
@@ -137,10 +145,14 @@ const useCourseOutline = ({ courseId }) => {
|
||||
isPublishModalOpen,
|
||||
openPublishModal,
|
||||
closePublishModal,
|
||||
isConfigureModalOpen,
|
||||
openConfigureModal,
|
||||
closeConfigureModal,
|
||||
headerNavigationsActions,
|
||||
handleEnableHighlightsSubmit,
|
||||
handleHighlightsFormSubmit,
|
||||
handlePublishSectionSubmit,
|
||||
handleConfigureSectionSubmit,
|
||||
handleEditSectionSubmit,
|
||||
statusBarData,
|
||||
isEnableHighlightsModalOpen,
|
||||
|
||||
@@ -16,6 +16,7 @@ const SectionCard = ({
|
||||
children,
|
||||
onOpenHighlightsModal,
|
||||
onOpenPublishModal,
|
||||
onOpenConfigureModal,
|
||||
onEditSectionSubmit,
|
||||
savingStatus,
|
||||
onOpenDeleteModal,
|
||||
@@ -89,6 +90,7 @@ const SectionCard = ({
|
||||
onExpand={handleExpandContent}
|
||||
onClickMenuButton={handleClickMenuButton}
|
||||
onClickPublish={onOpenPublishModal}
|
||||
onClickConfigure={onOpenConfigureModal}
|
||||
onClickEdit={openForm}
|
||||
onClickDelete={onOpenDeleteModal}
|
||||
isFormOpen={isFormOpen}
|
||||
@@ -149,6 +151,7 @@ SectionCard.propTypes = {
|
||||
children: PropTypes.node,
|
||||
onOpenHighlightsModal: PropTypes.func.isRequired,
|
||||
onOpenPublishModal: PropTypes.func.isRequired,
|
||||
onOpenConfigureModal: PropTypes.func.isRequired,
|
||||
onEditSectionSubmit: PropTypes.func.isRequired,
|
||||
savingStatus: PropTypes.string.isRequired,
|
||||
onOpenDeleteModal: PropTypes.func.isRequired,
|
||||
|
||||
@@ -41,3 +41,8 @@ export const DivisionSchemes = {
|
||||
NONE: 'none',
|
||||
COHORT: 'cohort',
|
||||
};
|
||||
|
||||
export const VisibilityTypes = {
|
||||
STAFF_ONLY: 'staff_only',
|
||||
HIDE_AFTER_DUE: 'hide_after_due',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user