Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Section Configure Modal #15

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/course-outline/CourseOutline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -52,11 +53,14 @@ const CourseOutline = ({ courseId }) => {
isDisabledReindexButton,
isHighlightsModalOpen,
isPublishModalOpen,
isConfigureModalOpen,
isDeleteModalOpen,
closeHighlightsModal,
closePublishModal,
closeConfigureModal,
closeDeleteModal,
openPublishModal,
openConfigureModal,
openDeleteModal,
headerNavigationsActions,
openEnableHighlightsModal,
Expand All @@ -66,6 +70,7 @@ const CourseOutline = ({ courseId }) => {
handleOpenHighlightsModal,
handleHighlightsFormSubmit,
handlePublishSectionSubmit,
handleConfigureSectionSubmit,
handleEditSectionSubmit,
handleDeleteSectionSubmit,
handleDuplicateSectionSubmit,
Expand Down Expand Up @@ -145,6 +150,7 @@ const CourseOutline = ({ courseId }) => {
savingStatus={savingStatus}
onOpenHighlightsModal={handleOpenHighlightsModal}
onOpenPublishModal={openPublishModal}
onOpenConfigureModal={openConfigureModal}
onOpenDeleteModal={openDeleteModal}
onEditSectionSubmit={handleEditSectionSubmit}
onDuplicateSubmit={handleDuplicateSectionSubmit}
Expand Down Expand Up @@ -190,6 +196,11 @@ const CourseOutline = ({ courseId }) => {
onClose={closePublishModal}
onPublishSubmit={handlePublishSectionSubmit}
/>
<ConfigureModal
isOpen={isConfigureModalOpen}
onClose={closeConfigureModal}
onConfigureSubmit={handleConfigureSectionSubmit}
/>
<DeleteModal
isOpen={isDeleteModalOpen}
close={closeDeleteModal}
Expand Down
1 change: 1 addition & 0 deletions src/course-outline/CourseOutline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
@import "./empty-placeholder/EmptyPlaceholder";
@import "./highlights-modal/HighlightsModal";
@import "./publish-modal/PublishModal";
@import "./configure-modal/ConfigureModal";
50 changes: 50 additions & 0 deletions src/course-outline/CourseOutline.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 />);

Expand Down
4 changes: 3 additions & 1 deletion src/course-outline/card-header/CardHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const CardHeader = ({
hasChanges,
isExpanded,
onClickPublish,
onClickConfigure,
onClickMenuButton,
onClickEdit,
onExpand,
Expand Down Expand Up @@ -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>
Expand All @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions src/course-outline/configure-modal/BasicTab.jsx
Original file line number Diff line number Diff line change
@@ -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
mavidser marked this conversation as resolved.
Show resolved Hide resolved
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 changes: 95 additions & 0 deletions src/course-outline/configure-modal/ConfigureModal.jsx
Original file line number Diff line number Diff line change
@@ -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 changes: 12 additions & 0 deletions src/course-outline/configure-modal/ConfigureModal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.configure-modal {
max-width: 33.6875rem;
overflow: visible;

.configure-modal__header {
padding-top: 1.5rem;
}

.configure-modal__body {
overflow: visible;
}
}
Loading