diff --git a/src/course-unit/CourseUnit.jsx b/src/course-unit/CourseUnit.jsx index a931f19ace..3c8ce2074d 100644 --- a/src/course-unit/CourseUnit.jsx +++ b/src/course-unit/CourseUnit.jsx @@ -2,9 +2,15 @@ import { useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { Container, Layout, Stack } from '@openedx/paragon'; +import { + Container, Layout, Stack, Button, +} from '@openedx/paragon'; import { useIntl, injectIntl } from '@edx/frontend-platform/i18n'; -import { Warning as WarningIcon } from '@openedx/paragon/icons'; +import { + Warning as WarningIcon, + ArrowDropDown as ArrowDownIcon, + ArrowDropUp as ArrowUpIcon, +} from '@openedx/paragon/icons'; import { DraggableList } from '@edx/frontend-lib-content-components'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; @@ -57,6 +63,9 @@ const CourseUnit = ({ courseId }) => { courseVerticalChildren, handleXBlockDragAndDrop, canPasteComponent, + isXBlocksExpanded, + isXBlocksRendered, + handleExpandAll, } = useCourseUnit({ courseId, blockId }); const initialXBlocksData = useMemo(() => courseVerticalChildren.children ?? [], [courseVerticalChildren.children]); @@ -156,6 +165,15 @@ const CourseUnit = ({ courseId }) => { setState={setUnitXBlocks} updateOrder={finalizeXBlockOrder} > + { userPartitionInfo={userPartitionInfo} blockId={blockId} actions={actions} + isXBlocksExpanded={isXBlocksExpanded} + isXBlocksRendered={isXBlocksRendered} /> ))} diff --git a/src/course-unit/CourseUnit.test.jsx b/src/course-unit/CourseUnit.test.jsx index e24ce536b2..d46aadd891 100644 --- a/src/course-unit/CourseUnit.test.jsx +++ b/src/course-unit/CourseUnit.test.jsx @@ -898,103 +898,6 @@ describe('', () => { )).toBeInTheDocument(); }); - it('checks if xblock is a duplicate when the corresponding duplicate button is clicked and if the sidebar status is updated', async () => { - axiosMock - .onPost(postXBlockBaseApiUrl({ - parent_locator: blockId, - duplicate_source_locator: courseVerticalChildrenMock.children[0].block_id, - })) - .replyOnce(200, { locator: '1234567890' }); - - axiosMock - .onGet(getCourseVerticalChildrenApiUrl(blockId)) - .reply(200, { - ...courseVerticalChildrenMock, - children: [ - ...courseVerticalChildrenMock.children, - { - ...courseVerticalChildrenMock.children[0], - name: 'New Cloned XBlock', - }, - ], - }); - - const { - getByText, - getAllByLabelText, - getAllByTestId, - queryByRole, - getByRole, - } = render(); - - await waitFor(() => { - userEvent.click(getByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })); - }); - - axiosMock - .onPost(getXBlockBaseApiUrl(blockId), { - publish: PUBLISH_TYPES.makePublic, - }) - .reply(200, { dummy: 'value' }); - axiosMock - .onGet(getCourseUnitApiUrl(blockId)) - .reply(200, { - ...courseUnitIndexMock, - visibility_state: UNIT_VISIBILITY_STATES.live, - has_changes: false, - published_by: userName, - }); - - await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); - - await waitFor(() => { - // check if the sidebar status is Published and Live - expect(getByText(sidebarMessages.sidebarTitlePublishedAndLive.defaultMessage)).toBeInTheDocument(); - expect(getByText( - sidebarMessages.publishLastPublished.defaultMessage - .replace('{publishedOn}', courseUnitIndexMock.published_on) - .replace('{publishedBy}', userName), - )).toBeInTheDocument(); - expect(queryByRole('button', { name: sidebarMessages.actionButtonPublishTitle.defaultMessage })).not.toBeInTheDocument(); - - expect(getByText(unitDisplayName)).toBeInTheDocument(); - const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage); - userEvent.click(xblockActionBtn); - - const duplicateBtn = getByText(courseXBlockMessages.blockLabelButtonDuplicate.defaultMessage); - userEvent.click(duplicateBtn); - - expect(getAllByTestId('course-xblock')).toHaveLength(3); - expect(getByText('New Cloned XBlock')).toBeInTheDocument(); - }); - - axiosMock - .onGet(getCourseUnitApiUrl(blockId)) - .reply(200, courseUnitIndexMock); - - await executeThunk(editCourseUnitVisibilityAndData(blockId, PUBLISH_TYPES.makePublic, true), store.dispatch); - - // after duplicate the xblock, the sidebar status changes to Draft (unpublished changes) - expect(getByText(sidebarMessages.sidebarTitleDraftUnpublishedChanges.defaultMessage)).toBeInTheDocument(); - expect(getByText(sidebarMessages.visibilityStaffAndLearnersTitle.defaultMessage)).toBeInTheDocument(); - expect(getByText(sidebarMessages.releaseStatusTitle.defaultMessage)).toBeInTheDocument(); - expect(getByText(sidebarMessages.sidebarBodyNote.defaultMessage)).toBeInTheDocument(); - expect(getByText(sidebarMessages.visibilityWillBeVisibleToTitle.defaultMessage)).toBeInTheDocument(); - expect(getByText(sidebarMessages.visibilityCheckboxTitle.defaultMessage)).toBeInTheDocument(); - expect(getByText(sidebarMessages.actionButtonPublishTitle.defaultMessage)).toBeInTheDocument(); - expect(getByText(sidebarMessages.actionButtonDiscardChangesTitle.defaultMessage)).toBeInTheDocument(); - expect(getByText(courseUnitIndexMock.release_date)).toBeInTheDocument(); - expect(getByText( - sidebarMessages.publishInfoDraftSaved.defaultMessage - .replace('{editedOn}', courseUnitIndexMock.edited_on) - .replace('{editedBy}', courseUnitIndexMock.edited_by), - )).toBeInTheDocument(); - expect(getByText( - sidebarMessages.releaseInfoWithSection.defaultMessage - .replace('{sectionName}', courseUnitIndexMock.release_date_from), - )).toBeInTheDocument(); - }); - it('should hide action buttons when their corresponding properties are set to false', async () => { const { getByText, @@ -1571,4 +1474,35 @@ describe('', () => { expect(xBlock1).toBe(xBlock2); }); }); + + it('should expand xblocks when "Expand all" button is clicked', async () => { + const { getByRole, getAllByTestId } = render(); + + axiosMock + .onGet(getCourseVerticalChildrenApiUrl(blockId)) + .reply(200, courseVerticalChildrenMock); + + await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch); + + const expandAllXBlocksBtn = getByRole('button', { name: messages.expandAllButton.defaultMessage }); + const unitXBlocks = getAllByTestId('course-xblock'); + + unitXBlocks.forEach((unitXBlock) => { + const unitXBlockContentSections = unitXBlock.querySelectorAll('.pgn__card-section'); + expect(unitXBlockContentSections).toHaveLength(0); + }); + + userEvent.click(expandAllXBlocksBtn); + + await waitFor(() => { + const collapseAllXBlocksBtn = getByRole('button', { name: messages.collapseAllButton.defaultMessage }); + expect(collapseAllXBlocksBtn).toBeInTheDocument(); + + unitXBlocks.forEach((unitXBlock) => { + const unitXBlockContentSections = unitXBlock.querySelectorAll('.pgn__card-section'); + // xblock content appears inside the xblock element + expect(unitXBlockContentSections.length).toBeGreaterThan(0); + }); + }); + }); }); diff --git a/src/course-unit/course-xblock/CourseXBlock.jsx b/src/course-unit/course-xblock/CourseXBlock.jsx index 0ed0991054..6860399919 100644 --- a/src/course-unit/course-xblock/CourseXBlock.jsx +++ b/src/course-unit/course-xblock/CourseXBlock.jsx @@ -52,7 +52,8 @@ const XBLOCK_EDIT_MODAL_CLASS_NAME = 'xblock-edit-modal'; const CourseXBlock = memo(({ id, title, type, unitXBlockActions, shouldScroll, userPartitionInfo, - handleConfigureSubmit, validationMessages, renderError, actions, blockId, ...props + handleConfigureSubmit, validationMessages, renderError, actions, blockId, + isXBlocksExpanded, isXBlocksRendered, ...props }) => { const courseXBlockElementRef = useRef(null); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false); @@ -69,8 +70,13 @@ const CourseXBlock = memo(({ ); const [showLegacyEditModal, toggleLegacyEditModal] = useState(false); const xblockLegacyEditModalRef = useRef(null); - const [isExpanded, setIsExpanded] = useState(false); - const [isRendered, setIsRendered] = useState(false); + const [isExpanded, setIsExpanded] = useState(isXBlocksExpanded); + const [isRendered, setIsRendered] = useState(isXBlocksRendered); + + useEffect(() => { + setIsExpanded(isXBlocksExpanded); + setIsRendered(isXBlocksRendered); + }, [isXBlocksExpanded, isXBlocksRendered]); const { canCopy, canDelete, canDuplicate, canManageAccess, canManageTags, canMove, @@ -348,6 +354,8 @@ CourseXBlock.propTypes = { canManageTags: PropTypes.bool, canMove: PropTypes.bool, }).isRequired, + isXBlocksExpanded: PropTypes.bool.isRequired, + isXBlocksRendered: PropTypes.bool.isRequired, }; export default CourseXBlock; diff --git a/src/course-unit/hooks.jsx b/src/course-unit/hooks.jsx index 14bb23dc43..69d92bc4cd 100644 --- a/src/course-unit/hooks.jsx +++ b/src/course-unit/hooks.jsx @@ -1,4 +1,6 @@ -import { useCallback, useEffect, useMemo } from 'react'; +import { + useCallback, useEffect, useMemo, useState, +} from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; @@ -33,6 +35,8 @@ import { PUBLISH_TYPES } from './constants'; // eslint-disable-next-line import/prefer-default-export export const useCourseUnit = ({ courseId, blockId }) => { const dispatch = useDispatch(); + const [isXBlocksExpanded, setXBlocksExpanded] = useState(false); + const [isXBlocksRendered, setIsXBlocksRendered] = useState(false); const courseUnit = useSelector(getCourseUnitData); const savingStatus = useSelector(getSavingStatus); @@ -100,6 +104,11 @@ export const useCourseUnit = ({ courseId, blockId }) => { dispatch(setXBlockOrderListQuery(blockId, xblockListIds, restoreCallback)); }; + const handleExpandAll = () => { + setIsXBlocksRendered(true); + setXBlocksExpanded((prevState) => !prevState); + }; + useEffect(() => { if (savingStatus === RequestStatus.SUCCESSFUL) { dispatch(updateQueryPendingStatus(true)); @@ -137,5 +146,8 @@ export const useCourseUnit = ({ courseId, blockId }) => { courseVerticalChildren, handleXBlockDragAndDrop, canPasteComponent, + isXBlocksExpanded, + isXBlocksRendered, + handleExpandAll, }; }; diff --git a/src/course-unit/messages.js b/src/course-unit/messages.js index 7ed3e78c86..b5934d1e28 100644 --- a/src/course-unit/messages.js +++ b/src/course-unit/messages.js @@ -13,6 +13,14 @@ const messages = defineMessages({ id: 'course-authoring.course-unit.paste-component.btn.text', defaultMessage: 'Paste component', }, + collapseAllButton: { + id: 'course-authoring.course-unit.xblocks.button.collapse-all', + defaultMessage: 'Collapse all', + }, + expandAllButton: { + id: 'course-authoring.course-unit.xblocks.button.expand-all', + defaultMessage: 'Expand all', + }, }); export default messages;