Skip to content

Commit

Permalink
feat: course outline - sections list
Browse files Browse the repository at this point in the history
* feat: [2u-259] add components

* feat: [2u-259] fix sidebar

* feat: [2u-259] add tests, fix links

* feat: [2u-259] fix messages

* feat: [2u-159] fix reducer and sidebar

* feat: [2u-259] fix reducer

* feat: [2u-259] remove warning from selectors

* feat: [2u-259] remove indents

---------

Co-authored-by: Vladislav Keblysh <vladislavkeblysh@Vladislavs-MacBook-Pro.local>

feat: Course Outline  - Sections list (#59)

* feat: [2u-336] add tests

* feat: [2u-271] fix button

* feat: [2u-336] add component, refactor header

* fix: [2u-342] fix translates and indents

* fix: [2u-342] fix constants and expand block

* feat: [2u-336] remove new section from menu

---------

Co-authored-by: Vladislav Keblysh <vladislavkeblysh@Vladislavs-MacBook-Pro.local>

feat: Course outline - Content empty (#72)

* feat: [2u-324] add component

* feat: [2u-324] add translates

* feat: [2u-324] update tests

* feat: [2u-324] update branch

* fix: [2u-324] fixed empty handler

feat: Course outline - Section Publish (#61)

* 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

feat: Course outline - Update section card (#71)

* feat: [2u-615] update section card

* fix: [2u-615] fix handler names

* fix: [2u-615] fix indents

* fix: [2u-615] add empty handler

* fix: [2u-615] fix data test id name

* fix: [2u-615] fix styles

fix: [2u-696] add saving processing for higlights and enable highlights (#78)

feat: Course outline - Section Edit (#70)

* feat: [2u-336] add tests

* feat: [2u-271] fix button

* feat: [2u-336] add component, refactor header

* feat: [2u-342] add modal

* fix: [2u-342] fix translates and indents

* feat: [2u-342] add modal

* feat: [2u-342] add api

* feat: [2u-342] add tests and translates

* feat: [2u-342] fix indents

* fix: [2u-342] fix indents, variant and utils

* feat: [2u-342] fixed slice, thunks, hooks

* feat: [2u-354] add publish modal, api and update tests

* feat: [2u-615] update section card

* feat: [2u-348] add api, handlers, tests

* feat: [2u-348] add description for api

* fix: [2u-348] fix useEscapeClick

* fix: [2u-348] remove useEffect from CardHeader

* fix: [2u-348] fixed handlers and tests

* fix: [2u-348] fixed handlers and tests

---------

Co-authored-by: Vladislav Keblysh <vladislavkeblysh@Vladislavs-MacBook-Pro.local>

feat: Course outline - Section Delete (#74)

* feat: [2u-336] add tests

* feat: [2u-271] fix button

* feat: [2u-336] add component, refactor header

* feat: [2u-342] add modal

* fix: [2u-342] fix translates and indents

* feat: [2u-342] add modal

* feat: [2u-342] add api

* feat: [2u-342] add tests and translates

* feat: [2u-342] fix indents

* fix: [2u-342] fix indents, variant and utils

* feat: [2u-342] fixed slice, thunks, hooks

* feat: [2u-354] add publish modal, api and update tests

* feat: [2u-615] update section card

* feat: [2u-348] add api, handlers, tests

* feat: [2u-510] add delete api, add delete modal

* fix: [2u-510] fixed tests

---------

Co-authored-by: Vladislav Keblysh <vladislavkeblysh@Vladislavs-MacBook-Pro.local>

feat: Course outline - Section duplicate (openedx#88)

* feat: [2u-336] add tests

* feat: [2u-271] fix button

* feat: [2u-336] add component, refactor header

* feat: [2u-342] add modal

* fix: [2u-342] fix translates and indents

* feat: [2u-342] add modal

* feat: [2u-342] add api

* feat: [2u-342] add tests and translates

* feat: [2u-342] fix indents

* fix: [2u-342] fix indents, variant and utils

* feat: [2u-342] fixed slice, thunks, hooks

* feat: [2u-354] add publish modal, api and update tests

* feat: [2u-615] update section card

* feat: [2u-348] add api, handlers, tests

* feat: [2u-510] add delete api, add delete modal

* feat: [2u-360] add api

* feat: [2u-360] add slice

* feat: [2u-360] add tests

* fix: [2u-360] fixed tests

---------

Co-authored-by: Vladislav Keblysh <vladislavkeblysh@Vladislavs-MacBook-Pro.local>

fix: Course outline - Highlights links (openedx#89)

* fix: fixed doc urls

* fix: fixed components

feat: Course outline - Collapse all sections (#75)

* feat: added collapse all section logic

* fix: fixed tests

fix: final revision commits

fix: increase code coverage on the page
  • Loading branch information
vladislavkeblysh authored and navinkarkera committed Dec 19, 2023
1 parent bf46008 commit 99588e8
Show file tree
Hide file tree
Showing 43 changed files with 2,390 additions and 43 deletions.
76 changes: 72 additions & 4 deletions src/course-outline/CourseOutline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,35 @@ import {
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
} from '@edx/paragon/icons';
import { useSelector } from 'react-redux';

import SubHeader from '../generic/sub-header/SubHeader';
import { getProcessingNotification } from '../generic/processing-notification/data/selectors';
import { RequestStatus } from '../data/constants';
import SubHeader from '../generic/sub-header/SubHeader';
import ProcessingNotification from '../generic/processing-notification';
import InternetConnectionAlert from '../generic/internet-connection-alert';
import AlertMessage from '../generic/alert-message';
import getPageHeadTitle from '../generic/utils';
import HeaderNavigations from './header-navigations/HeaderNavigations';
import OutlineSideBar from './outline-sidebar/OutlineSidebar';
import messages from './messages';
import { useCourseOutline } from './hooks';
import StatusBar from './status-bar/StatusBar';
import EnableHighlightsModal from './enable-highlights-modal/EnableHighlightsModal';
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 DeleteModal from './delete-modal/DeleteModal';
import { useCourseOutline } from './hooks';
import messages from './messages';

const CourseOutline = ({ courseId }) => {
const intl = useIntl();

const {
courseName,
savingStatus,
statusBarData,
sectionsList,
isLoading,
isReIndexShow,
showErrorAlert,
Expand All @@ -36,13 +47,34 @@ const CourseOutline = ({ courseId }) => {
isEnableHighlightsModalOpen,
isInternetConnectionAlertFailed,
isDisabledReindexButton,
isHighlightsModalOpen,
isPublishModalOpen,
isDeleteModalOpen,
closeHighlightsModal,
closePublishModal,
closeDeleteModal,
openPublishModal,
openDeleteModal,
headerNavigationsActions,
openEnableHighlightsModal,
closeEnableHighlightsModal,
handleEnableHighlightsSubmit,
handleInternetConnectionFailed,
handleOpenHighlightsModal,
handleHighlightsFormSubmit,
handlePublishSectionSubmit,
handleEditSectionSubmit,
handleDeleteSectionSubmit,
handleDuplicateSectionSubmit,
} = useCourseOutline({ courseId });

document.title = getPageHeadTitle(courseName, intl.formatMessage(messages.headingTitle));

const {
isShow: isShowProcessingNotification,
title: processingNotificationTitle,
} = useSelector(getProcessingNotification);

if (isLoading) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
Expand Down Expand Up @@ -77,6 +109,7 @@ const CourseOutline = ({ courseId }) => {
isSectionsExpanded={isSectionsExpanded}
headerNavigationsActions={headerNavigationsActions}
isDisabledReindexButton={isDisabledReindexButton}
hasSections={Boolean(sectionsList.length)}
/>
)}
/>
Expand All @@ -97,6 +130,23 @@ const CourseOutline = ({ courseId }) => {
statusBarData={statusBarData}
openEnableHighlightsModal={openEnableHighlightsModal}
/>
<div className="pt-4">
{/* TODO add create new section handler in EmptyPlaceholder */}
{sectionsList.length ? sectionsList.map((section) => (
<SectionCard
section={section}
savingStatus={savingStatus}
onOpenHighlightsModal={handleOpenHighlightsModal}
onOpenPublishModal={openPublishModal}
onOpenDeleteModal={openDeleteModal}
onEditSectionSubmit={handleEditSectionSubmit}
onDuplicateSubmit={handleDuplicateSectionSubmit}
isSectionsExpanded={isSectionsExpanded}
/>
)) : (
<EmptyPlaceholder onCreateNewSection={() => ({})} />
)}
</div>
</section>
</div>
</article>
Expand All @@ -109,11 +159,29 @@ const CourseOutline = ({ courseId }) => {
isOpen={isEnableHighlightsModalOpen}
close={closeEnableHighlightsModal}
onEnableHighlightsSubmit={handleEnableHighlightsSubmit}
highlightsDocUrl={statusBarData.highlightsDocUrl}
/>
</section>
<HighlightsModal
isOpen={isHighlightsModalOpen}
onClose={closeHighlightsModal}
onSubmit={handleHighlightsFormSubmit}
/>
<PublishModal
isOpen={isPublishModalOpen}
onClose={closePublishModal}
onPublishSubmit={handlePublishSectionSubmit}
/>
<DeleteModal
isOpen={isDeleteModalOpen}
close={closeDeleteModal}
onDeleteSubmit={handleDeleteSectionSubmit}
/>
</Container>
<div className="alert-toast">
<ProcessingNotification
isShow={isShowProcessingNotification}
title={processingNotificationTitle}
/>
<InternetConnectionAlert
isFailed={isInternetConnectionAlertFailed}
isQueryPending={savingStatus === RequestStatus.PENDING}
Expand Down
5 changes: 5 additions & 0 deletions src/course-outline/CourseOutline.scss
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
@import "./header-navigations/HeaderNavigations";
@import "./status-bar/StatusBar";
@import "./section-card/SectionCard";
@import "./card-header/CardHeader";
@import "./empty-placeholder/EmptyPlaceholder";
@import "./highlights-modal/HighlightsModal";
@import "./publish-modal/PublishModal";
181 changes: 180 additions & 1 deletion src/course-outline/CourseOutline.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { render, waitFor, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
Expand All @@ -11,18 +11,29 @@ import {
getCourseLaunchApiUrl,
getCourseOutlineIndexApiUrl,
getCourseReindexApiUrl,
getCourseReindexApiUrl,
getCourseSectionApiUrl,
getCourseSectionDuplicateApiUrl,
getEnableHighlightsEmailsApiUrl,
getUpdateCourseSectionApiUrl,
} from './data/api';
import {
deleteCourseSectionQuery,
duplicateCourseSectionQuery,
editCourseSectionQuery,
enableCourseHighlightsEmailsQuery,
fetchCourseBestPracticesQuery,
fetchCourseLaunchQuery,
fetchCourseOutlineIndexQuery,
fetchCourseReindexQuery,
fetchCourseSectionQuery,
publishCourseSectionQuery,
updateCourseSectionHighlightsQuery,
} from './data/thunk';
import initializeStore from '../store';
import {
courseOutlineIndexMock,
courseOutlineIndexWithoutSections,
courseBestPracticesMock,
courseLaunchMock,
} from './__mocks__';
Expand Down Expand Up @@ -147,4 +158,172 @@ describe('<CourseOutline />', () => {
await executeThunk(enableCourseHighlightsEmailsQuery(courseId), store.dispatch);
expect(await findByTestId('highlights-enabled-span')).toBeInTheDocument();
});

it('should expand and collapse subsections, after click on subheader buttons', async () => {
const { queryAllByTestId, getByText } = render(<RootWrapper />);

await waitFor(() => {
const collapseBtn = getByText(messages.collapseAllButton.defaultMessage);
expect(collapseBtn).toBeInTheDocument();
fireEvent.click(collapseBtn);

const expendBtn = getByText(messages.expandAllButton.defaultMessage);
expect(expendBtn).toBeInTheDocument();

fireEvent.click(expendBtn);

const cardSubsections = queryAllByTestId('section-card__subsections');
cardSubsections.forEach(element => expect(element).toBeVisible());

fireEvent.click(collapseBtn);
cardSubsections.forEach(element => expect(element).not.toBeVisible());
});
});

it('render CourseOutline component without sections correctly', async () => {
cleanup();
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, courseOutlineIndexWithoutSections);

const { getByTestId } = render(<RootWrapper />);

await waitFor(() => {
expect(getByTestId('empty-placeholder')).toBeInTheDocument();
});
});

it('check edit section when edit query is successfully', async () => {
const { getByText } = render(<RootWrapper />);
const newDisplayName = 'New section name';

const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];

axiosMock
.onPost(getUpdateCourseSectionApiUrl(section.id, {
metadata: {
display_name: newDisplayName,
},
}))
.reply(200);

await executeThunk(editCourseSectionQuery(section.id, newDisplayName), store.dispatch);

axiosMock
.onGet(getCourseSectionApiUrl(section.id))
.reply(200);
await executeThunk(fetchCourseSectionQuery(section.id), store.dispatch);

await waitFor(() => {
expect(getByText(section.displayName)).toBeInTheDocument();
});
});

it('check delete section when edit query is successfully', async () => {
const { queryByText } = render(<RootWrapper />);
const section = courseOutlineIndexMock.courseStructure.childInfo.children[1];

axiosMock.onDelete(getUpdateCourseSectionApiUrl(section.id)).reply(200);
await executeThunk(deleteCourseSectionQuery(section.id), store.dispatch);

await waitFor(() => {
expect(queryByText(section.displayName)).not.toBeInTheDocument();
});
});

it('check duplicate section when duplicate query is successfully', async () => {
const { getAllByTestId } = render(<RootWrapper />);
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
const courseBlockId = courseOutlineIndexMock.courseStructure.id;

axiosMock
.onPost(getCourseSectionDuplicateApiUrl())
.reply(200, {
duplicate_source_locator: section.id,
parent_locator: courseBlockId,
});
await executeThunk(duplicateCourseSectionQuery(section.id, courseBlockId), store.dispatch);

await waitFor(() => {
expect(getAllByTestId('section-card')).toHaveLength(4);
});
});

it('check publish section when publish query is successfully', async () => {
cleanup();
const { getAllByTestId } = render(<RootWrapper />);
const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];

axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, {
courseOutlineIndexMock,
courseStructure: {
childInfo: {
children: [
{
...section,
published: false,
},
],
},
},
});

axiosMock
.onPost(getUpdateCourseSectionApiUrl(section.id), {
publish: 'make_public',
})
.reply(200);

await executeThunk(publishCourseSectionQuery(section.id), store.dispatch);

axiosMock
.onGet(getCourseSectionApiUrl(section.id))
.reply(200, {
...section,
published: true,
releasedToStudents: false,
});

await executeThunk(fetchCourseSectionQuery(section.id), store.dispatch);

const firstSection = getAllByTestId('section-card')[0];
expect(firstSection.querySelector('.section-card-header__badge-status')).toHaveTextContent('Published not live');
});

it('check update highlights when update highlights query is successfully', async () => {
const { getByRole } = render(<RootWrapper />);

const section = courseOutlineIndexMock.courseStructure.childInfo.children[0];
const highlights = [
'New Highlight 1',
'New Highlight 2',
'New Highlight 3',
'New Highlight 4',
'New Highlight 5',
];

axiosMock
.onPost(getUpdateCourseSectionApiUrl(section.id), {
publish: 'republish',
metadata: {
highlights,
},
})
.reply(200);

await executeThunk(updateCourseSectionHighlightsQuery(section.id, highlights), store.dispatch);

axiosMock
.onGet(getCourseSectionApiUrl(section.id))
.reply(200, {
...section,
highlights,
});

await executeThunk(fetchCourseSectionQuery(section.id), store.dispatch);

expect(getByRole('button', { name: '5 Section highlights' })).toBeInTheDocument();
});
});
Loading

0 comments on commit 99588e8

Please sign in to comment.