Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [FC-0044] Unit page - manage tags xblocks #967

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
5 changes: 4 additions & 1 deletion src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,20 +159,23 @@ const CourseUnit = ({ courseId }) => {
strategy={verticalListSortingStrategy}
>
{unitXBlocks.map(({
name, id, blockType: type, shouldScroll, userPartitionInfo, validationMessages,
name, id, blockType: type, renderError, shouldScroll,
userPartitionInfo, validationMessages, actions,
}) => (
<CourseXBlock
id={id}
key={id}
title={name}
type={type}
renderError={renderError}
blockId={blockId}
validationMessages={validationMessages}
shouldScroll={shouldScroll}
handleConfigureSubmit={handleConfigureSubmit}
unitXBlockActions={unitXBlockActions}
data-testid="course-xblock"
userPartitionInfo={userPartitionInfo}
actions={actions}
/>
))}
</SortableContext>
Expand Down
149 changes: 149 additions & 0 deletions src/course-unit/CourseUnit.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ describe('<CourseUnit />', () => {
block_id: '1234567890',
block_type: 'drag-and-drop-v2',
user_partition_info: {},
actions: courseVerticalChildrenMock.children[0].actions,
},
],
});
Expand Down Expand Up @@ -971,6 +972,153 @@ 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,
getAllByLabelText,
queryByRole,
} = render(<RootWrapper />);

const convertedXBlockActions = camelCaseObject(courseVerticalChildrenMock.children[0].actions);

const updatedXBlockActions = Object.keys(convertedXBlockActions).reduce((acc, key) => {
acc[key] = false;
return acc;
}, {});

axiosMock
.onGet(getCourseVerticalChildrenApiUrl(blockId))
.reply(200, {
children: [
{
...courseVerticalChildrenMock.children[0],
actions: {
...courseVerticalChildrenMock.children[0].actions,
updatedXBlockActions,
},
},
],
});

await executeThunk(fetchCourseVerticalChildrenData(blockId), store.dispatch);

await waitFor(() => {
expect(getByText(unitDisplayName)).toBeInTheDocument();
const [xblockActionBtn] = getAllByLabelText(courseXBlockMessages.blockActionsDropdownAlt.defaultMessage);
userEvent.click(xblockActionBtn);
const deleteBtn = queryByRole('button', { name: courseXBlockMessages.blockLabelButtonDelete.defaultMessage });
const duplicateBtn = queryByRole('button', { name: courseXBlockMessages.blockLabelButtonDuplicate.defaultMessage });
const moveBtn = queryByRole('button', { name: courseXBlockMessages.blockLabelButtonMove.defaultMessage });
const copyToClipboardBtn = queryByRole('button', { name: courseXBlockMessages.blockLabelButtonCopyToClipboard.defaultMessage });
const manageAccessBtn = queryByRole('button', { name: courseXBlockMessages.blockLabelButtonManageAccess.defaultMessage });
const manageTagsBtn = queryByRole('button', { name: courseXBlockMessages.blockLabelButtonManageTags.defaultMessage });

expect(deleteBtn).not.toBeInTheDocument();
expect(duplicateBtn).not.toBeInTheDocument();
expect(moveBtn).not.toBeInTheDocument();
expect(copyToClipboardBtn).not.toBeInTheDocument();
expect(manageAccessBtn).not.toBeInTheDocument();
expect(manageTagsBtn).not.toBeInTheDocument();
});
});

