Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 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 { DraggableList } from '@edx/frontend-lib-content-components';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';

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

const initialXBlocksData = useMemo(() => courseVerticalChildren.children ?? [], [courseVerticalChildren.children]);
Expand Down Expand Up @@ -156,6 +165,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 @@ -180,6 +198,8 @@ const CourseUnit = ({ courseId }) => {
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 @@ -898,103 +898,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 () => {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was a copy of an already existing test

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 @@ -1571,4 +1474,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 @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -348,6 +354,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 } from 'react-router-dom';

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -137,5 +146,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;