Skip to content

Commit

Permalink
feat: Course outline - Section Publish (#61)
Browse files Browse the repository at this point in the history
* feat: [2u-354] add publish modal, api and update tests

* feat: [2u-354] refactor modal

* fix: [2u-354] removed comments

* fix: [2u-354] fix indents

* fix: [2u-354] removed translates duplicates

* fix: [2u-354] rename handlers
  • Loading branch information
vladislavkeblysh authored and navinkarkera committed Dec 5, 2023
1 parent a9c148e commit e1cf27d
Show file tree
Hide file tree
Showing 13 changed files with 415 additions and 17 deletions.
17 changes: 16 additions & 1 deletion src/course-outline/CourseOutline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import EnableHighlightsModal from './enable-highlights-modal/EnableHighlightsMod
import SectionCard from './section-card/SectionCard';
import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
import messages from './messages';
import PublishModal from './publish-modal/PublishModal';

const CourseOutline = ({ courseId }) => {
const intl = useIntl();
Expand All @@ -39,11 +40,16 @@ const CourseOutline = ({ courseId }) => {
isEnableHighlightsModalOpen,
isInternetConnectionAlertFailed,
isDisabledReindexButton,
isPublishModalOpen,
closePublishModal,
openPublishModal,
headerNavigationsActions,
openEnableHighlightsModal,
closeEnableHighlightsModal,
handleEnableHighlightsSubmit,
handleInternetConnectionFailed,
handleOpenHighlightsModal,
handleSubmitPublishSection,
} = useCourseOutline({ courseId });

if (isLoading) {
Expand Down Expand Up @@ -103,7 +109,11 @@ const CourseOutline = ({ courseId }) => {
<div className="pt-4">
{/* TODO add create new section handler in EmptyPlaceholder */}
{sectionsList.length ? sectionsList.map((section) => (
<SectionCard section={section} />
<SectionCard
section={section}
onOpenHighlightsModal={handleOpenHighlightsModal}
onOpenPublishModal={openPublishModal}
/>
)) : (
<EmptyPlaceholder onCreateNewSection={() => ({})} />
)}
Expand All @@ -123,6 +133,11 @@ const CourseOutline = ({ courseId }) => {
highlightsDocUrl={statusBarData.highlightsDocUrl}
/>
</section>
<PublishModal
isOpen={isPublishModalOpen}
onClose={closePublishModal}
onPublishSubmit={handleSubmitPublishSection}
/>
</Container>
<div className="alert-toast">
<InternetConnectionAlert
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 @@ -4,3 +4,4 @@
@import "./card-header/CardHeader";
@import "./empty-placeholder/EmptyPlaceholder";
@import "./highlights-modal/HighlightsModal";
@import "./publish-modal/PublishModal";
26 changes: 21 additions & 5 deletions src/course-outline/card-header/CardHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ const CardHeader = ({
title,
sectionStatus,
isExpanded,
handleExpand,
onExpand,
onClickPublish,
onClickMenuButton,
}) => {
const intl = useIntl();

const { badgeTitle, badgeIcon } = getSectionStatusBadgeContent(sectionStatus, messages, intl);
const isDisabledPublish = sectionStatus === SECTION_BADGE_STATUTES.live
|| sectionStatus === SECTION_BADGE_STATUTES.publishedNotLive;

return (
<div className="section-card-header" data-testid="section-card-header">
Expand All @@ -48,7 +52,7 @@ const CardHeader = ({
className={classNames('section-card-header__expanded-btn', {
collapsed: !isExpanded,
})}
onClick={() => handleExpand((prevState) => !prevState)}
onClick={() => onExpand((prevState) => !prevState)}
>
<Truncate lines={1} className="h3 mb-0">{title}</Truncate>
{badgeTitle && (
Expand All @@ -65,18 +69,28 @@ const CardHeader = ({
)}
</Button>
</OverlayTrigger>
<Dropdown data-testid="section-card-header__menu" className="ml-auto">
<Dropdown
data-testid="section-card-header__menu"
className="ml-auto"
onClick={onClickMenuButton}
>
<Dropdown.Toggle
className="section-card-header__menu"
id="section-card-header__menu"
data-testid="section-card-header__menu-button"
as={IconButton}
src={MoveVertIcon}
alt="section-card-header__menu"
iconAs={Icon}
/>
<Dropdown.Menu>
<Dropdown.Item>{intl.formatMessage(messages.menuEdit)}</Dropdown.Item>
<Dropdown.Item>{intl.formatMessage(messages.menuPublish)}</Dropdown.Item>
<Dropdown.Item
disabled={isDisabledPublish}
onClick={onClickPublish}
>
{intl.formatMessage(messages.menuPublish)}
</Dropdown.Item>
<Dropdown.Item>{intl.formatMessage(messages.menuConfigure)}</Dropdown.Item>
<Dropdown.Item>{intl.formatMessage(messages.menuDuplicate)}</Dropdown.Item>
<Dropdown.Item>{intl.formatMessage(messages.menuDelete)}</Dropdown.Item>
Expand All @@ -90,7 +104,9 @@ CardHeader.propTypes = {
title: PropTypes.string.isRequired,
sectionStatus: PropTypes.string.isRequired,
isExpanded: PropTypes.bool.isRequired,
handleExpand: PropTypes.func.isRequired,
onExpand: PropTypes.func.isRequired,
onClickPublish: PropTypes.func.isRequired,
onClickMenuButton: PropTypes.func.isRequired,
};

export default CardHeader;
43 changes: 40 additions & 3 deletions src/course-outline/card-header/CardHeader.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ import { SECTION_BADGE_STATUTES } from '../constants';
import CardHeader from './CardHeader';
import messages from './messages';

const handleExpandMock = jest.fn();
const onExpandMock = jest.fn();
const onClickMenuButtonMock = jest.fn();
const onClickPublishMock = jest.fn();

const cardHeaderProps = {
title: 'Some title',
sectionStatus: SECTION_BADGE_STATUTES.live,
isExpanded: true,
handleExpand: handleExpandMock,
onExpand: onExpandMock,
onClickMenuButton: onClickMenuButtonMock,
onClickPublish: onClickPublishMock,
};

const renderComponent = (props) => render(
Expand Down Expand Up @@ -66,11 +70,44 @@ describe('<CardHeader />', () => {
expect(getByText(messages.statusBadgeDraft.defaultMessage)).toBeInTheDocument();
});

it('check publish menu item is disabled when section status is live or published not live', async () => {
const { getByText, getByTestId } = renderComponent({
...cardHeaderProps,
sectionStatus: SECTION_BADGE_STATUTES.publishedNotLive,
});

const menuButton = getByTestId('section-card-header__menu-button');
fireEvent.click(menuButton);
expect(getByText(messages.menuPublish.defaultMessage)).toHaveAttribute('aria-disabled', 'true');
});

it('calls handleExpanded when button is clicked', () => {
const { getByTestId } = renderComponent();

const expandButton = getByTestId('section-card-header__expanded-btn');
fireEvent.click(expandButton);
expect(handleExpandMock).toHaveBeenCalled();
expect(onExpandMock).toHaveBeenCalled();
});

it('calls onMenuButtonClick when menu is clicked', () => {
const { getByTestId } = renderComponent();

const menuButton = getByTestId('section-card-header__menu-button');
fireEvent.click(menuButton);
expect(onClickMenuButtonMock).toHaveBeenCalled();
});

it('calls onClickPublish when item is clicked', () => {
const { getByText, getByTestId } = renderComponent({
...cardHeaderProps,
sectionStatus: SECTION_BADGE_STATUTES.draft,
});

const menuButton = getByTestId('section-card-header__menu-button');
fireEvent.click(menuButton);

const publishMenuItem = getByText(messages.menuPublish.defaultMessage);
fireEvent.click(publishMenuItem);
expect(onClickPublishMock).toHaveBeenCalled();
});
});
14 changes: 14 additions & 0 deletions src/course-outline/data/api.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

Expand Down Expand Up @@ -105,3 +106,16 @@ export async function restartIndexingOnCourse(reindexLink) {

return camelCaseObject(data);
}
/**
* Update publish course section
* @param {string} sectionId
* @returns {Promise<Object>}
*/
export async function publishCourseSection(sectionId) {
const { data } = await getAuthenticatedHttpClient()
.post(getUpdateCourseSectionApiUrl(sectionId), {
publish: 'make_public',
});

return data;
}
21 changes: 21 additions & 0 deletions src/course-outline/data/thunk.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-undef */
import { RequestStatus } from '../../data/constants';
import {
getCourseBestPracticesChecklist,
Expand All @@ -8,6 +9,7 @@ import {
getCourseBestPractices,
getCourseLaunch,
getCourseOutlineIndex,
publishCourseSection,
restartIndexingOnCourse,
} from './api';
import {
Expand Down Expand Up @@ -102,3 +104,22 @@ export function fetchCourseReindexQuery(courseId, reindexLink) {
}
};
}

export function publishCourseSectionQuery(sectionId) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));

try {
await publishCourseSection(sectionId).then(async (result) => {
if (result) {
await dispatch(fetchCourseSectionQuery(sectionId));
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
}
});
return true;
} catch (error) {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
return false;
}
};
}
9 changes: 9 additions & 0 deletions src/course-outline/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const useCourseOutline = ({ courseId }) => {
const [isDisabledReindexButton, setDisableReindexButton] = useState(false);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [showErrorAlert, setShowErrorAlert] = useState(false);
const [isPublishModalOpen, openPublishModal, closePublishModal] = useToggle(false);

const headerNavigationsActions = {
handleNewSection: () => {
Expand Down Expand Up @@ -62,6 +63,10 @@ const useCourseOutline = ({ courseId }) => {
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
};

const handleSubmitPublishSection = () => {
closePublishModal();
};

useEffect(() => {
dispatch(fetchCourseOutlineIndexQuery(courseId));
dispatch(fetchCourseBestPracticesQuery({ courseId }));
Expand All @@ -86,8 +91,12 @@ const useCourseOutline = ({ courseId }) => {
showErrorAlert,
isDisabledReindexButton,
isSectionsExpanded,
isPublishModalOpen,
openPublishModal,
closePublishModal,
headerNavigationsActions,
handleEnableHighlightsSubmit,
handleSubmitPublishSection,
statusBarData,
isEnableHighlightsModalOpen,
openEnableHighlightsModal,
Expand Down
77 changes: 77 additions & 0 deletions src/course-outline/publish-modal/PublishModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* eslint-disable import/named */
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
ModalDialog,
Button,
ActionRow,
} from '@edx/paragon';
import { useSelector } from 'react-redux';

import { getCurrentSection } from '../data/selectors';
import messages from './messages';

const PublishModal = ({
isOpen,
onClose,
onPublishSubmit,
}) => {
const intl = useIntl();
const { displayName, childInfo } = useSelector(getCurrentSection);
const subSections = childInfo?.children || [];

return (
<ModalDialog
className="publish-modal"
isOpen={isOpen}
onClose={onClose}
hasCloseButton
isFullscreenOnMobile
>
<ModalDialog.Header className="publish-modal__header">
<ModalDialog.Title>
{intl.formatMessage(messages.title, { title: displayName })}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body>
<p className="small">{intl.formatMessage(messages.description)}</p>
{subSections.length ? subSections.map((subSection) => {
const units = subSection.childInfo.children;

return units.length ? (
<React.Fragment key={subSection.id}>
<span className="small text-gray-400">{subSection.displayName}</span>
{units.map((unit) => (
<div
key={unit.id}
className="small border border-light-400 p-2 publish-modal__subsection"
>
{unit.displayName}
</div>
))}
</React.Fragment>
) : null;
}) : null}
</ModalDialog.Body>
<ModalDialog.Footer className="pt-1">
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages.cancelButton)}
</ModalDialog.CloseButton>
<Button onClick={onPublishSubmit}>
{intl.formatMessage(messages.publishButton)}
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
);
};

PublishModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onPublishSubmit: PropTypes.func.isRequired,
};

export default PublishModal;
15 changes: 15 additions & 0 deletions src/course-outline/publish-modal/PublishModal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.publish-modal {
max-width: 33.6875rem;

.pgn__modal-close-container {
transform: translateY(.5rem);
}

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

.publish-modal__subsection:not(:last-child) {
margin-bottom: .5rem;
}
}
Loading

0 comments on commit e1cf27d

Please sign in to comment.