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:
committed by
Kristin Aoki
parent
70b4795650
commit
b417cd64a0
@@ -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>
|
||||
|
||||
@@ -8,3 +8,4 @@
|
||||
@import "./highlights-modal/HighlightsModal";
|
||||
@import "./publish-modal/PublishModal";
|
||||
@import "./configure-modal/ConfigureModal";
|
||||
@import "./drag-helper/ConditionalSortableElement";
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -30,6 +30,12 @@ const cardHeaderProps = {
|
||||
onClickDelete: onClickDeleteMock,
|
||||
onClickDuplicate: onClickDuplicateMock,
|
||||
namePrefix: 'section',
|
||||
actions: {
|
||||
draggable: true,
|
||||
childAddable: true,
|
||||
deletable: true,
|
||||
duplicable: true,
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (props) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
.extend-margin {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
.item-children {
|
||||
margin-right: -2.75rem;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -9,7 +9,10 @@ const onCreateNewSectionMock = jest.fn();
|
||||
|
||||
const renderComponent = () => render(
|
||||
<IntlProvider locale="en">
|
||||
<EmptyPlaceholder onCreateNewSection={onCreateNewSectionMock} />
|
||||
<EmptyPlaceholder
|
||||
onCreateNewSection={onCreateNewSectionMock}
|
||||
childAddable
|
||||
/>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
.section-card__subsections {
|
||||
margin-top: $spacer;
|
||||
margin-right: -2.75rem;
|
||||
}
|
||||
|
||||
.section-card-title {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
.subsection-card__units {
|
||||
margin-top: $spacer;
|
||||
margin-right: -2.75rem;
|
||||
}
|
||||
|
||||
.item-card-header__badge-status {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user