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;