it('should toggle visibility from header configure modal and update course unit state accordingly', async () => {
const { getByRole, getByTestId } = render(<RootWrapper />);
let courseUnitSidebar;
Expand Down Expand Up @@ -1175,6 +1323,7 @@ describe('<CourseUnit />', () => {
selected_partition_index: -1,
selected_groups_label: '',
},
actions: courseVerticalChildrenMock.children[0].actions,
},
],
});
Expand Down
2 changes: 2 additions & 0 deletions src/course-unit/__mocks__/courseVerticalChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
can_duplicate: true,
can_move: true,
can_manage_access: true,
can_manage_tags: true,
can_delete: true,
},
user_partition_info: {
Expand Down Expand Up @@ -80,6 +81,7 @@ module.exports = {
can_duplicate: true,
can_move: true,
can_manage_access: true,
can_manage_tags: true,
can_delete: true,
},
user_partition_info: {
Expand Down
79 changes: 64 additions & 15 deletions src/course-unit/course-xblock/CourseXBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import {
ActionRow, Card, Dropdown, Icon, IconButton, useToggle,
ActionRow, Card, Dropdown, Icon, IconButton, useToggle, Sheet,
} from '@openedx/paragon';
import { EditOutline as EditIcon, MoreVert as MoveVertIcon } from '@openedx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { useNavigate, useSearchParams } from 'react-router-dom';

import { getCanEdit, getCourseId } from 'CourseAuthoring/course-unit/data/selectors';
import ContentTagsDrawer from '../../content-tags-drawer/ContentTagsDrawer';
import { useContentTagsCount } from '../../generic/data/apiHooks';
import TagCount from '../../generic/tag-count';
import DeleteModal from '../../generic/delete-modal/DeleteModal';
import ConfigureModal from '../../generic/configure-modal/ConfigureModal';
import SortableItem from '../../generic/drag-helper/SortableItem';
Expand All @@ -22,11 +25,12 @@ import messages from './messages';

const CourseXBlock = ({
id, title, type, unitXBlockActions, shouldScroll, userPartitionInfo,
handleConfigureSubmit, validationMessages, ...props
handleConfigureSubmit, validationMessages, actions, ...props
}) => {
const courseXBlockElementRef = useRef(null);
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useToggle(false);
const [isConfigureModalOpen, openConfigureModal, closeConfigureModal] = useToggle(false);
const [isManageTagsOpen, openManageTagsModal, closeManageTagsModal] = useToggle(false);
const dispatch = useDispatch();
const navigate = useNavigate();
const canEdit = useSelector(getCanEdit);
Expand All @@ -37,6 +41,15 @@ const CourseXBlock = ({
const locatorId = searchParams.get('show');
const isScrolledToElement = locatorId === id;

const {
canCopy, canDelete, canDuplicate, canManageAccess, canManageTags, canMove,
} = actions;

const {
data: contentTaxonomyTagsCount,
isSuccess: isContentTaxonomyTagsCountLoaded,
} = useContentTagsCount(id || '');

const visibilityMessage = userPartitionInfo.selectedGroupsLabel
? intl.formatMessage(messages.visibilityMessage, { selectedGroupsLabel: userPartitionInfo.selectedGroupsLabel })
: null;
Expand Down Expand Up @@ -95,6 +108,12 @@ const CourseXBlock = ({
subtitle={visibilityMessage}
actions={(
<ActionRow className="mr-2">
{
canManageTags
&& isContentTaxonomyTagsCountLoaded
&& contentTaxonomyTagsCount > 0
&& <div className="ml-2"><TagCount count={contentTaxonomyTagsCount} onClick={openManageTagsModal} /></div>
}
<IconButton
alt={intl.formatMessage(messages.blockAltButtonEdit)}
iconAs={EditIcon}
Expand All @@ -109,23 +128,36 @@ const CourseXBlock = ({
iconAs={Icon}
/>
<Dropdown.Menu>
<Dropdown.Item onClick={() => unitXBlockActions.handleDuplicate(id)}>
{intl.formatMessage(messages.blockLabelButtonDuplicate)}
</Dropdown.Item>
<Dropdown.Item>
{intl.formatMessage(messages.blockLabelButtonMove)}
</Dropdown.Item>
{canEdit && (
{canManageTags && (
<Dropdown.Item onClick={openManageTagsModal}>
{intl.formatMessage(messages.blockLabelButtonManageTags)}
</Dropdown.Item>
)}
{canEdit && canCopy && (
<Dropdown.Item onClick={() => dispatch(copyToClipboard(id))}>
{intl.formatMessage(messages.blockLabelButtonCopyToClipboard)}
</Dropdown.Item>
)}
<Dropdown.Item onClick={openConfigureModal}>
{intl.formatMessage(messages.blockLabelButtonManageAccess)}
</Dropdown.Item>
<Dropdown.Item onClick={openDeleteModal}>
{intl.formatMessage(messages.blockLabelButtonDelete)}
</Dropdown.Item>
{canDuplicate && (
<Dropdown.Item onClick={() => unitXBlockActions.handleDuplicate(id)}>
{intl.formatMessage(messages.blockLabelButtonDuplicate)}
</Dropdown.Item>
)}
{canMove && (
<Dropdown.Item>
{intl.formatMessage(messages.blockLabelButtonMove)}
</Dropdown.Item>
)}
{canManageAccess && (
<Dropdown.Item onClick={openConfigureModal}>
{intl.formatMessage(messages.blockLabelButtonManageAccess)}
</Dropdown.Item>
)}
{canDelete && (
<Dropdown.Item onClick={openDeleteModal}>
{intl.formatMessage(messages.blockLabelButtonDelete)}
</Dropdown.Item>
)}
</Dropdown.Menu>
</Dropdown>
<DeleteModal
Expand All @@ -141,6 +173,15 @@ const CourseXBlock = ({
onConfigureSubmit={onConfigureSubmit}
currentItemData={currentItemData}
/>
<Sheet
position="right"
show={isManageTagsOpen}
blocking={false}
variant="light"
onClose={closeManageTagsModal}
>
<ContentTagsDrawer id={id} onClose={closeManageTagsModal} />
</Sheet>
</ActionRow>
)}
/>
Expand Down Expand Up @@ -187,6 +228,14 @@ CourseXBlock.propTypes = {
selectedGroupsLabel: PropTypes.string,
}).isRequired,
handleConfigureSubmit: PropTypes.func.isRequired,
actions: PropTypes.shape({
canCopy: PropTypes.bool,
canDelete: PropTypes.bool,
canDuplicate: PropTypes.bool,
canManageAccess: PropTypes.bool,
canManageTags: PropTypes.bool,
canMove: PropTypes.bool,
}).isRequired,
};

export default CourseXBlock;
Loading
Loading