Skip to content

Commit

Permalink
feat: add Section Configure
Browse files Browse the repository at this point in the history
  • Loading branch information
CefBoud committed Nov 30, 2023
1 parent 7d988b4 commit a996739
Show file tree
Hide file tree
Showing 14 changed files with 443 additions and 2 deletions.
11 changes: 11 additions & 0 deletions src/course-outline/CourseOutline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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 @@ -51,11 +52,14 @@ const CourseOutline = ({ courseId }) => {
isDisabledReindexButton,
isHighlightsModalOpen,
isPublishModalOpen,
isConfigureModalOpen,
isDeleteModalOpen,
closeHighlightsModal,
closePublishModal,
closeConfigureModal,
closeDeleteModal,
openPublishModal,
openConfigureModal,
openDeleteModal,
headerNavigationsActions,
openEnableHighlightsModal,
Expand All @@ -65,6 +69,7 @@ const CourseOutline = ({ courseId }) => {
handleOpenHighlightsModal,
handleHighlightsFormSubmit,
handlePublishSectionSubmit,
handleConfigureSectionSubmit,
handleEditSectionSubmit,
handleDeleteSectionSubmit,
handleDuplicateSectionSubmit,
Expand Down Expand Up @@ -143,6 +148,7 @@ const CourseOutline = ({ courseId }) => {
savingStatus={savingStatus}
onOpenHighlightsModal={handleOpenHighlightsModal}
onOpenPublishModal={openPublishModal}
onOpenConfigureModal={openConfigureModal}
onOpenDeleteModal={openDeleteModal}
onEditSectionSubmit={handleEditSectionSubmit}
onDuplicateSubmit={handleDuplicateSectionSubmit}
Expand Down Expand Up @@ -188,6 +194,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";
6 changes: 4 additions & 2 deletions 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 @@ -82,7 +83,7 @@ const CardHeader = ({
<Tooltip
id={intl.formatMessage(messages.expandTooltip)}
className="section-card-header-tooltip"
>
> useToggle
{intl.formatMessage(messages.expandTooltip)}
</Tooltip>
)}
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);

Check warning on line 11 in src/course-outline/configure-modal/BasicTab.jsx

View check run for this annotation

Codecov / codecov/patch

src/course-outline/configure-modal/BasicTab.jsx#L11

Added line #L11 was not covered by tests
};

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)}

Check warning on line 24 in src/course-outline/configure-modal/BasicTab.jsx

View check run for this annotation

Codecov / codecov/patch

src/course-outline/configure-modal/BasicTab.jsx#L24

Added line #L24 was not covered by tests
/>
<DatepickerControl
type={DATEPICKER_TYPES.time}
value={releaseDate}
label={intl.formatMessage(messages.releaseTimeUTC)}
controlName="start-time"
onChange={(date) => onChange(date)}

Check warning on line 31 in src/course-outline/configure-modal/BasicTab.jsx

View check run for this annotation

Codecov / codecov/patch

src/course-outline/configure-modal/BasicTab.jsx#L31

Added line #L31 was not covered by tests
/>
</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);

Check warning on line 45 in src/course-outline/configure-modal/ConfigureModal.jsx

View check run for this annotation

Codecov / codecov/patch

src/course-outline/configure-modal/ConfigureModal.jsx#L45

Added line #L45 was not covered by tests
};

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;
}
}
131 changes: 131 additions & 0 deletions src/course-outline/configure-modal/ConfigureModal.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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('calls the onClose function when the cancel button is clicked', () => {
const { getByRole } = renderComponent();

const cancelButton = getByRole('button', { name: messages.cancelButton.defaultMessage });
fireEvent.click(cancelButton);
expect(onCloseMock).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit a996739

Please sign in to comment.