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 committed Apr 30, 2024
1 parent 418bba8 commit 0a3b5a3
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 102 deletions.
24 changes: 22 additions & 2 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ 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 { DraggableList, ErrorAlert } from '@edx/frontend-lib-content-components';
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 { getProcessingNotification } from '../generic/processing-notification/data/selectors';
Expand Down Expand Up @@ -60,6 +66,9 @@ const CourseUnit = ({ courseId }) => {
courseVerticalChildren,
handleXBlockDragAndDrop,
canPasteComponent,
isXBlocksExpanded,
isXBlocksRendered,
handleExpandAll,
} = useCourseUnit({ courseId, blockId });

const initialXBlocksData = useMemo(() => courseVerticalChildren.children ?? [], [courseVerticalChildren.children]);
Expand Down Expand Up @@ -163,6 +172,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 @@ -186,6 +204,8 @@ const CourseUnit = ({ courseId }) => {
data-testid="course-xblock"
userPartitionInfo={userPartitionInfo}
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 @@ -967,103 +967,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 @@ -1647,4 +1550,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 @@ -41,7 +41,8 @@ import { extractStylesWithContent } from './utils';

const CourseXBlock = memo(({
id, title, type, unitXBlockActions, shouldScroll, userPartitionInfo,
handleConfigureSubmit, validationMessages, renderError, actions, ...props
handleConfigureSubmit, validationMessages, renderError, actions,
isXBlocksExpanded, isXBlocksRendered, ...props
}) => {
const courseXBlockElementRef = useRef(null);
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
Expand All @@ -55,8 +56,13 @@ const CourseXBlock = memo(({
() => find(xblockIFrameHtmlAndResources, { xblockId: id }),
[id, xblockIFrameHtmlAndResources],
);
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, canMove,
Expand Down Expand Up @@ -279,6 +285,8 @@ CourseXBlock.propTypes = {
canManageAccess: PropTypes.bool,
canMove: PropTypes.bool,
}).isRequired,
isXBlocksExpanded: PropTypes.bool.isRequired,
isXBlocksRendered: PropTypes.bool.isRequired,
};

export default CourseXBlock;
10 changes: 10 additions & 0 deletions src/course-unit/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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 [isErrorAlert, toggleErrorAlert] = useState(false);
const [hasInternetConnectionError, setInternetConnectionError] = useState(false);
Expand Down Expand Up @@ -117,6 +119,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 @@ -159,5 +166,8 @@ export const useCourseUnit = ({ courseId, blockId }) => {
courseVerticalChildren,
handleXBlockDragAndDrop,
canPasteComponent,
isXBlocksExpanded,
isXBlocksRendered,
handleExpandAll,
};
};
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 0a3b5a3

Please sign in to comment.