feat: use actions and other flags to control item actions

Uses action flags from API to control display of delete, duplicate, child new button and dragging.
Use isHeaderVisible flag to control display of subsection headers.

All these changes prepare outline for entrance exam section display.

feat: use actions flags for subsections

test: actions
This commit is contained in:
Navin Karkera
2024-01-08 19:47:41 +05:30
committed by Kristin Aoki
parent 70b4795650
commit b417cd64a0
23 changed files with 443 additions and 122 deletions

View File

@@ -19,7 +19,6 @@ import {
import { useSelector } from 'react-redux';
import {
DraggableList,
SortableItem,
ErrorAlert,
} from '@edx/frontend-lib-content-components';
@@ -43,6 +42,7 @@ import EmptyPlaceholder from './empty-placeholder/EmptyPlaceholder';
import PublishModal from './publish-modal/PublishModal';
import ConfigureModal from './configure-modal/ConfigureModal';
import DeleteModal from './delete-modal/DeleteModal';
import ConditionalSortableElement from './drag-helper/ConditionalSortableElement';
import { useCourseOutline } from './hooks';
import messages from './messages';
@@ -53,6 +53,7 @@ const CourseOutline = ({ courseId }) => {
courseName,
savingStatus,
statusBarData,
courseActions,
sectionsList,
isLoading,
isReIndexShow,
@@ -175,6 +176,7 @@ const CourseOutline = ({ courseId }) => {
headerNavigationsActions={headerNavigationsActions}
isDisabledReindexButton={isDisabledReindexButton}
hasSections={Boolean(sectionsList.length)}
courseActions={courseActions}
/>
)}
/>
@@ -201,9 +203,10 @@ const CourseOutline = ({ courseId }) => {
<>
<DraggableList itemList={sections} setState={setSections} updateOrder={finalizeSectionOrder}>
{sections.map((section, index) => (
<SortableItem
<ConditionalSortableElement
id={section.id}
key={section.id}
draggable={section.actions.draggable}
componentStyle={{
background: 'white',
padding: '1.75rem',
@@ -231,9 +234,12 @@ const CourseOutline = ({ courseId }) => {
updateOrder={finalizeSubsectionOrder(section)}
>
{section.childInfo.children.map((subsection) => (
<SortableItem
<ConditionalSortableElement
id={subsection.id}
key={subsection.id}
draggable={
subsection.actions.draggable && !(subsection.isHeaderVisible === false)
}
componentStyle={{
background: '#f8f7f6',
padding: '1rem 1.5rem',
@@ -267,26 +273,31 @@ const CourseOutline = ({ courseId }) => {
/>
))}
</SubsectionCard>
</SortableItem>
</ConditionalSortableElement>
))}
</DraggableList>
</SectionCard>
</SortableItem>
</ConditionalSortableElement>
))}
</DraggableList>
<Button
data-testid="new-section-button"
className="mt-4"
variant="outline-primary"
onClick={handleNewSectionSubmit}
iconBefore={IconAdd}
block
>
{intl.formatMessage(messages.newSectionButton)}
</Button>
{courseActions.childAddable && (
<Button
data-testid="new-section-button"
className="mt-4"
variant="outline-primary"
onClick={handleNewSectionSubmit}
iconBefore={IconAdd}
block
>
{intl.formatMessage(messages.newSectionButton)}
</Button>
)}
</>
) : (
<EmptyPlaceholder onCreateNewSection={handleNewSectionSubmit} />
<EmptyPlaceholder
onCreateNewSection={handleNewSectionSubmit}
childAddable={courseActions.childAddable}
/>
)}
</div>
</section>

View File

@@ -8,3 +8,4 @@
@import "./highlights-modal/HighlightsModal";
@import "./publish-modal/PublishModal";
@import "./configure-modal/ConfigureModal";
@import "./drag-helper/ConditionalSortableElement";

View File

@@ -794,4 +794,36 @@ describe('<CourseOutline />', () => {
expect(subsection1).toBe(subsection1New);
});
});
it('check that drag handle is not visible for non-draggable sections', async () => {
cleanup();
axiosMock
.onGet(getCourseOutlineIndexApiUrl(courseId))
.reply(200, {
...courseOutlineIndexMock,
courseStructure: {
...courseOutlineIndexMock.courseStructure,
childInfo: {
...courseOutlineIndexMock.courseStructure.childInfo,
children: [
{
...courseOutlineIndexMock.courseStructure.childInfo.children[0],
actions: {
draggable: false,
childAddable: true,
deletable: true,
duplicable: true,
},
},
...courseOutlineIndexMock.courseStructure.childInfo.children.slice(1),
],
},
},
});
const { queryByTestId } = render(<RootWrapper />);
await waitFor(() => {
expect(queryByTestId('conditional-sortable-element--no-drag-handle')).toBeInTheDocument();
});
});
});

