From 61ff8f432311eb627d31d4c7d0d15f4fb9494a62 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Mon, 20 Nov 2023 15:52:20 +0530 Subject: [PATCH] feat: section list and new section button Also refactor api and hooks fix: publish button behaviour and card header tests fix: warning in highlights and publish modal test fix: courseoutline tests test: add test for new section functionality fix(lint): lint issues refactor: remove unnecessary css in CardHeader refactor: rename emptyPlaceholder test file refactor: replace ternary operator with 'and' condition refactor: add black color to expand/collapse button refactor: display only changed subsection and units in publish modal --- src/constants.js | 1 + src/course-outline/CourseOutline.jsx | 45 ++++-- src/course-outline/CourseOutline.test.jsx | 52 +++++-- src/course-outline/__mocks__/courseSection.js | 93 +++++++++++ src/course-outline/__mocks__/index.js | 1 + src/course-outline/card-header/CardHeader.jsx | 13 +- .../card-header/CardHeader.scss | 23 +-- .../card-header/CardHeader.test.jsx | 145 +++++++++++------- src/course-outline/constants.js | 5 + src/course-outline/data/api.js | 26 +++- src/course-outline/data/slice.js | 7 + src/course-outline/data/thunk.js | 28 ++++ .../empty-placeholder/EmptyPlaceholder.jsx | 2 +- ...ent.test.jsx => EmptyPlaceholder.test.jsx} | 0 .../EnableHighlightsModal.test.jsx | 6 + .../header-navigations/HeaderNavigations.jsx | 4 +- .../HeaderNavigations.test.jsx | 4 +- .../highlights-modal/HighlightsModal.test.jsx | 6 + src/course-outline/hooks.jsx | 12 +- src/course-outline/messages.js | 10 +- .../publish-modal/PublishModal.jsx | 6 +- .../publish-modal/PublishModal.test.jsx | 9 ++ .../section-card/SectionCard.jsx | 7 +- .../section-card/SectionCard.test.jsx | 1 + .../status-bar/StatusBar.test.jsx | 6 + .../processing-notification/data/slice.js | 4 +- 26 files changed, 377 insertions(+), 139 deletions(-) create mode 100644 src/course-outline/__mocks__/courseSection.js rename src/course-outline/empty-placeholder/{EmptyContent.test.jsx => EmptyPlaceholder.test.jsx} (100%) diff --git a/src/constants.js b/src/constants.js index 6305606e70..7c293fe122 100644 --- a/src/constants.js +++ b/src/constants.js @@ -23,6 +23,7 @@ export const NOTIFICATION_MESSAGES = { saving: 'Saving', duplicating: 'Duplicating', deleting: 'Deleting', + empty: '', }; export const DEFAULT_TIME_STAMP = '00:00'; diff --git a/src/course-outline/CourseOutline.jsx b/src/course-outline/CourseOutline.jsx index 6c9df68a87..c9ec8acf89 100644 --- a/src/course-outline/CourseOutline.jsx +++ b/src/course-outline/CourseOutline.jsx @@ -2,11 +2,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; import { + Button, Container, Layout, TransitionReplace, } from '@edx/paragon'; import { + Add as IconAdd, CheckCircle as CheckCircleIcon, Warning as WarningIcon, } from '@edx/paragon/icons'; @@ -66,6 +68,7 @@ const CourseOutline = ({ courseId }) => { handleEditSectionSubmit, handleDeleteSectionSubmit, handleDuplicateSectionSubmit, + handleNewSectionSubmit, } = useCourseOutline({ courseId }); document.title = getPageHeadTitle(courseName, intl.formatMessage(messages.headingTitle)); @@ -131,20 +134,34 @@ const CourseOutline = ({ courseId }) => { openEnableHighlightsModal={openEnableHighlightsModal} />
- {/* TODO add create new section handler in EmptyPlaceholder */} - {sectionsList.length ? sectionsList.map((section) => ( - - )) : ( - ({})} /> + {sectionsList.length ? ( + <> + {sectionsList.map((section) => ( + + ))} + + + ) : ( + )}
diff --git a/src/course-outline/CourseOutline.test.jsx b/src/course-outline/CourseOutline.test.jsx index 429978025b..83c215f927 100644 --- a/src/course-outline/CourseOutline.test.jsx +++ b/src/course-outline/CourseOutline.test.jsx @@ -1,5 +1,7 @@ import React from 'react'; -import { render, waitFor, fireEvent } from '@testing-library/react'; +import { + render, waitFor, cleanup, 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'; @@ -11,13 +13,13 @@ import { getCourseLaunchApiUrl, getCourseOutlineIndexApiUrl, getCourseReindexApiUrl, - getCourseReindexApiUrl, - getCourseSectionApiUrl, - getCourseSectionDuplicateApiUrl, + getXBlockApiUrl, getEnableHighlightsEmailsApiUrl, getUpdateCourseSectionApiUrl, + getXBlockBaseApiUrl, } from './data/api'; import { + addNewCourseSectionQuery, deleteCourseSectionQuery, duplicateCourseSectionQuery, editCourseSectionQuery, @@ -36,10 +38,12 @@ import { courseOutlineIndexWithoutSections, courseBestPracticesMock, courseLaunchMock, + courseSectionMock, } from './__mocks__'; import { executeThunk } from '../utils'; import CourseOutline from './CourseOutline'; import messages from './messages'; +import headerMessages from './header-navigations/messages'; let axiosMock; let store; @@ -53,6 +57,15 @@ jest.mock('react-router-dom', () => ({ }), })); +jest.mock('../help-urls/hooks', () => ({ + useHelpUrls: () => ({ + contentHighlights: 'some', + visibility: 'some', + grading: 'some', + outline: 'some', + }), +})); + const RootWrapper = () => ( @@ -100,6 +113,25 @@ describe('', () => { expect(getByText(messages.alertSuccessDescription.defaultMessage)).toBeInTheDocument(); }); + it('adds new section correctly', async () => { + const { findAllByTestId } = render(); + let element = await findAllByTestId('section-card'); + expect(element.length).toBe(4); + + axiosMock + .onPost(getXBlockBaseApiUrl()) + .reply(200, { + locator: courseSectionMock.id, + }); + axiosMock + .onGet(getXBlockApiUrl(courseSectionMock.id)) + .reply(200, courseSectionMock); + await executeThunk(addNewCourseSectionQuery(courseId), store.dispatch); + + element = await findAllByTestId('section-card'); + expect(element.length).toBe(5); + }); + it('render error alert after failed reindex correctly', async () => { const { getByText } = render(); @@ -163,11 +195,11 @@ describe('', () => { const { queryAllByTestId, getByText } = render(); await waitFor(() => { - const collapseBtn = getByText(messages.collapseAllButton.defaultMessage); + const collapseBtn = getByText(headerMessages.collapseAllButton.defaultMessage); expect(collapseBtn).toBeInTheDocument(); fireEvent.click(collapseBtn); - const expendBtn = getByText(messages.expandAllButton.defaultMessage); + const expendBtn = getByText(headerMessages.expandAllButton.defaultMessage); expect(expendBtn).toBeInTheDocument(); fireEvent.click(expendBtn); @@ -210,7 +242,7 @@ describe('', () => { await executeThunk(editCourseSectionQuery(section.id, newDisplayName), store.dispatch); axiosMock - .onGet(getCourseSectionApiUrl(section.id)) + .onGet(getXBlockApiUrl(section.id)) .reply(200); await executeThunk(fetchCourseSectionQuery(section.id), store.dispatch); @@ -237,7 +269,7 @@ describe('', () => { const courseBlockId = courseOutlineIndexMock.courseStructure.id; axiosMock - .onPost(getCourseSectionDuplicateApiUrl()) + .onPost(getXBlockBaseApiUrl()) .reply(200, { duplicate_source_locator: section.id, parent_locator: courseBlockId, @@ -279,7 +311,7 @@ describe('', () => { await executeThunk(publishCourseSectionQuery(section.id), store.dispatch); axiosMock - .onGet(getCourseSectionApiUrl(section.id)) + .onGet(getXBlockApiUrl(section.id)) .reply(200, { ...section, published: true, @@ -316,7 +348,7 @@ describe('', () => { await executeThunk(updateCourseSectionHighlightsQuery(section.id, highlights), store.dispatch); axiosMock - .onGet(getCourseSectionApiUrl(section.id)) + .onGet(getXBlockApiUrl(section.id)) .reply(200, { ...section, highlights, diff --git a/src/course-outline/__mocks__/courseSection.js b/src/course-outline/__mocks__/courseSection.js new file mode 100644 index 0000000000..2e4e6f8de9 --- /dev/null +++ b/src/course-outline/__mocks__/courseSection.js @@ -0,0 +1,93 @@ +module.exports = { + id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d0e78d363a424da6be5c22704c34f7a7', + display_name: 'Section', + category: 'chapter', + has_children: true, + edited_on: 'Nov 22, 2023 at 07:45 UTC', + published: true, + published_on: 'Nov 22, 2023 at 07:45 UTC', + studio_url: '/course/course-v1:edX+DemoX+Demo_Course?show=block-v1%3AedX%2BDemoX%2BDemo_Course%2Btype%40chapter%2Bblock%40d0e78d363a424da6be5c22704c34f7a7', + released_to_students: true, + release_date: 'Feb 05, 2013 at 05:00 UTC', + visibility_state: 'live', + has_explicit_staff_lock: false, + start: '2013-02-05T05:00:00Z', + graded: false, + due_date: '', + due: null, + relative_weeks_due: null, + format: null, + course_graders: [ + 'Homework', + 'Exam', + ], + has_changes: false, + actions: { + deletable: true, + draggable: true, + childAddable: true, + duplicable: true, + }, + explanatory_message: null, + group_access: {}, + user_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + show_correctness: 'always', + highlights: [], + highlights_enabled: true, + highlights_preview_only: false, + highlights_doc_url: 'http://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/developing_course/course_sections.html#set-section-highlights-for-weekly-course-highlight-messages', + child_info: { + category: 'sequential', + display_name: 'Subsection', + children: [], + }, + ancestor_has_staff_lock: false, + staff_only_message: false, + enable_copy_paste_units: false, + has_partition_group_components: false, + user_partition_info: { + selectable_partitions: [ + { + id: 50, + name: 'Enrollment Track Groups', + scheme: 'enrollment_track', + groups: [ + { + id: 2, + name: 'Verified Certificate', + selected: false, + deleted: false, + }, + { + id: 1, + name: 'Audit', + selected: false, + deleted: false, + }, + ], + }, + ], + selected_partition_index: -1, + selected_groups_label: '', + }, +}; diff --git a/src/course-outline/__mocks__/index.js b/src/course-outline/__mocks__/index.js index e699605ef6..9421452577 100644 --- a/src/course-outline/__mocks__/index.js +++ b/src/course-outline/__mocks__/index.js @@ -2,3 +2,4 @@ export { default as courseOutlineIndexMock } from './courseOutlineIndex'; export { default as courseOutlineIndexWithoutSections } from './courseOutlineIndexWithoutSections'; export { default as courseBestPracticesMock } from './courseBestPractices'; export { default as courseLaunchMock } from './courseLaunch'; +export { default as courseSectionMock } from './courseSection'; diff --git a/src/course-outline/card-header/CardHeader.jsx b/src/course-outline/card-header/CardHeader.jsx index b8a61a44e1..a6d87171a9 100644 --- a/src/course-outline/card-header/CardHeader.jsx +++ b/src/course-outline/card-header/CardHeader.jsx @@ -13,6 +13,7 @@ import { } from '@edx/paragon'; import { ArrowDropDown as ArrowDownIcon, + ArrowDropUp as ArrowUpIcon, MoreVert as MoveVertIcon, EditOutline as EditIcon, } from '@edx/paragon/icons'; @@ -26,6 +27,7 @@ import messages from './messages'; const CardHeader = ({ title, sectionStatus, + hasChanges, isExpanded, onClickPublish, onClickMenuButton, @@ -42,8 +44,8 @@ const CardHeader = ({ const [titleValue, setTitleValue] = useState(title); const { badgeTitle, badgeIcon } = getSectionStatusBadgeContent(sectionStatus, messages, intl); - const isDisabledPublish = sectionStatus === SECTION_BADGE_STATUTES.live - || sectionStatus === SECTION_BADGE_STATUTES.publishedNotLive; + const isDisabledPublish = (sectionStatus === SECTION_BADGE_STATUTES.live + || sectionStatus === SECTION_BADGE_STATUTES.publishedNotLive) && !hasChanges; useEscapeClick({ onEscape: () => { @@ -86,12 +88,10 @@ const CardHeader = ({ )} >