Skip to content

Commit

Permalink
feat: [AXIMST-800] Course unit - Added Collapse and Expand all button…
Browse files Browse the repository at this point in the history
…s for xblocks (#234)

* feat: [AXIMST-800] added Collapse and Expand all buttons for xblocks

* feat: added tests
  • Loading branch information
PKulkoRaccoonGang authored and monteri committed May 3, 2024
1 parent 6263c24 commit defd01f
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 104 deletions.
25 changes: 22 additions & 3 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
import { DraggableList } from '@edx/frontend-lib-content-components';

Expand Down Expand Up @@ -58,6 +64,9 @@ const CourseUnit = ({ courseId }) => {
courseVerticalChildren,
handleXBlockDragAndDrop,
canPasteComponent,
isXBlocksExpanded,
isXBlocksRendered,
handleExpandAll,
} = useCourseUnit({ courseId, blockId });

const initialXBlocksData = useMemo(() => courseVerticalChildren.children ?? [], [courseVerticalChildren.children]);
Expand Down Expand Up @@ -157,6 +166,15 @@ const CourseUnit = ({ courseId }) => {
setState={setUnitXBlocks}
updateOrder={finalizeXBlockOrder}
>
<Button
variant="outline-primary"
iconBefore={isXBlocksExpanded ? ArrowUpIcon : ArrowDownIcon}
onClick={handleExpandAll}
>
{isXBlocksExpanded
? intl.formatMessage(messages.collapseAllButton)
: intl.formatMessage(messages.expandAllButton)}
</Button>
<SortableContext
id="root"
items={unitXBlocks}
Expand All @@ -179,8 +197,9 @@ const CourseUnit = ({ courseId }) => {
unitXBlockActions={unitXBlockActions}
data-testid="course-xblock"
userPartitionInfo={userPartitionInfo}
blockId={blockId}
actions={actions}
isXBlocksExpanded={isXBlocksExpanded}
isXBlocksRendered={isXBlocksRendered}
/>
))}
</SortableContext>
Expand Down
128 changes: 31 additions & 97 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -970,103 +970,6 @@ describe('<CourseUnit />', () => {
)).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(<RootWrapper />);

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,
Expand Down Expand Up @@ -1651,4 +1554,35 @@ describe('<CourseUnit />', () => {
expect(xBlock1).toBe(xBlock2);
});
});

it('should expand xblocks when "Expand all" button is clicked', async () => {
const { getByRole, getAllByTestId } = render(<RootWrapper />);

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);
});
});
});
});
14 changes: 11 additions & 3 deletions src/course-unit/course-xblock/CourseXBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,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);
Expand All @@ -68,8 +69,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,
Expand Down Expand Up @@ -357,6 +363,8 @@ CourseXBlock.propTypes = {
canManageTags: PropTypes.bool,
canMove: PropTypes.bool,
}).isRequired,
isXBlocksExpanded: PropTypes.bool.isRequired,
isXBlocksRendered: PropTypes.bool.isRequired,
};

export default CourseXBlock;
14 changes: 13 additions & 1 deletion src/course-unit/hooks.jsx
Original file line number Diff line number Diff line change
@@ -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, useSearchParams } from 'react-router-dom';

Expand Down Expand Up @@ -35,6 +37,8 @@ import { useCopyToClipboard } from '../generic/clipboard';
export const useCourseUnit = ({ courseId, blockId }) => {
const dispatch = useDispatch();
const [searchParams] = useSearchParams();
const [isXBlocksExpanded, setXBlocksExpanded] = useState(false);
const [isXBlocksRendered, setIsXBlocksRendered] = useState(false);

const courseUnit = useSelector(getCourseUnitData);
const savingStatus = useSelector(getSavingStatus);
Expand Down Expand Up @@ -112,6 +116,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));
Expand Down Expand Up @@ -149,6 +158,9 @@ export const useCourseUnit = ({ courseId, blockId }) => {
handleConfigureSubmit,
courseVerticalChildren,
handleXBlockDragAndDrop,
isXBlocksExpanded,
isXBlocksRendered,
handleExpandAll,
canPasteComponent,
};
};
8 changes: 8 additions & 0 deletions src/course-unit/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit defd01f

Please sign in to comment.