Skip to content

Commit

Permalink
feat: Unit page - tagging
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyrylo Hudym-Levkovych authored and Kyrylo Hudym-Levkovych committed Jul 22, 2024
1 parent 3a14141 commit 92939bb
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 37 deletions.
7 changes: 5 additions & 2 deletions src/course-unit/CourseUnit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const CourseUnit = ({ courseId }) => {

const initialXBlocksData = useMemo(() => courseVerticalChildren.children ?? [], [courseVerticalChildren.children]);
const [unitXBlocks, setUnitXBlocks] = useState(initialXBlocksData);

// temp comment to trigger github workflow
useEffect(() => {
document.title = getPageHeadTitle('', unitTitle);
}, [unitTitle]);
Expand Down 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

0 comments on commit 92939bb

Please sign in to comment.