feat: New align sidebar for Course Outline [FC-0114] (#2812)
- Adds the align sidebar for Course and Section/Subsection/Unit cards (https://github.com/openedx/frontend-app-authoring/issues/2625) - Add a new library lock icon to tags imported from upstream (https://github.com/openedx/frontend-app-authoring/issues/2234)
This commit is contained in:
@@ -7,6 +7,7 @@ import type {} from 'react-select/base';
|
||||
export interface TagTreeEntry {
|
||||
explicit: boolean;
|
||||
children: Record<string, TagTreeEntry>;
|
||||
isCopied: boolean;
|
||||
canChangeObjecttag: boolean;
|
||||
canDeleteObjecttag: boolean;
|
||||
}
|
||||
@@ -37,4 +38,14 @@ declare module 'react-select/base' {
|
||||
}
|
||||
}
|
||||
|
||||
export type TagTree = {
|
||||
[key: string]: {
|
||||
children: TagTree,
|
||||
canChangeObjecttag: boolean,
|
||||
canDeleteObjecttag: boolean,
|
||||
explicit: boolean,
|
||||
isCopied: boolean,
|
||||
}
|
||||
};
|
||||
|
||||
export default ContentTagsCollapsible;
|
||||
|
||||
@@ -191,6 +191,7 @@ const useContentTagsCollapsibleHelper = (
|
||||
children: {},
|
||||
canChangeObjecttag: item.canChangeObjecttag,
|
||||
canDeleteObjecttag: item.canDeleteObjecttag,
|
||||
isCopied: item.isCopied,
|
||||
};
|
||||
|
||||
// Populating the SelectableBox with "selected" (explicit) tags
|
||||
|
||||
@@ -57,4 +57,17 @@ describe('<TagsTree>', () => {
|
||||
fireEvent.click(xButton);
|
||||
expect(mockRemoveTagHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render library lock icon', async () => {
|
||||
render(
|
||||
<RootWrapper
|
||||
tags={contentTaxonomyTagsTreeMock}
|
||||
removeTagHandler={mockRemoveTagHandler}
|
||||
isEditMode
|
||||
/>,
|
||||
);
|
||||
|
||||
const view = screen.getByText(/hierarchical taxonomy tag 3\.4\.50/i);
|
||||
expect(within(view).getByTestId('lock-icon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,54 @@
|
||||
// @ts-check
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, Stack, IconButton } from '@openedx/paragon';
|
||||
import { Tag, Close } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Icon, Stack, IconButton, OverlayTrigger, Tooltip,
|
||||
} from '@openedx/paragon';
|
||||
import { Tag, Close, Lock } from '@openedx/paragon/icons';
|
||||
import messages from './messages';
|
||||
import { ContentTagsDrawerContext } from './common/context';
|
||||
import { TagTree } from './ContentTagsCollapsible';
|
||||
|
||||
const LibraryLockIcon = ({ key }: { key: string }) => (
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
overlay={(
|
||||
<Tooltip
|
||||
id={`tooltip-lock-${key}`}
|
||||
>
|
||||
<FormattedMessage {...messages.libraryLockIconTooltip} />
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
src={Lock}
|
||||
size="xs"
|
||||
className="ml-1"
|
||||
data-testid="lock-icon"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
interface TagComponentProps {
|
||||
value: string;
|
||||
lineage: string[];
|
||||
canDelete: boolean;
|
||||
explicit: boolean;
|
||||
removeTagHandler?: (value: string) => void;
|
||||
afterComponent?: React.ReactNode;
|
||||
}
|
||||
|
||||
const TagComponent = ({
|
||||
value,
|
||||
canDelete,
|
||||
explicit,
|
||||
removeTagHandler,
|
||||
lineage,
|
||||
}) => {
|
||||
removeTagHandler,
|
||||
canDelete = false,
|
||||
explicit,
|
||||
afterComponent,
|
||||
}: TagComponentProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleClick = React.useCallback(() => {
|
||||
if (explicit && canDelete) {
|
||||
if (explicit && canDelete && removeTagHandler) {
|
||||
removeTagHandler(lineage.join(','));
|
||||
}
|
||||
}, [explicit, lineage, canDelete, removeTagHandler]);
|
||||
@@ -38,23 +69,19 @@ const TagComponent = ({
|
||||
className="tags-tree-delete-button ml-2 text-gray-600"
|
||||
/>
|
||||
)}
|
||||
{afterComponent}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
TagComponent.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
canDelete: PropTypes.bool,
|
||||
explicit: PropTypes.bool,
|
||||
lineage: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
removeTagHandler: PropTypes.func,
|
||||
};
|
||||
|
||||
TagComponent.defaultProps = {
|
||||
removeTagHandler: undefined,
|
||||
canDelete: false,
|
||||
explicit: false,
|
||||
};
|
||||
interface TagsTreeProps {
|
||||
tags: TagTree;
|
||||
parentKey?: string;
|
||||
rootDepth?: number;
|
||||
lineage?: string[];
|
||||
removeTagHandler?: (value: string) => void;
|
||||
afterTagsComponent?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders Tags under a Taxonomy in the nested tree format.
|
||||
@@ -92,23 +119,21 @@ TagComponent.defaultProps = {
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {Object} props.tags - Array of taxonomy tags that are applied to the content.
|
||||
* @param {number} props.rootDepth - Depth of the parent tag (root), used to render tabs for the tree.
|
||||
* @param {string} props.parentKey - Key of the parent tag.
|
||||
* @param {string[]} props.lineage - Lineage of the tag.
|
||||
* @param {(
|
||||
* tagSelectableBoxValue: string,
|
||||
* checked: boolean
|
||||
* ) => void} props.removeTagHandler - Function that is called when removing tags from the tree.
|
||||
*/
|
||||
const TagsTree = ({
|
||||
/** Array of taxonomy tags that are applied to the content. */
|
||||
tags,
|
||||
rootDepth,
|
||||
/** Depth of the parent tag (root), used to render tabs for the tree. */
|
||||
rootDepth = 0,
|
||||
/** Key of the parent tag. */
|
||||
parentKey,
|
||||
lineage,
|
||||
/** Lineage of the tag. */
|
||||
lineage = [],
|
||||
/** Function that is called when removing tags from the tree. */
|
||||
removeTagHandler,
|
||||
}) => {
|
||||
/** Optional component to render after the tags components. */
|
||||
afterTagsComponent,
|
||||
}: TagsTreeProps) => {
|
||||
const { isEditMode } = useContext(ContentTagsDrawerContext);
|
||||
|
||||
if (Object.keys(tags).length === 0) {
|
||||
@@ -132,6 +157,11 @@ const TagsTree = ({
|
||||
explicit={tags[key].explicit}
|
||||
lineage={[...lineage, encodeURIComponent(key)]}
|
||||
removeTagHandler={removeTagHandler}
|
||||
afterComponent={isEditMode && tags[key].explicit && tags[key].isCopied && (
|
||||
// So far, `isCopied` is only used to check if the tag has been imported from a library.
|
||||
// If another function is added to `isCopied`, this may change.
|
||||
<LibraryLockIcon key={key} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{ tags[key].children
|
||||
@@ -142,6 +172,7 @@ const TagsTree = ({
|
||||
parentKey={key}
|
||||
lineage={[...lineage, encodeURIComponent(key)]}
|
||||
removeTagHandler={removeTagHandler}
|
||||
afterTagsComponent={afterTagsComponent}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -150,19 +181,4 @@ const TagsTree = ({
|
||||
);
|
||||
};
|
||||
|
||||
TagsTree.propTypes = {
|
||||
tags: PropTypes.shape({}).isRequired,
|
||||
parentKey: PropTypes.string,
|
||||
rootDepth: PropTypes.number,
|
||||
lineage: PropTypes.arrayOf(PropTypes.string),
|
||||
removeTagHandler: PropTypes.func,
|
||||
};
|
||||
|
||||
TagsTree.defaultProps = {
|
||||
rootDepth: 0,
|
||||
parentKey: undefined,
|
||||
lineage: [],
|
||||
removeTagHandler: undefined,
|
||||
};
|
||||
|
||||
export default TagsTree;
|
||||
@@ -2,14 +2,17 @@ module.exports = {
|
||||
'hierarchical taxonomy tag 1': {
|
||||
explicit: false,
|
||||
canDeleteObjecttag: true,
|
||||
isCopied: false,
|
||||
children: {
|
||||
'hierarchical taxonomy tag 1.7': {
|
||||
explicit: false,
|
||||
canDeleteObjecttag: true,
|
||||
isCopied: false,
|
||||
children: {
|
||||
'hierarchical taxonomy tag 1.7.59': {
|
||||
explicit: true,
|
||||
canDeleteObjecttag: true,
|
||||
isCopied: false,
|
||||
children: {},
|
||||
},
|
||||
},
|
||||
@@ -19,14 +22,17 @@ module.exports = {
|
||||
'hierarchical taxonomy tag 2': {
|
||||
explicit: false,
|
||||
canDeleteObjecttag: true,
|
||||
isCopied: false,
|
||||
children: {
|
||||
'hierarchical taxonomy tag 2.13': {
|
||||
explicit: false,
|
||||
canDeleteObjecttag: true,
|
||||
isCopied: false,
|
||||
children: {
|
||||
'hierarchical taxonomy tag 2.13.46': {
|
||||
explicit: true,
|
||||
canDeleteObjecttag: true,
|
||||
isCopied: false,
|
||||
children: {},
|
||||
},
|
||||
},
|
||||
@@ -36,14 +42,17 @@ module.exports = {
|
||||
'hierarchical taxonomy tag 3': {
|
||||
explicit: false,
|
||||
canDeleteObjecttag: true,
|
||||
isCopied: true,
|
||||
children: {
|
||||
'hierarchical taxonomy tag 3.4': {
|
||||
explicit: false,
|
||||
canDeleteObjecttag: true,
|
||||
isCopied: true,
|
||||
children: {
|
||||
'hierarchical taxonomy tag 3.4.50': {
|
||||
explicit: false,
|
||||
canDeleteObjecttag: true,
|
||||
explicit: true,
|
||||
canDeleteObjecttag: false,
|
||||
isCopied: true,
|
||||
children: {},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useQueries,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
skipToken,
|
||||
} from '@tanstack/react-query';
|
||||
import { useParams } from 'react-router';
|
||||
import { TagData, TagListData } from '@src/taxonomy/data/types';
|
||||
@@ -115,11 +116,10 @@ export const useContentTaxonomyTagsData = (contentId: string) => (
|
||||
* @param contentId The id of the content object
|
||||
* @param enabled Flag to enable/disable the query
|
||||
*/
|
||||
export const useContentData = (contentId: string, enabled: boolean) => (
|
||||
export const useContentData = (contentId?: string, enabled: boolean = true) => (
|
||||
useQuery({
|
||||
queryKey: ['contentData', contentId],
|
||||
queryFn: () => getContentData(contentId),
|
||||
enabled,
|
||||
queryFn: (enabled && contentId) ? () => getContentData(contentId) : skipToken,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Tag {
|
||||
lineage: string[];
|
||||
canChangeObjecttag: boolean;
|
||||
canDeleteObjecttag: boolean;
|
||||
isCopied: boolean;
|
||||
}
|
||||
|
||||
/** A list of the tags from one taxonomy that are applied to a content object. */
|
||||
|
||||
@@ -139,6 +139,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'enable a taxonomy',
|
||||
description: 'Message of the link used in empty drawer message.',
|
||||
},
|
||||
libraryLockIconTooltip: {
|
||||
id: 'course-authoring.content-tags-drawer.tag.library-lock.tooltip',
|
||||
defaultMessage: 'Tags applied within a library cannot be removed',
|
||||
description: 'Tooltip message for the library lock icon.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -12,15 +12,12 @@ import { useContentTaxonomyTagsData } from '../data/apiHooks';
|
||||
import type { ContentTaxonomyTagData, Tag } from '../data/types';
|
||||
import { LoadingSpinner } from '../../generic/Loading';
|
||||
import TagsTree from '../TagsTree';
|
||||
import { TagTree } from '../ContentTagsCollapsible';
|
||||
|
||||
interface TagsSidebarBodyProps {
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
type TagTree = {
|
||||
[key: string]: { children: TagTree, canChangeObjecttag: boolean, canDeleteObjecttag: boolean }
|
||||
};
|
||||
|
||||
const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
|
||||
const intl = useIntl();
|
||||
const [showManageTags, setShowManageTags] = useState(false);
|
||||
@@ -43,6 +40,8 @@ const TagsSidebarBody = ({ readOnly }: TagsSidebarBodyProps) => {
|
||||
children: {},
|
||||
canChangeObjecttag: item.canChangeObjecttag,
|
||||
canDeleteObjecttag: item.canDeleteObjecttag,
|
||||
explicit: false,
|
||||
isCopied: item.isCopied,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import CardHeader from './CardHeader';
|
||||
import TitleButton from './TitleButton';
|
||||
import messages from './messages';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import { OutlineSidebarProvider } from '../outline-sidebar/OutlineSidebarContext';
|
||||
|
||||
const onExpandMock = jest.fn();
|
||||
const onClickMenuButtonMock = jest.fn();
|
||||
@@ -79,6 +80,7 @@ const renderComponent = (props?: object, entry = '/') => {
|
||||
routerProps: {
|
||||
initialEntries: [entry],
|
||||
},
|
||||
extraWrapper: OutlineSidebarProvider,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
ReactNode, useEffect, useRef, useState,
|
||||
ReactNode, useCallback, useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -21,15 +21,16 @@ import {
|
||||
} from '@openedx/paragon/icons';
|
||||
|
||||
import { useContentTagsCount } from '@src/generic/data/apiHooks';
|
||||
import { ContentTagsDrawerSheet } from '@src/content-tags-drawer';
|
||||
import TagCount from '@src/generic/tag-count';
|
||||
import { useEscapeClick } from '@src/hooks';
|
||||
import { XBlockActions } from '@src/data/types';
|
||||
import { RequestStatus, RequestStatusType } from '@src/data/constants';
|
||||
import { ContentTagsDrawerSheet } from '@src/content-tags-drawer';
|
||||
import { ITEM_BADGE_STATUS } from '../constants';
|
||||
import { scrollToElement } from '../utils';
|
||||
import CardStatus from './CardStatus';
|
||||
import messages from './messages';
|
||||
import { useOutlineSidebarContext } from '../outline-sidebar/OutlineSidebarContext';
|
||||
|
||||
interface CardHeaderProps {
|
||||
title: string;
|
||||
@@ -112,7 +113,17 @@ const CardHeader = ({
|
||||
const [searchParams] = useSearchParams();
|
||||
const [titleValue, setTitleValue] = useState(title);
|
||||
const cardHeaderRef = useRef(null);
|
||||
const [isManageTagsDrawerOpen, openManageTagsDrawer, closeManageTagsDrawer] = useToggle(false);
|
||||
const [isLegacyManageTagsDrawerOpen, openLegacyTagsDrawer, closeLegacyTagsDrawer] = useToggle(false);
|
||||
const { setCurrentPageKey } = useOutlineSidebarContext();
|
||||
|
||||
const openManageTagsDrawer = useCallback(() => {
|
||||
const showNewSidebar = getConfig().ENABLE_COURSE_OUTLINE_NEW_DESIGN?.toString().toLowerCase() === 'true';
|
||||
if (showNewSidebar) {
|
||||
setCurrentPageKey('align', cardId);
|
||||
} else {
|
||||
openLegacyTagsDrawer();
|
||||
}
|
||||
}, [setCurrentPageKey, openLegacyTagsDrawer, cardId]);
|
||||
|
||||
// Use studio url as base if proctoringExamConfigurationLink is a relative link
|
||||
const fullProctoringExamConfigurationLink = () => (
|
||||
@@ -283,42 +294,42 @@ const CardHeader = ({
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{actions.draggable && (
|
||||
<>
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-move-up-button`}
|
||||
onClick={onClickMoveUp}
|
||||
disabled={!actions.allowMoveUp}
|
||||
>
|
||||
{intl.formatMessage(messages.menuMoveUp)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-move-down-button`}
|
||||
onClick={onClickMoveDown}
|
||||
disabled={!actions.allowMoveDown}
|
||||
>
|
||||
{intl.formatMessage(messages.menuMoveDown)}
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
<>
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-move-up-button`}
|
||||
onClick={onClickMoveUp}
|
||||
disabled={!actions.allowMoveUp}
|
||||
>
|
||||
{intl.formatMessage(messages.menuMoveUp)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-move-down-button`}
|
||||
onClick={onClickMoveDown}
|
||||
disabled={!actions.allowMoveDown}
|
||||
>
|
||||
{intl.formatMessage(messages.menuMoveDown)}
|
||||
</Dropdown.Item>
|
||||
</>
|
||||
)}
|
||||
{((actions.unlinkable ?? null) !== null || actions.deletable) && <Dropdown.Divider />}
|
||||
{(actions.unlinkable ?? null) !== null && (
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-unlink-button`}
|
||||
onClick={onClickUnlink}
|
||||
disabled={!actions.unlinkable}
|
||||
className="allow-hover-on-disabled"
|
||||
title={!actions.unlinkable ? intl.formatMessage(messages.menuUnlinkDisabledTooltip) : undefined}
|
||||
>
|
||||
{intl.formatMessage(messages.menuUnlink)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-unlink-button`}
|
||||
onClick={onClickUnlink}
|
||||
disabled={!actions.unlinkable}
|
||||
className="allow-hover-on-disabled"
|
||||
title={!actions.unlinkable ? intl.formatMessage(messages.menuUnlinkDisabledTooltip) : undefined}
|
||||
>
|
||||
{intl.formatMessage(messages.menuUnlink)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{actions.deletable && (
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-delete-button`}
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
{intl.formatMessage(messages.menuDelete)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
data-testid={`${namePrefix}-card-header__menu-delete-button`}
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
{intl.formatMessage(messages.menuDelete)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
@@ -326,8 +337,8 @@ const CardHeader = ({
|
||||
</div>
|
||||
<ContentTagsDrawerSheet
|
||||
id={cardId}
|
||||
onClose={/* istanbul ignore next */ () => closeManageTagsDrawer()}
|
||||
showSheet={isManageTagsDrawerOpen}
|
||||
onClose={/* istanbul ignore next */ () => closeLegacyTagsDrawer()}
|
||||
showSheet={isLegacyManageTagsDrawerOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
import {
|
||||
Add as IconAdd, FindInPage, ViewSidebar,
|
||||
} from '@openedx/paragon/icons';
|
||||
import { OUTLINE_SIDEBAR_PAGES } from '@src/course-outline/outline-sidebar/constants';
|
||||
|
||||
import { OutlinePageErrors, XBlockActions } from '@src/data/types';
|
||||
import type { SidebarPage } from '@src/generic/sidebar';
|
||||
@@ -13,6 +12,7 @@ import type { SidebarPage } from '@src/generic/sidebar';
|
||||
import { type OutlineSidebarPageKeys, useOutlineSidebarContext } from '../outline-sidebar/OutlineSidebarContext';
|
||||
|
||||
import messages from './messages';
|
||||
import { getOutlineSidebarPages } from '../outline-sidebar/sidebarPages';
|
||||
|
||||
export interface HeaderActionsProps {
|
||||
actions: {
|
||||
@@ -29,6 +29,7 @@ const HeaderActions = ({
|
||||
}: HeaderActionsProps) => {
|
||||
const intl = useIntl();
|
||||
const { lmsLink } = actions;
|
||||
const sidebarPages = getOutlineSidebarPages();
|
||||
|
||||
const { setCurrentPageKey } = useOutlineSidebarContext();
|
||||
|
||||
@@ -80,7 +81,7 @@ const HeaderActions = ({
|
||||
<Icon src={ViewSidebar} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="mt-1">
|
||||
{Object.entries(OUTLINE_SIDEBAR_PAGES).filter(([, page]) => !page.hideFromActionMenu)
|
||||
{Object.entries(sidebarPages).filter(([, page]) => !page.hideFromActionMenu)
|
||||
.map(([key, page]: [OutlineSidebarPageKeys, SidebarPage]) => (
|
||||
<Dropdown.Item
|
||||
key={key}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import * as CourseAuthoringContext from '@src/CourseAuthoringContext';
|
||||
import * as CourseDetailsApi from '@src/data/apiHooks';
|
||||
import * as ContentDataApi from '@src/content-tags-drawer/data/apiHooks';
|
||||
import * as OutlineSidebarContext from './OutlineSidebarContext';
|
||||
import { OutlineAlignSidebar } from './OutlineAlignSidebar';
|
||||
|
||||
jest.mock('@src/content-tags-drawer', () => ({
|
||||
ContentTagsDrawer: jest.fn(({ id, variant }) => (
|
||||
<div data-testid="content-tags-drawer">
|
||||
drawer-mock-{id}-{variant}
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
describe('OutlineAlignSidebar', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(CourseAuthoringContext, 'useCourseAuthoringContext')
|
||||
.mockReturnValue({
|
||||
courseId: 'course-v1:test+course+run',
|
||||
} as any);
|
||||
jest
|
||||
.spyOn(OutlineSidebarContext, 'useOutlineSidebarContext')
|
||||
.mockReturnValue({
|
||||
currentContainerId:
|
||||
'block-v1:test+course+run+type@sequential+block@seq1',
|
||||
} as any);
|
||||
jest
|
||||
.spyOn(CourseDetailsApi, 'useCourseDetails')
|
||||
.mockReturnValue({
|
||||
data: { name: 'Test Course' },
|
||||
} as any);
|
||||
jest
|
||||
.spyOn(ContentDataApi, 'useContentData')
|
||||
.mockReturnValue({
|
||||
data: { displayName: 'Sequential 1' },
|
||||
} as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders ContentTagsDrawer with the correct id and variant', () => {
|
||||
render(<OutlineAlignSidebar />);
|
||||
|
||||
const drawer = screen.getByTestId('content-tags-drawer');
|
||||
|
||||
expect(drawer).toBeInTheDocument();
|
||||
expect(drawer).toHaveTextContent(
|
||||
'drawer-mock-block-v1:test+course+run+type@sequential+block@seq1-component',
|
||||
);
|
||||
});
|
||||
});
|
||||
39
src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx
Normal file
39
src/course-outline/outline-sidebar/OutlineAlignSidebar.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { SchoolOutline } from '@openedx/paragon/icons';
|
||||
import { ContentTagsDrawer } from '@src/content-tags-drawer';
|
||||
import { useContentData } from '@src/content-tags-drawer/data/apiHooks';
|
||||
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
|
||||
import { useCourseDetails } from '@src/data/apiHooks';
|
||||
import { SidebarTitle } from '@src/generic/sidebar';
|
||||
import { useOutlineSidebarContext } from './OutlineSidebarContext';
|
||||
|
||||
export const OutlineAlignSidebar = () => {
|
||||
const { courseId } = useCourseAuthoringContext();
|
||||
const { currentContainerId } = useOutlineSidebarContext();
|
||||
|
||||
const sidebarContentId = currentContainerId || courseId;
|
||||
|
||||
const {
|
||||
data: courseData,
|
||||
} = useCourseDetails(courseId);
|
||||
|
||||
const {
|
||||
data: contentData,
|
||||
} = useContentData(currentContainerId);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SidebarTitle
|
||||
title={
|
||||
contentData && 'displayName' in contentData
|
||||
? contentData.displayName
|
||||
: courseData?.name || ''
|
||||
}
|
||||
icon={SchoolOutline}
|
||||
/>
|
||||
<ContentTagsDrawer
|
||||
id={sidebarContentId}
|
||||
variant="component"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,13 +3,14 @@ import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
import { Sidebar } from '@src/generic/sidebar';
|
||||
|
||||
import { OUTLINE_SIDEBAR_PAGES } from '@src/course-outline/outline-sidebar/constants';
|
||||
import OutlineHelpSidebar from './OutlineHelpSidebar';
|
||||
import { useOutlineSidebarContext } from './OutlineSidebarContext';
|
||||
import { isOutlineNewDesignEnabled } from '../utils';
|
||||
import { getOutlineSidebarPages } from './sidebarPages';
|
||||
|
||||
const OutlineSideBar = () => {
|
||||
const isMedium = useMediaQuery({ maxWidth: breakpoints.medium.maxWidth });
|
||||
const sidebarPages = getOutlineSidebarPages();
|
||||
|
||||
const {
|
||||
currentPageKey,
|
||||
@@ -31,7 +32,7 @@ const OutlineSideBar = () => {
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
pages={OUTLINE_SIDEBAR_PAGES}
|
||||
pages={sidebarPages}
|
||||
currentPageKey={currentPageKey}
|
||||
setCurrentPageKey={setCurrentPageKey}
|
||||
isOpen={isOpen}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useToggle } from '@openedx/paragon';
|
||||
import { useStateWithUrlSearchParam } from '@src/hooks';
|
||||
import { isOutlineNewDesignEnabled } from '../utils';
|
||||
|
||||
export type OutlineSidebarPageKeys = 'help' | 'info' | 'add';
|
||||
export type OutlineSidebarPageKeys = 'help' | 'info' | 'add' | 'align';
|
||||
export type OutlineFlowType = 'use-section' | 'use-subsection' | 'use-unit' | null;
|
||||
export type OutlineFlow = {
|
||||
flowType: 'use-section';
|
||||
@@ -25,7 +25,7 @@ export type OutlineFlow = {
|
||||
|
||||
interface OutlineSidebarContextData {
|
||||
currentPageKey: OutlineSidebarPageKeys;
|
||||
setCurrentPageKey: (pageKey: OutlineSidebarPageKeys) => void;
|
||||
setCurrentPageKey: (pageKey: OutlineSidebarPageKeys, containerId?: string) => void;
|
||||
currentFlow: OutlineFlow | null;
|
||||
startCurrentFlow: (flow: OutlineFlow) => void;
|
||||
stopCurrentFlow: () => void;
|
||||
@@ -33,12 +33,17 @@ interface OutlineSidebarContextData {
|
||||
open: () => void;
|
||||
toggle: () => void;
|
||||
selectedContainerId?: string;
|
||||
// The Id of the container used in the current sidebar page
|
||||
// The container is not necessarily selected to open a selected sidebar.
|
||||
// Example: Align sidebar
|
||||
currentContainerId?: string;
|
||||
openContainerInfoSidebar: (containerId: string) => void;
|
||||
}
|
||||
|
||||
const OutlineSidebarContext = createContext<OutlineSidebarContextData | undefined>(undefined);
|
||||
|
||||
export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNode }) => {
|
||||
const [currentContainerId, setCurrentContainerId] = useState<string>();
|
||||
const [currentPageKey, setCurrentPageKeyState] = useStateWithUrlSearchParam<OutlineSidebarPageKeys>(
|
||||
'info',
|
||||
'sidebar',
|
||||
@@ -64,9 +69,10 @@ export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNod
|
||||
setCurrentFlow(null);
|
||||
}, [setCurrentFlow]);
|
||||
|
||||
const setCurrentPageKey = useCallback((pageKey: OutlineSidebarPageKeys) => {
|
||||
const setCurrentPageKey = useCallback((pageKey: OutlineSidebarPageKeys, containerId?: string) => {
|
||||
setCurrentPageKeyState(pageKey);
|
||||
setCurrentFlow(null);
|
||||
setCurrentContainerId(containerId);
|
||||
open();
|
||||
}, [open, setCurrentFlow]);
|
||||
|
||||
@@ -104,6 +110,7 @@ export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNod
|
||||
open,
|
||||
toggle,
|
||||
selectedContainerId,
|
||||
currentContainerId,
|
||||
openContainerInfoSidebar,
|
||||
}),
|
||||
[
|
||||
@@ -116,6 +123,7 @@ export const OutlineSidebarProvider = ({ children }: { children?: React.ReactNod
|
||||
open,
|
||||
toggle,
|
||||
selectedContainerId,
|
||||
currentContainerId,
|
||||
openContainerInfoSidebar,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { HelpOutline, Info, Plus } from '@openedx/paragon/icons';
|
||||
import type { SidebarPage } from '@src/generic/sidebar';
|
||||
import OutlineHelpSidebar from './OutlineHelpSidebar';
|
||||
import { OutlineInfoSidebar } from './OutlineInfoSidebar';
|
||||
import messages from './messages';
|
||||
import { AddSidebar } from './AddSidebar';
|
||||
import type { OutlineSidebarPageKeys } from './OutlineSidebarContext';
|
||||
|
||||
export type OutlineSidebarPages = Record<OutlineSidebarPageKeys, SidebarPage>;
|
||||
|
||||
export const OUTLINE_SIDEBAR_PAGES: OutlineSidebarPages = {
|
||||
info: {
|
||||
component: OutlineInfoSidebar,
|
||||
icon: Info,
|
||||
title: messages.sidebarButtonInfo,
|
||||
},
|
||||
help: {
|
||||
component: OutlineHelpSidebar,
|
||||
icon: HelpOutline,
|
||||
title: messages.sidebarButtonHelp,
|
||||
},
|
||||
add: {
|
||||
component: AddSidebar,
|
||||
icon: Plus,
|
||||
title: messages.sidebarButtonAdd,
|
||||
hideFromActionMenu: true,
|
||||
},
|
||||
};
|
||||
@@ -80,6 +80,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Info',
|
||||
description: 'Button label for the info sidebar',
|
||||
},
|
||||
sidebarButtonAlign: {
|
||||
id: 'course-authoring.course-outline.sidebar.sidebar-button-align',
|
||||
defaultMessage: 'Align',
|
||||
description: 'Alt text for the align button in the outline sidebar',
|
||||
},
|
||||
sidebarSectionSummary: {
|
||||
id: 'course-authoring.course-outline.sidebar.sidebar-section-summary',
|
||||
defaultMessage: 'Course Content Summary',
|
||||
|
||||
46
src/course-outline/outline-sidebar/sidebarPages.ts
Normal file
46
src/course-outline/outline-sidebar/sidebarPages.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
HelpOutline, Info, Plus, Tag,
|
||||
} from '@openedx/paragon/icons';
|
||||
import type { SidebarPage } from '@src/generic/sidebar';
|
||||
import OutlineHelpSidebar from './OutlineHelpSidebar';
|
||||
import { OutlineInfoSidebar } from './OutlineInfoSidebar';
|
||||
import messages from './messages';
|
||||
import { AddSidebar } from './AddSidebar';
|
||||
import { OutlineAlignSidebar } from './OutlineAlignSidebar';
|
||||
|
||||
export type OutlineSidebarPages = {
|
||||
info: SidebarPage;
|
||||
help: SidebarPage;
|
||||
add: SidebarPage;
|
||||
align?: SidebarPage;
|
||||
};
|
||||
|
||||
export const getOutlineSidebarPages = (): OutlineSidebarPages => {
|
||||
const showAlignSidebar = getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true';
|
||||
return {
|
||||
info: {
|
||||
component: OutlineInfoSidebar,
|
||||
icon: Info,
|
||||
title: messages.sidebarButtonInfo,
|
||||
},
|
||||
...(showAlignSidebar && {
|
||||
align: {
|
||||
component: OutlineAlignSidebar,
|
||||
icon: Tag,
|
||||
title: messages.sidebarButtonAlign,
|
||||
},
|
||||
}),
|
||||
help: {
|
||||
component: OutlineHelpSidebar,
|
||||
icon: HelpOutline,
|
||||
title: messages.sidebarButtonHelp,
|
||||
},
|
||||
add: {
|
||||
component: AddSidebar,
|
||||
icon: Plus,
|
||||
title: messages.sidebarButtonAdd,
|
||||
hideFromActionMenu: true,
|
||||
},
|
||||
} satisfies OutlineSidebarPages;
|
||||
};
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
act, fireEvent, initializeMocks, render, screen, waitFor, within,
|
||||
} from '@src/testUtils';
|
||||
import { XBlock } from '@src/data/types';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
import SectionCard from './SectionCard';
|
||||
import { OutlineSidebarProvider } from '../outline-sidebar/OutlineSidebarContext';
|
||||
import * as OutlineSidebarContext from '../outline-sidebar/OutlineSidebarContext';
|
||||
import { OutlineInfoSidebar } from '../outline-sidebar/OutlineInfoSidebar';
|
||||
|
||||
const mockUseAcceptLibraryBlockChanges = jest.fn();
|
||||
const mockUseIgnoreLibraryBlockChanges = jest.fn();
|
||||
@@ -118,7 +120,7 @@ const renderComponent = (props?: object, entry = '/course/:courseId') => render(
|
||||
routerProps: {
|
||||
initialEntries: [entry],
|
||||
},
|
||||
extraWrapper: OutlineSidebarProvider,
|
||||
extraWrapper: OutlineSidebarContext.OutlineSidebarProvider,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -306,4 +308,71 @@ describe('<SectionCard />', () => {
|
||||
|
||||
await waitFor(() => expect(mockUseIgnoreLibraryBlockChanges).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should open legacy manage tags', async () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'false',
|
||||
});
|
||||
renderComponent();
|
||||
const element = await screen.findByTestId('section-card');
|
||||
const menu = await within(element).findByTestId('section-card-header__menu-button');
|
||||
await fireEvent.click(menu);
|
||||
|
||||
const manageTagsBtn = await within(element).findByTestId('section-card-header__menu-manage-tags-button');
|
||||
expect(manageTagsBtn).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(manageTagsBtn);
|
||||
|
||||
const drawer = await screen.findByRole('alert');
|
||||
expect(within(drawer).getByText(/manage tags/i));
|
||||
});
|
||||
|
||||
it('should open align sidebar', async () => {
|
||||
const mockSetCurrentPageKey = jest.fn();
|
||||
|
||||
const testSidebarPage = {
|
||||
component: OutlineInfoSidebar,
|
||||
icon: Info,
|
||||
title: '',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(OutlineSidebarContext, 'useOutlineSidebarContext')
|
||||
.mockImplementation(() => ({
|
||||
setCurrentPageKey: mockSetCurrentPageKey,
|
||||
currentPageKey: 'info',
|
||||
sidebarPages: {
|
||||
info: testSidebarPage,
|
||||
help: testSidebarPage,
|
||||
add: testSidebarPage,
|
||||
},
|
||||
isOpen: true,
|
||||
open: jest.fn(),
|
||||
toggle: jest.fn(),
|
||||
currentFlow: null,
|
||||
startCurrentFlow: jest.fn(),
|
||||
stopCurrentFlow: jest.fn(),
|
||||
openContainerInfoSidebar: jest.fn(),
|
||||
}));
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'true',
|
||||
});
|
||||
renderComponent();
|
||||
const element = await screen.findByTestId('section-card');
|
||||
const menu = await within(element).findByTestId('section-card-header__menu-button');
|
||||
await fireEvent.click(menu);
|
||||
|
||||
const manageTagsBtn = await within(element).findByTestId('section-card-header__menu-manage-tags-button');
|
||||
expect(manageTagsBtn).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(manageTagsBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetCurrentPageKey).toHaveBeenCalledWith('align', section.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
act, fireEvent, initializeMocks, render, screen, waitFor, within,
|
||||
} from '@src/testUtils';
|
||||
import { XBlock } from '@src/data/types';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import cardHeaderMessages from '../card-header/messages';
|
||||
import SubsectionCard from './SubsectionCard';
|
||||
import { OutlineSidebarProvider } from '../outline-sidebar/OutlineSidebarContext';
|
||||
import * as OutlineSidebarContext from '../outline-sidebar/OutlineSidebarContext';
|
||||
import { OutlineInfoSidebar } from '../outline-sidebar/OutlineInfoSidebar';
|
||||
|
||||
let store;
|
||||
const containerKey = 'lct:org:lib:unit:1';
|
||||
@@ -145,7 +147,7 @@ const renderComponent = (props?: object, entry = '/course/:courseId') => render(
|
||||
routerProps: {
|
||||
initialEntries: [entry],
|
||||
},
|
||||
extraWrapper: OutlineSidebarProvider,
|
||||
extraWrapper: OutlineSidebarContext.OutlineSidebarProvider,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -416,4 +418,71 @@ describe('<SubsectionCard />', () => {
|
||||
|
||||
await waitFor(() => expect(mockUseIgnoreLibraryBlockChanges).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should open legacy manage tags', async () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'false',
|
||||
});
|
||||
renderComponent();
|
||||
const element = await screen.findByTestId('subsection-card');
|
||||
const menu = await within(element).findByTestId('subsection-card-header__menu-button');
|
||||
await fireEvent.click(menu);
|
||||
|
||||
const manageTagsBtn = await within(element).findByTestId('subsection-card-header__menu-manage-tags-button');
|
||||
expect(manageTagsBtn).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(manageTagsBtn);
|
||||
|
||||
const drawer = await screen.findByRole('alert');
|
||||
expect(within(drawer).getByText(/manage tags/i));
|
||||
});
|
||||
|
||||
it('should open align sidebar', async () => {
|
||||
const mockSetCurrentPageKey = jest.fn();
|
||||
|
||||
const testSidebarPage = {
|
||||
component: OutlineInfoSidebar,
|
||||
icon: Info,
|
||||
title: '',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(OutlineSidebarContext, 'useOutlineSidebarContext')
|
||||
.mockImplementation(() => ({
|
||||
setCurrentPageKey: mockSetCurrentPageKey,
|
||||
currentPageKey: 'info',
|
||||
sidebarPages: {
|
||||
info: testSidebarPage,
|
||||
help: testSidebarPage,
|
||||
add: testSidebarPage,
|
||||
},
|
||||
isOpen: true,
|
||||
open: jest.fn(),
|
||||
toggle: jest.fn(),
|
||||
currentFlow: null,
|
||||
startCurrentFlow: jest.fn(),
|
||||
stopCurrentFlow: jest.fn(),
|
||||
openContainerInfoSidebar: jest.fn(),
|
||||
}));
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'true',
|
||||
});
|
||||
renderComponent();
|
||||
const element = await screen.findByTestId('subsection-card');
|
||||
const menu = await within(element).findByTestId('subsection-card-header__menu-button');
|
||||
await fireEvent.click(menu);
|
||||
|
||||
const manageTagsBtn = await within(element).findByTestId('subsection-card-header__menu-manage-tags-button');
|
||||
expect(manageTagsBtn).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(manageTagsBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetCurrentPageKey).toHaveBeenCalledWith('align', subsection.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,11 @@ import {
|
||||
} from '@src/testUtils';
|
||||
|
||||
import { XBlock } from '@src/data/types';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
import UnitCard from './UnitCard';
|
||||
import cardMessages from '../card-header/messages';
|
||||
import { OutlineSidebarProvider } from '../outline-sidebar/OutlineSidebarContext';
|
||||
import * as OutlineSidebarContext from '../outline-sidebar/OutlineSidebarContext';
|
||||
import { OutlineInfoSidebar } from '../outline-sidebar/OutlineInfoSidebar';
|
||||
|
||||
const mockUseAcceptLibraryBlockChanges = jest.fn();
|
||||
const mockUseIgnoreLibraryBlockChanges = jest.fn();
|
||||
@@ -107,7 +109,7 @@ const renderComponent = (props?: object) => render(
|
||||
{
|
||||
path: '/course/:courseId',
|
||||
params: { courseId: '5' },
|
||||
extraWrapper: OutlineSidebarProvider,
|
||||
extraWrapper: OutlineSidebarContext.OutlineSidebarProvider,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -274,4 +276,71 @@ describe('<UnitCard />', () => {
|
||||
|
||||
await waitFor(() => expect(mockUseIgnoreLibraryBlockChanges).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should open legacy manage tags', async () => {
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'false',
|
||||
});
|
||||
renderComponent();
|
||||
const element = await screen.findByTestId('unit-card');
|
||||
const menu = await within(element).findByTestId('unit-card-header__menu-button');
|
||||
await fireEvent.click(menu);
|
||||
|
||||
const manageTagsBtn = await within(element).findByTestId('unit-card-header__menu-manage-tags-button');
|
||||
expect(manageTagsBtn).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(manageTagsBtn);
|
||||
|
||||
const drawer = await screen.findByRole('alert');
|
||||
expect(within(drawer).getByText(/manage tags/i));
|
||||
});
|
||||
|
||||
it('should open align sidebar', async () => {
|
||||
const mockSetCurrentPageKey = jest.fn();
|
||||
|
||||
const testSidebarPage = {
|
||||
component: OutlineInfoSidebar,
|
||||
icon: Info,
|
||||
title: '',
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(OutlineSidebarContext, 'useOutlineSidebarContext')
|
||||
.mockImplementation(() => ({
|
||||
setCurrentPageKey: mockSetCurrentPageKey,
|
||||
currentPageKey: 'info',
|
||||
sidebarPages: {
|
||||
info: testSidebarPage,
|
||||
help: testSidebarPage,
|
||||
add: testSidebarPage,
|
||||
},
|
||||
isOpen: true,
|
||||
open: jest.fn(),
|
||||
toggle: jest.fn(),
|
||||
currentFlow: null,
|
||||
startCurrentFlow: jest.fn(),
|
||||
stopCurrentFlow: jest.fn(),
|
||||
openContainerInfoSidebar: jest.fn(),
|
||||
}));
|
||||
setConfig({
|
||||
...getConfig(),
|
||||
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
|
||||
ENABLE_COURSE_OUTLINE_NEW_DESIGN: 'true',
|
||||
});
|
||||
renderComponent();
|
||||
const element = await screen.findByTestId('unit-card');
|
||||
const menu = await within(element).findByTestId('unit-card-header__menu-button');
|
||||
await fireEvent.click(menu);
|
||||
|
||||
const manageTagsBtn = await within(element).findByTestId('unit-card-header__menu-manage-tags-button');
|
||||
expect(manageTagsBtn).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(manageTagsBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetCurrentPageKey).toHaveBeenCalledWith('align', unit.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user