feat: add drag n drop functionality to section cards

feat: use react-dnd library for drag and drop implementation

style: fix linting issues

fix: finalize section order on drop instead of hover

fix: prevent same index drag to start request and restore state on error

fix: restore sectionlist order

fix: prevent drag event while editing the text

style: fix linting issues

test: fix failing tests

test: add missing hooks to SectionCard component in test

test: add wrapping to SectionCard test component

test: add tests for checking the API that sets the ordering

fix: merge scroll-to-bottom with drag and drop implementations

fix: fix linting issues
This commit is contained in:
Stephannie Jimenez
2023-12-04 18:45:47 -05:00
committed by Kristin Aoki
parent f79bebceeb
commit 48ab324100
11 changed files with 418 additions and 103 deletions

78
package-lock.json generated
View File

@@ -29,12 +29,15 @@
"email-validator": "2.0.4",
"file-saver": "^2.0.5",
"formik": "2.2.6",
"immutability-helper": "^3.1.1",
"jszip": "^3.10.1",
"lodash": "4.17.21",
"moment": "2.29.4",
"prop-types": "15.7.2",
"react": "17.0.2",
"react-datepicker": "^4.13.0",
"react-dnd": "14.0.5",
"react-dnd-html5-backend": "14.1.0",
"react-dom": "17.0.2",
"react-helmet": "^6.1.0",
"react-redux": "7.2.9",
@@ -4999,6 +5002,21 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-dnd/asap": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz",
"integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg=="
},
"node_modules/@react-dnd/invariant": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz",
"integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw=="
},
"node_modules/@react-dnd/shallowequal": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg=="
},
"node_modules/@reduxjs/toolkit": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz",
@@ -11480,6 +11498,24 @@
"dev": true,
"license": "MIT"
},
"node_modules/dnd-core": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz",
"integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==",
"dependencies": {
"@react-dnd/asap": "^4.0.0",
"@react-dnd/invariant": "^2.0.0",
"redux": "^4.1.1"
}
},
"node_modules/dnd-core/node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/dns-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
@@ -16255,6 +16291,11 @@
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutability-helper": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz",
"integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ=="
},
"node_modules/immutable": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz",
@@ -23357,6 +23398,43 @@
"node": ">= 8"
}
},
"node_modules/react-dnd": {
"version": "14.0.5",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz",
"integrity": "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==",
"dependencies": {
"@react-dnd/invariant": "^2.0.0",
"@react-dnd/shallowequal": "^2.0.0",
"dnd-core": "14.0.1",
"fast-deep-equal": "^3.1.3",
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
"@types/hoist-non-react-statics": ">= 3.3.1",
"@types/node": ">= 12",
"@types/react": ">= 16",
"react": ">= 16.14"
},
"peerDependenciesMeta": {
"@types/hoist-non-react-statics": {
"optional": true
},
"@types/node": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dnd-html5-backend": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz",
"integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==",
"dependencies": {
"dnd-core": "14.0.1"
}
},
"node_modules/react-dom": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",

View File

@@ -56,12 +56,15 @@
"email-validator": "2.0.4",
"file-saver": "^2.0.5",
"formik": "2.2.6",
"immutability-helper": "^3.1.1",
"jszip": "^3.10.1",
"lodash": "4.17.21",
"moment": "2.29.4",
"prop-types": "15.7.2",
"react": "17.0.2",
"react-datepicker": "^4.13.0",
"react-dnd": "14.0.5",
"react-dnd-html5-backend": "14.1.0",
"react-dom": "17.0.2",
"react-helmet": "^6.1.0",
"react-redux": "7.2.9",

View File

@@ -1,4 +1,9 @@
import { React, useEffect, useRef } from 'react';
import {
React, useState, useCallback, useEffect, useRef,
} from 'react';
import update from 'immutability-helper';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
@@ -80,17 +85,48 @@ const CourseOutline = ({ courseId }) => {
handleDuplicateSubsectionSubmit,
handleNewSectionSubmit,
handleNewSubsectionSubmit,
handleDragNDrop,
} = useCourseOutline({ courseId });
useEffect(() => {
scrollToElement(listRef);
}, [sectionsList]);
document.title = getPageHeadTitle(courseName, intl.formatMessage(messages.headingTitle));
const [sections, setSections] = useState(sectionsList);
const initialSections = [...sectionsList];
const {
isShow: isShowProcessingNotification,
title: processingNotificationTitle,
} = useSelector(getProcessingNotification);
const moveSection = useCallback((dragIndex, hoverIndex) => {
setSections((prevSections) => {
const newList = update(prevSections, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, prevSections[dragIndex]],
],
});
return newList;
});
}, []);
const finalizeSectionOrder = () => {
handleDragNDrop(sections.map((section) => section.id), () => {
setSections(() => initialSections);
});
};
useEffect(() => {
if (sectionsList) {
setSections((prevSections) => {
if (prevSections.length < sectionsList.length) {
scrollToElement(listRef);
}
return sectionsList;
});
}
}, [sectionsList]);
if (isLoading) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
@@ -150,38 +186,45 @@ const CourseOutline = ({ courseId }) => {
openEnableHighlightsModal={openEnableHighlightsModal}
/>
<div className="pt-4">
{sectionsList.length ? (
{sections.length ? (
<>
{sectionsList.map((section) => (
<SectionCard
key={section.id}
section={section}
savingStatus={savingStatus}
onOpenHighlightsModal={handleOpenHighlightsModal}
onOpenPublishModal={openPublishModal}
onOpenConfigureModal={openConfigureModal}
onOpenDeleteModal={openDeleteModal}
onEditSectionSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateSectionSubmit}
isSectionsExpanded={isSectionsExpanded}
onNewSubsectionSubmit={handleNewSubsectionSubmit}
ref={listRef}
>
{section.childInfo.children.map((subsection) => (
<SubsectionCard
key={subsection.id}
section={section}
subsection={subsection}
savingStatus={savingStatus}
onOpenPublishModal={openPublishModal}
onOpenDeleteModal={openDeleteModal}
onEditSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateSubsectionSubmit}
ref={listRef}
/>
))}
</SectionCard>
))}
<DndProvider
backend={HTML5Backend}
>
{sections.map((section, index) => (
<SectionCard
key={section.id}
index={index}
section={section}
savingStatus={savingStatus}
onOpenHighlightsModal={handleOpenHighlightsModal}
onOpenPublishModal={openPublishModal}
onOpenConfigureModal={openConfigureModal}
onOpenDeleteModal={openDeleteModal}
onEditSectionSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateSectionSubmit}
isSectionsExpanded={isSectionsExpanded}
onNewSubsectionSubmit={handleNewSubsectionSubmit}
moveSection={moveSection}
finalizeSectionOrder={finalizeSectionOrder}
ref={listRef}
>
{section.childInfo.children.map((subsection) => (
<SubsectionCard
key={subsection.id}
section={section}
subsection={subsection}
savingStatus={savingStatus}
onOpenPublishModal={openPublishModal}
onOpenDeleteModal={openDeleteModal}
onEditSubmit={handleEditSubmit}
onDuplicateSubmit={handleDuplicateSubsectionSubmit}
ref={listRef}
/>
))}
</SectionCard>
))}
</DndProvider>
<Button
data-testid="new-section-button"
className="mt-4"

View File

@@ -34,6 +34,7 @@ import {
fetchCourseSectionQuery,
publishCourseItemQuery,
updateCourseSectionHighlightsQuery,
setSectionOrderListQuery,
} from './data/thunk';
import initializeStore from '../store';
import {
@@ -488,4 +489,47 @@ describe('<CourseOutline />', () => {
expect(getByRole('button', { name: '5 Section highlights' })).toBeInTheDocument();
});
it('check section order list when set section order query is successful', async () => {
const { getAllByTestId } = render(<RootWrapper />);
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
let { children } = courseOutlineIndexMock.courseStructure.childInfo;
children = children.splice(2, 0, children.splice(0, 1)[0]);
axiosMock
.onPut(getEnableHighlightsEmailsApiUrl(courseBlockId), children)
.reply(200);
await executeThunk(setSectionOrderListQuery(courseBlockId, children, () => {}), store.dispatch);
await waitFor(() => {
expect(getAllByTestId('section-card')).toHaveLength(4);
const newSections = getAllByTestId('section-card');
for (let i; i < children.length; i++) {
expect(children[i].id === newSections[i].id);
}
});
});
it('check section order list when set section order query is unsuccessful', async () => {
const { getAllByTestId } = render(<RootWrapper />);
const courseBlockId = courseOutlineIndexMock.courseStructure.id;
const { children } = courseOutlineIndexMock.courseStructure.childInfo;
const newChildren = children.splice(2, 0, children.splice(0, 1)[0]);
axiosMock
.onPut(getEnableHighlightsEmailsApiUrl(courseBlockId), undefined)
.reply(500);
await executeThunk(setSectionOrderListQuery(courseBlockId, undefined, () => children), store.dispatch);
await waitFor(() => {
expect(getAllByTestId('section-card')).toHaveLength(4);
const newSections = getAllByTestId('section-card');
for (let i; i < children.length; i++) {
expect(children[i].id === newSections[i].id);
expect(newChildren[i].id !== newSections[i].id);
}
});
});
});

View File

@@ -296,3 +296,18 @@ export async function addNewCourseItem(parentLocator, category, displayName) {
return data;
}
/**
* Set order for the list of the sections
* @param {string} courseId
* @param {Array<string>} children list of sections id's
* @returns {Promise<Object>}
*/
export async function setSectionOrderList(courseId, children) {
const { data } = await getAuthenticatedHttpClient()
.put(getEnableHighlightsEmailsApiUrl(courseId), {
children,
});
return data;
}

View File

@@ -76,6 +76,12 @@ const slice = createSlice({
setCurrentItem: (state, { payload }) => {
state.currentItem = payload;
},
reorderSectionList: (state, { payload }) => {
const sectionsList = [...state.sectionsList];
sectionsList.sort((a, b) => payload.indexOf(a.id) - payload.indexOf(b.id));
state.sectionsList = [...sectionsList];
},
setCurrentSection: (state, { payload }) => {
state.currentSection = payload;
},
@@ -162,6 +168,7 @@ export const {
deleteSubsection,
deleteUnit,
duplicateSection,
reorderSectionList,
} = slice.actions;
export const {

View File

@@ -23,6 +23,7 @@ import {
configureCourseSection,
restartIndexingOnCourse,
updateCourseSectionHighlights,
setSectionOrderList,
} from './api';
import {
addSection,
@@ -40,6 +41,7 @@ import {
deleteSubsection,
deleteUnit,
duplicateSection,
reorderSectionList,
} from './slice';
export function fetchCourseOutlineIndexQuery(courseId) {
@@ -375,3 +377,24 @@ export function addNewSubsectionQuery(parentLocator) {
));
};
}
export function setSectionOrderListQuery(courseId, newListId, restoreCallback) {
return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
dispatch(showProcessingNotification(NOTIFICATION_MESSAGES.saving));
try {
await setSectionOrderList(courseId, newListId).then(async (result) => {
if (result) {
dispatch(reorderSectionList(newListId));
dispatch(updateSavingStatus({ status: RequestStatus.SUCCESSFUL }));
dispatch(hideProcessingNotification());
}
});
} catch (error) {
restoreCallback();
dispatch(hideProcessingNotification());
dispatch(updateSavingStatus({ status: RequestStatus.FAILED }));
}
};
}

View File

@@ -36,6 +36,7 @@ import {
publishCourseItemQuery,
updateCourseSectionHighlightsQuery,
configureCourseSectionQuery,
setSectionOrderListQuery,
} from './data/thunk';
const useCourseOutline = ({ courseId }) => {
@@ -152,6 +153,10 @@ const useCourseOutline = ({ courseId }) => {
dispatch(duplicateSubsectionQuery(currentSubsection.id, currentSection.id));
};
const handleDragNDrop = (newListId, restoreCallback) => {
dispatch(setSectionOrderListQuery(courseId, newListId, restoreCallback));
};
useEffect(() => {
dispatch(fetchCourseOutlineIndexQuery(courseId));
dispatch(fetchCourseBestPracticesQuery({ courseId }));
@@ -207,6 +212,7 @@ const useCourseOutline = ({ courseId }) => {
handleDuplicateSubsectionSubmit,
handleNewSectionSubmit,
handleNewSubsectionSubmit,
handleDragNDrop,
};
};

View File

@@ -1,8 +1,7 @@
import {
forwardRef,
useEffect,
useState,
import React, {
forwardRef, useEffect, useState, useRef,
} from 'react';
import { useDrag, useDrop } from 'react-dnd';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -14,9 +13,11 @@ import { RequestStatus } from '../../data/constants';
import CardHeader from '../card-header/CardHeader';
import { getItemStatus } from '../utils';
import messages from './messages';
import ItemTypes from './itemTypes';
const SectionCard = forwardRef(({
section,
index,
children,
onOpenHighlightsModal,
onOpenPublishModal,
@@ -27,6 +28,8 @@ const SectionCard = forwardRef(({
onDuplicateSubmit,
isSectionsExpanded,
onNewSubsectionSubmit,
moveSection,
finalizeSectionOrder,
}, lastItemRef) => {
const intl = useIntl();
const dispatch = useDispatch();
@@ -49,6 +52,73 @@ const SectionCard = forwardRef(({
highlights,
} = section;
const moveRef = useRef(null);
const [{ handlerId }, drop] = useDrop({
accept: ItemTypes.SECTION,
collect(monitor) {
return {
handlerId: monitor.getHandlerId(),
};
},
hover(item, monitor) {
if (!moveRef.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = moveRef.current?.getBoundingClientRect();
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Time to actually perform the action
moveSection(dragIndex, hoverIndex);
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
// eslint-disable-next-line no-param-reassign
item.index = hoverIndex;
},
});
const indexCopy = index;
const [{ isDragging }, drag] = useDrag({
type: ItemTypes.SECTION,
item: () => ({ id, index, startingIndex: indexCopy }),
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
canDrag: () => !isFormOpen,
end: (item) => {
const { startingIndex } = item;
if (index !== startingIndex) {
finalizeSectionOrder();
}
},
});
const opacity = isDragging ? 0 : 1;
drag(drop(moveRef));
const sectionStatus = getItemStatus({
published,
releasedToStudents,
@@ -91,56 +161,65 @@ const SectionCard = forwardRef(({
}, [savingStatus]);
return (
<div className="section-card" data-testid="section-card" ref={lastItemRef}>
<CardHeader
sectionId={id}
title={displayName}
status={sectionStatus}
hasChanges={hasChanges}
isExpanded={isExpanded}
onExpand={handleExpandContent}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickConfigure={onOpenConfigureModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
namePrefix="section"
/>
<div className="section-card__content" data-testid="section-card__content">
<div className="outline-section__status">
<Button
className="section-card__highlights"
data-destid="section-card-highlights-button"
variant="tertiary"
onClick={handleOpenHighlightsModal}
>
<Badge className="highlights-badge">{highlights.length}</Badge>
<p className="m-0 text-black">{messages.sectionHighlightsBadge.defaultMessage}</p>
</Button>
</div>
</div>
{isExpanded && (
<>
<div data-testid="section-card__subsections" className="section-card__subsections">
{children}
<div
className="section-card"
data-testid="section-card"
data-handler-id={handlerId}
style={{ opacity }}
ref={moveRef}
>
<div ref={lastItemRef}>
<CardHeader
sectionId={id}
title={displayName}
status={sectionStatus}
hasChanges={hasChanges}
isExpanded={isExpanded}
onExpand={handleExpandContent}
onClickMenuButton={handleClickMenuButton}
onClickPublish={onOpenPublishModal}
onClickConfigure={onOpenConfigureModal}
onClickEdit={openForm}
onClickDelete={onOpenDeleteModal}
isFormOpen={isFormOpen}
closeForm={closeForm}
onEditSubmit={handleEditSubmit}
isDisabledEditField={savingStatus === RequestStatus.IN_PROGRESS}
onClickDuplicate={onDuplicateSubmit}
namePrefix="section"
className="nodrag"
/>
<div className="section-card__content" data-testid="section-card__content">
<div className="outline-section__status">
<Button
className="section-card__highlights"
data-destid="section-card-highlights-button"
variant="tertiary"
onClick={handleOpenHighlightsModal}
>
<Badge className="highlights-badge">{highlights.length}</Badge>
<p className="m-0 text-black">{messages.sectionHighlightsBadge.defaultMessage}</p>
</Button>
</div>
<Button
data-testid="new-subsection-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewSubsectionSubmit}
>
{intl.formatMessage(messages.newSubsectionButton)}
</Button>
</>
)}
</div>
{isExpanded && (
<>
<div data-testid="section-card__subsections" className="section-card__subsections">
{children}
</div>
<Button
data-testid="new-subsection-button"
className="mt-4"
variant="outline-primary"
iconBefore={IconAdd}
block
onClick={handleNewSubsectionSubmit}
>
{intl.formatMessage(messages.newSubsectionButton)}
</Button>
</>
)}
</div>
</div>
);
});
@@ -161,6 +240,7 @@ SectionCard.propTypes = {
staffOnlyMessage: PropTypes.bool.isRequired,
highlights: PropTypes.arrayOf(PropTypes.string).isRequired,
}).isRequired,
index: PropTypes.number.isRequired,
children: PropTypes.node,
onOpenHighlightsModal: PropTypes.func.isRequired,
onOpenPublishModal: PropTypes.func.isRequired,
@@ -171,6 +251,8 @@ SectionCard.propTypes = {
onDuplicateSubmit: PropTypes.func.isRequired,
isSectionsExpanded: PropTypes.bool.isRequired,
onNewSubsectionSubmit: PropTypes.func.isRequired,
moveSection: PropTypes.func.isRequired,
finalizeSectionOrder: PropTypes.func.isRequired,
};
export default SectionCard;

View File

@@ -1,4 +1,6 @@
import React from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { render, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
@@ -28,21 +30,28 @@ const section = {
const renderComponent = (props) => render(
<AppProvider store={store}>
<IntlProvider locale="en">
<SectionCard
section={section}
onOpenPublishModal={jest.fn()}
onOpenHighlightsModal={jest.fn()}
onOpenDeleteModal={jest.fn()}
onEditClick={jest.fn()}
savingStatus=""
onEditSectionSubmit={jest.fn()}
onDuplicateSubmit={jest.fn()}
isSectionsExpanded
namePrefix="section"
{...props}
>
<span>children</span>
</SectionCard>
<DndProvider backend={HTML5Backend}>
<SectionCard
section={section}
index={0}
onOpenPublishModal={jest.fn()}
onOpenHighlightsModal={jest.fn()}
onOpenDeleteModal={jest.fn()}
onEditClick={jest.fn()}
savingStatus=""
onEditSectionSubmit={jest.fn()}
onDuplicateSubmit={jest.fn()}
isSectionsExpanded
namePrefix="section"
moveSection={jest.fn()}
finalizeSectionOrder={jest.fn()}
connectDragSource={(el) => el}
isDragging
{...props}
>
<span>children</span>
</SectionCard>
</DndProvider>
</IntlProvider>,
</AppProvider>,
);

View File

@@ -0,0 +1,5 @@
const ItemTypes = {
SECTION: 'section',
};
export default ItemTypes;