View File

@@ -32,6 +32,7 @@ const CardHeader = ({
onClickDuplicate,
titleComponent,
namePrefix,
actions,
}) => {
const intl = useIntl();
const [titleValue, setTitleValue] = useState(title);
@@ -103,18 +104,22 @@ const CardHeader = ({
>
{intl.formatMessage(messages.menuConfigure)}
</Dropdown.Item>
<Dropdown.Item
data-testid={`${namePrefix}-card-header__menu-duplicate-button`}
onClick={onClickDuplicate}
>
{intl.formatMessage(messages.menuDuplicate)}
</Dropdown.Item>
<Dropdown.Item
data-testid={`${namePrefix}-card-header__menu-delete-button`}
onClick={onClickDelete}
>
{intl.formatMessage(messages.menuDelete)}
</Dropdown.Item>
{actions.duplicable && (
<Dropdown.Item
data-testid={`${namePrefix}-card-header__menu-duplicate-button`}
onClick={onClickDuplicate}
>
{intl.formatMessage(messages.menuDuplicate)}
</Dropdown.Item>
)}
{actions.deletable && (
<Dropdown.Item
data-testid={`${namePrefix}-card-header__menu-delete-button`}
onClick={onClickDelete}
>
{intl.formatMessage(messages.menuDelete)}
</Dropdown.Item>
)}
</Dropdown.Menu>
</Dropdown>
</div>
@@ -138,6 +143,12 @@ CardHeader.propTypes = {
onClickDuplicate: PropTypes.func.isRequired,
titleComponent: PropTypes.node.isRequired,
namePrefix: PropTypes.string.isRequired,
actions: PropTypes.shape({
deletable: PropTypes.bool.isRequired,
draggable: PropTypes.bool.isRequired,
childAddable: PropTypes.bool.isRequired,
duplicable: PropTypes.bool.isRequired,
}).isRequired,
};
export default CardHeader;

View File

@@ -30,6 +30,12 @@ const cardHeaderProps = {
onClickDelete: onClickDeleteMock,
onClickDuplicate: onClickDuplicateMock,
namePrefix: 'section',
actions: {
draggable: true,
childAddable: true,
deletable: true,
duplicable: true,
},
};
const renderComponent = (props) => {

View File

@@ -6,3 +6,4 @@ export const getSectionsList = (state) => state.courseOutline.sectionsList;
export const getCurrentItem = (state) => state.courseOutline.currentItem;
export const getCurrentSection = (state) => state.courseOutline.currentSection;
export const getCurrentSubsection = (state) => state.courseOutline.currentSubsection;
export const getCourseActions = (state) => state.courseOutline.actions;

View File

@@ -31,6 +31,12 @@ const slice = createSlice({
currentSection: {},
currentSubsection: {},
currentItem: {},
actions: {
deletable: true,
draggable: true,
childAddable: true,
duplicable: true,
},
},
reducers: {
fetchOutlineIndexSuccess: (state, { payload }) => {
@@ -61,6 +67,12 @@ const slice = createSlice({
...payload,
};
},
updateCourseActions: (state, { payload }) => {
state.actions = {
...state.actions,
...payload,
};
},
fetchStatusBarChecklistSuccess: (state, { payload }) => {
state.statusBarData.checklist = {
...state.statusBarData.checklist,
@@ -166,6 +178,7 @@ export const {
updateOutlineIndexLoadingStatus,
updateReindexLoadingStatus,
updateStatusBar,
updateCourseActions,
fetchStatusBarChecklistSuccess,
fetchStatusBarSelPacedSuccess,
updateFetchSectionLoadingStatus,

View File

@@ -34,6 +34,7 @@ import {
updateOutlineIndexLoadingStatus,
updateReindexLoadingStatus,
updateStatusBar,
updateCourseActions,
fetchStatusBarChecklistSuccess,
fetchStatusBarSelPacedSuccess,
updateSavingStatus,
@@ -59,6 +60,7 @@ export function fetchCourseOutlineIndexQuery(courseId) {
highlightsEnabledForMessaging,
videoSharingEnabled,
videoSharingOptions,
actions,
},
} = outlineIndex;
dispatch(fetchOutlineIndexSuccess(outlineIndex));
@@ -68,6 +70,7 @@ export function fetchCourseOutlineIndexQuery(courseId) {
videoSharingOptions,
videoSharingEnabled,
}));
dispatch(updateCourseActions(actions));
dispatch(updateOutlineIndexLoadingStatus({ status: RequestStatus.SUCCESSFUL }));
} catch (error) {

View File

@@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Row } from '@edx/paragon';
import { SortableItem } from '@edx/frontend-lib-content-components';
const ConditionalSortableElement = ({
id,
draggable,
children,
componentStyle,
}) => {
if (draggable) {
return (
<SortableItem
id={id}
componentStyle={componentStyle}
>
<div className="extend-margin">
{children}
</div>
</SortableItem>
);
}
return (
<Row
data-testid="conditional-sortable-element--no-drag-handle"
style={componentStyle}
className="mx-0"
>
{children}
</Row>
);
};
ConditionalSortableElement.defaultProps = {
componentStyle: null,
};
ConditionalSortableElement.propTypes = {
id: PropTypes.string.isRequired,
draggable: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
componentStyle: PropTypes.shape({}),
};
export default ConditionalSortableElement;

View File

@@ -0,0 +1,8 @@
.extend-margin {
display: flex;
flex-grow: 1;
.item-children {
margin-right: -2.75rem;
}
}

View File

@@ -6,35 +6,41 @@ import { Button, OverlayTrigger, Tooltip } from '@edx/paragon';
import messages from './messages';
const EmptyPlaceholder = ({ onCreateNewSection }) => {
const EmptyPlaceholder = ({
onCreateNewSection,
childAddable,
}) => {
const intl = useIntl();
return (
<div className="outline-empty-placeholder bg-gray-100" data-testid="empty-placeholder">
<p className="mb-0 text-gray-500">{intl.formatMessage(messages.title)}</p>
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip id={intl.formatMessage(messages.tooltip)}>
{intl.formatMessage(messages.tooltip)}
</Tooltip>
)}
>
<Button
variant="primary"
size="sm"
iconBefore={IconAdd}
onClick={onCreateNewSection}
{childAddable && (
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip id={intl.formatMessage(messages.tooltip)}>
{intl.formatMessage(messages.tooltip)}
</Tooltip>
)}
>
{intl.formatMessage(messages.button)}
</Button>
</OverlayTrigger>
<Button
variant="primary"
size="sm"
iconBefore={IconAdd}
onClick={onCreateNewSection}
>
{intl.formatMessage(messages.button)}
</Button>
</OverlayTrigger>
)}
</div>
);
};
EmptyPlaceholder.propTypes = {
onCreateNewSection: PropTypes.func.isRequired,
childAddable: PropTypes.bool.isRequired,
};
export default EmptyPlaceholder;

View File

@@ -9,7 +9,10 @@ const onCreateNewSectionMock = jest.fn();
const renderComponent = () => render(
<IntlProvider locale="en">
<EmptyPlaceholder onCreateNewSection={onCreateNewSectionMock} />
<EmptyPlaceholder
onCreateNewSection={onCreateNewSectionMock}
childAddable
/>
</IntlProvider>,
);

View File

@@ -16,6 +16,7 @@ const HeaderNavigations = ({
isSectionsExpanded,
isDisabledReindexButton,
hasSections,
courseActions,
}) => {
const intl = useIntl();
const {
@@ -24,21 +25,23 @@ const HeaderNavigations = ({
return (
<nav className="header-navigations ml-auto">
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip id={intl.formatMessage(messages.newSectionButtonTooltip)}>
{intl.formatMessage(messages.newSectionButtonTooltip)}
</Tooltip>
)}
>
<Button
iconBefore={IconAdd}
onClick={handleNewSection}
{courseActions.childAddable && (
<OverlayTrigger
placement="bottom"
overlay={(
<Tooltip id={intl.formatMessage(messages.newSectionButtonTooltip)}>
{intl.formatMessage(messages.newSectionButtonTooltip)}
</Tooltip>
)}
>
{intl.formatMessage(messages.newSectionButton)}
</Button>
</OverlayTrigger>
<Button
iconBefore={IconAdd}
onClick={handleNewSection}
>
{intl.formatMessage(messages.newSectionButton)}
</Button>
</OverlayTrigger>
)}
{isReIndexShow && (
<OverlayTrigger
placement="bottom"
@@ -100,6 +103,12 @@ HeaderNavigations.propTypes = {
lmsLink: PropTypes.string.isRequired,
}).isRequired,
hasSections: PropTypes.bool.isRequired,
courseActions: PropTypes.shape({
deletable: PropTypes.bool.isRequired,
draggable: PropTypes.bool.isRequired,
childAddable: PropTypes.bool.isRequired,
duplicable: PropTypes.bool.isRequired,
}).isRequired,
};
export default HeaderNavigations;

View File

@@ -17,6 +17,13 @@ const headerNavigationsActions = {
lmsLink: '',
};
const courseActions = {
draggable: true,
childAddable: true,
deletable: true,
duplicable: true,
};
const renderComponent = (props) => render(
<IntlProvider locale="en">
<HeaderNavigations
@@ -25,6 +32,7 @@ const renderComponent = (props) => render(
isDisabledReindexButton={false}
isReIndexShow
hasSections
courseActions={courseActions}
{...props}
/>
</IntlProvider>,

View File

@@ -17,6 +17,7 @@ import {
getSavingStatus,
getStatusBarData,
getSectionsList,
getCourseActions,
getCurrentItem,
getCurrentSection,
getCurrentSubsection,
@@ -53,6 +54,7 @@ const useCourseOutline = ({ courseId }) => {
const { outlineIndexLoadingStatus, reIndexLoadingStatus } = useSelector(getLoadingStatus);
const statusBarData = useSelector(getStatusBarData);
const savingStatus = useSelector(getSavingStatus);
const courseActions = useSelector(getCourseActions);
const sectionsList = useSelector(getSectionsList);
const currentItem = useSelector(getCurrentItem);
const currentSection = useSelector(getCurrentSection);
@@ -213,6 +215,7 @@ const useCourseOutline = ({ courseId }) => {
}, [reIndexLoadingStatus]);
return {
courseActions,
savingStatus,
sectionsList,
isLoading: outlineIndexLoadingStatus === RequestStatus.IN_PROGRESS,

View File

@@ -56,6 +56,9 @@ const SectionCard = ({
visibilityState,
staffOnlyMessage,
highlights,
actions,
isHeaderVisible = true,
explanatoryMessage = '',
} = section;
const sectionStatus = getItemStatus({
@@ -120,25 +123,29 @@ const SectionCard = ({
ref={currentRef}
>
<div>
<CardHeader
sectionId={id}
title={displayName}
status={sectionStatus}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickConfigure={onOpenConfigureModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
/>
{isHeaderVisible && (
<CardHeader
sectionId={id}
title={displayName}
status={sectionStatus}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickConfigure={onOpenConfigureModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
/>
)}
<div className="section-card__content" data-testid="section-card__content">
{explanatoryMessage && <p className="text-secondary-400 x-small mb-1">{explanatoryMessage}</p>}
<div className="outline-section__status">
<Button
className="section-card__highlights"
@@ -152,18 +159,20 @@ const SectionCard = ({
</div>
</div>
{isExpanded && (
<div data-testid="section-card__subsections" className="section-card__subsections">
<div data-testid="section-card__subsections" className="item-children section-card__subsections">
{children}
<Button
data-testid="new-subsection-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewSubsectionSubmit}
>
{intl.formatMessage(messages.newSubsectionButton)}
</Button>
{actions.childAddable && (
<Button
data-testid="new-subsection-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewSubsectionSubmit}
>
{intl.formatMessage(messages.newSubsectionButton)}
</Button>
)}
</div>
)}
</div>
@@ -187,6 +196,14 @@ SectionCard.propTypes = {
staffOnlyMessage: PropTypes.bool.isRequired,
highlights: PropTypes.arrayOf(PropTypes.string).isRequired,
shouldScroll: PropTypes.bool,
explanatoryMessage: PropTypes.string,
actions: PropTypes.shape({
deletable: PropTypes.bool.isRequired,
draggable: PropTypes.bool.isRequired,
childAddable: PropTypes.bool.isRequired,
duplicable: PropTypes.bool.isRequired,
}).isRequired,
isHeaderVisible: PropTypes.bool,
}).isRequired,
children: PropTypes.node,
onOpenHighlightsModal: PropTypes.func.isRequired,

View File

@@ -3,7 +3,6 @@
.section-card__subsections {
margin-top: $spacer;
margin-right: -2.75rem;
}
.section-card-title {

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import {
act, render, fireEvent, within,
} from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
@@ -24,6 +26,13 @@ const section = {
staffOnlyMessage: false,
hasChanges: false,
highlights: ['highlight 1', 'highlight 2'],
actions: {
draggable: true,
childAddable: true,
deletable: true,
duplicable: true,
},
isHeaderVisible: true,
};
const onEditSectionSubmit = jest.fn();
@@ -148,4 +157,34 @@ describe('<SectionCard />', () => {
});
expect(await findByText(cardHeaderMessages.statusBadgeDraft.defaultMessage)).toBeInTheDocument();
});
it('hides header based on isHeaderVisible flag', async () => {
const { queryByTestId } = renderComponent({
section: {
...section,
isHeaderVisible: false,
},
});
expect(queryByTestId('section-card-header')).not.toBeInTheDocument();
});
it('hides add new, duplicate & delete option based on childAddable, duplicable & deletable action flag', async () => {
const { findByTestId, queryByTestId } = renderComponent({
section: {
...section,
actions: {
draggable: true,
childAddable: false,
deletable: false,
duplicable: false,
},
},
});
const element = await findByTestId('section-card');
const menu = await within(element).findByTestId('section-card-header__menu-button');
await act(async () => fireEvent.click(menu));
expect(within(element).queryByTestId('section-card-header__menu-duplicate-button')).not.toBeInTheDocument();
expect(within(element).queryByTestId('section-card-header__menu-delete-button')).not.toBeInTheDocument();
expect(queryByTestId('new-subsection-button')).not.toBeInTheDocument();
});
});

View File

@@ -27,7 +27,6 @@ const SubsectionCard = ({
const currentRef = useRef(null);
const intl = useIntl();
const dispatch = useDispatch();
const [isExpanded, setIsExpanded] = useState(false);
const [isFormOpen, openForm, closeForm] = useToggle(false);
const namePrefix = 'subsection';
@@ -40,8 +39,11 @@ const SubsectionCard = ({
visibleToStaffOnly = false,
visibilityState,
staffOnlyMessage,
actions,
isHeaderVisible = true,
} = subsection;
const [isExpanded, setIsExpanded] = useState(!isHeaderVisible);
const subsectionStatus = getItemStatus({
published,
releasedToStudents,
@@ -102,35 +104,40 @@ const SubsectionCard = ({
return (
<div className="subsection-card" data-testid="subsection-card" ref={currentRef}>
<CardHeader
title={displayName}
status={subsectionStatus}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
/>
{isHeaderVisible && (
<CardHeader
title={displayName}
status={subsectionStatus}
hasChanges={hasChanges}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
/>
)}
{isExpanded && (
<div data-testid="subsection-card__units" className="subsection-card__units">
<div data-testid="subsection-card__units" className="item-children subsection-card__units">
{children}
<Button
data-testid="new-unit-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewButtonClick}
>
{intl.formatMessage(messages.newUnitButton)}
</Button>
{actions.childAddable && (
<Button
data-testid="new-unit-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewButtonClick}
>
{intl.formatMessage(messages.newUnitButton)}
</Button>
)}
</div>
)}
</div>
@@ -163,6 +170,13 @@ SubsectionCard.propTypes = {
visibilityState: PropTypes.string.isRequired,
staffOnlyMessage: PropTypes.bool.isRequired,
shouldScroll: PropTypes.bool,
actions: PropTypes.shape({
deletable: PropTypes.bool.isRequired,
draggable: PropTypes.bool.isRequired,
childAddable: PropTypes.bool.isRequired,
duplicable: PropTypes.bool.isRequired,
}).isRequired,
isHeaderVisible: PropTypes.bool,
}).isRequired,
children: PropTypes.node,
onOpenPublishModal: PropTypes.func.isRequired,

View File

@@ -3,7 +3,6 @@
.subsection-card__units {
margin-top: $spacer;
margin-right: -2.75rem;
}
.item-card-header__badge-status {

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import {
act, render, fireEvent, within,
} from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
@@ -34,6 +36,13 @@ const subsection = {
visibilityState: 'visible',
staffOnlyMessage: false,
hasChanges: false,
actions: {
draggable: true,
childAddable: true,
deletable: true,
duplicable: true,
},
isHeaderVisible: true,
};
const onEditSubectionSubmit = jest.fn();
@@ -122,4 +131,34 @@ describe('<SubsectionCard />', () => {
fireEvent.keyDown(editField, { key: 'Enter', keyCode: 13 });
expect(onEditSubectionSubmit).toHaveBeenCalled();
});
it('hides header based on isHeaderVisible flag', async () => {
const { queryByTestId } = renderComponent({
subsection: {
...subsection,
isHeaderVisible: false,
},
});
expect(queryByTestId('subsection-card-header')).not.toBeInTheDocument();
});
it('hides add new, duplicate & delete option based on childAddable, duplicable & deletable action flag', async () => {
const { findByTestId, queryByTestId } = renderComponent({
subsection: {
...subsection,
actions: {
draggable: true,
childAddable: false,
deletable: false,
duplicable: false,
},
},
});
const element = await findByTestId('subsection-card');
const menu = await within(element).findByTestId('subsection-card-header__menu-button');
await act(async () => fireEvent.click(menu));
expect(within(element).queryByTestId('subsection-card-header__menu-duplicate-button')).not.toBeInTheDocument();
expect(within(element).queryByTestId('subsection-card-header__menu-delete-button')).not.toBeInTheDocument();
expect(queryByTestId('new-unit-button')).not.toBeInTheDocument();
});
});

View File

@@ -35,6 +35,8 @@ const UnitCard = ({
visibleToStaffOnly = false,
visibilityState,
staffOnlyMessage,
actions,
isHeaderVisible = true,
} = unit;
const unitStatus = getItemStatus({
@@ -88,6 +90,11 @@ const UnitCard = ({
}
}, [savingStatus]);
if (!isHeaderVisible) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
return (
<div className="unit-card" data-testid="unit-card" ref={currentRef}>
<CardHeader
@@ -105,6 +112,7 @@ const UnitCard = ({
onClickDuplicate={onDuplicateSubmit}
titleComponent={titleComponent}
namePrefix={namePrefix}
actions={actions}
/>
</div>
);
@@ -121,6 +129,13 @@ UnitCard.propTypes = {
visibilityState: PropTypes.string.isRequired,
staffOnlyMessage: PropTypes.bool.isRequired,
shouldScroll: PropTypes.bool,
actions: PropTypes.shape({
deletable: PropTypes.bool.isRequired,
draggable: PropTypes.bool.isRequired,
childAddable: PropTypes.bool.isRequired,
duplicable: PropTypes.bool.isRequired,
}).isRequired,
isHeaderVisible: PropTypes.bool,
}).isRequired,
subsection: PropTypes.shape({
id: PropTypes.string.isRequired,

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import {
act, render, fireEvent, within,
} from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp } from '@edx/frontend-platform';
@@ -45,6 +47,13 @@ const unit = {
visibilityState: 'visible',
staffOnlyMessage: false,
hasChanges: false,
actions: {
draggable: true,
childAddable: true,
deletable: true,
duplicable: true,
},
isHeaderVisible: true,
};
const renderComponent = (props) => render(
@@ -87,4 +96,33 @@ describe('<UnitCard />', () => {
expect(await findByTestId('unit-card-header')).toBeInTheDocument();
expect(await findByTestId('unit-card-header__title-link')).toHaveAttribute('href', '/some/123');
});
it('hides header based on isHeaderVisible flag', async () => {
const { queryByTestId } = renderComponent({
unit: {
...unit,
isHeaderVisible: false,
},
});
expect(queryByTestId('unit-card-header')).not.toBeInTheDocument();
});
it('hides duplicate & delete option based on duplicable & deletable action flag', async () => {
const { findByTestId } = renderComponent({
unit: {
...unit,
actions: {
draggable: true,
childAddable: false,
deletable: false,
duplicable: false,
},
},
});
const element = await findByTestId('unit-card');
const menu = await within(element).findByTestId('unit-card-header__menu-button');
await act(async () => fireEvent.click(menu));
expect(within(element).queryByTestId('unit-card-header__menu-duplicate-button')).not.toBeInTheDocument();
expect(within(element).queryByTestId('unit-card-header__menu-delete-button')).not.toBeInTheDocument();
});
});