feat: reorder components in unit page [FC-00083] (#1816)

Reorders components in unit page via drag and drop. This PR also refactors and moves draggable list and sortable item components to appropriate location.

Course authors will be affected by this change.
This commit is contained in:
Navin Karkera
2025-04-16 19:34:28 +00:00
committed by GitHub
parent 4bd2c3b29a
commit 3b2adc2fc1
26 changed files with 262 additions and 52 deletions

View File

@@ -0,0 +1,101 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { createPortal } from 'react-dom';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragOverlay,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
const DraggableList = ({
itemList,
setState,
updateOrder,
children,
renderOverlay,
activeId,
setActiveId,
}) => {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragEnd = useCallback((event) => {
const { active, over } = event;
if (active.id !== over.id) {
let updatedArray;
setState(() => {
const [activeElement] = itemList.filter(item => item.id === active.id);
const [overElement] = itemList.filter(item => item.id === over.id);
const oldIndex = itemList.indexOf(activeElement);
const newIndex = itemList.indexOf(overElement);
updatedArray = arrayMove(itemList, oldIndex, newIndex);
return updatedArray;
});
updateOrder()(updatedArray);
}
setActiveId?.(null);
}, [updateOrder, setActiveId]);
const handleDragStart = useCallback((event) => {
setActiveId?.(event.active.id);
}, [setActiveId]);
return (
<DndContext
sensors={sensors}
modifiers={[restrictToVerticalAxis]}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext
items={itemList}
strategy={verticalListSortingStrategy}
>
{children}
</SortableContext>
{renderOverlay && createPortal(
<DragOverlay>
{renderOverlay(activeId)}
</DragOverlay>,
document.body,
)}
</DndContext>
);
};
DraggableList.defaultProps = {
renderOverlay: undefined,
activeId: null,
setActiveId: () => {},
};
DraggableList.propTypes = {
itemList: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
})).isRequired,
setState: PropTypes.func.isRequired,
updateOrder: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
renderOverlay: PropTypes.func,
activeId: PropTypes.string,
setActiveId: PropTypes.func,
};
export default DraggableList;

View File

@@ -4,19 +4,20 @@ import { intlShape, injectIntl } from '@edx/frontend-platform/i18n';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import {
Col, Icon, Row,
ActionRow, Card, Icon, IconButtonWithTooltip,
} from '@openedx/paragon';
import { DragIndicator } from '@openedx/paragon/icons';
import messages from './messages';
const SortableItem = ({
id,
category,
isDraggable,
isDroppable,
componentStyle,
actions,
actionStyle,
children,
isClickable,
onClick,
disabled,
// injected
intl,
}) => {
@@ -26,74 +27,72 @@ const SortableItem = ({
setNodeRef,
transform,
transition,
isDragging,
setActivatorNodeRef,
isDragging,
} = useSortable({
id,
data: {
category,
},
disabled: {
draggable: !isDraggable,
droppable: !isDroppable,
},
animateLayoutChanges: () => false,
disabled: {
draggable: disabled,
},
});
const style = {
position: 'relative',
zIndex: isDragging ? 200 : undefined,
transform: CSS.Translate.toString(transform),
zIndex: isDragging ? 200 : undefined,
transition,
background: 'white',
padding: '1rem 1.5rem',
marginBottom: '1.5rem',
borderRadius: '0.35rem',
boxShadow: '0 0 .125rem rgba(0, 0, 0, .15), 0 0 .25rem rgba(0, 0, 0, .15)',
...componentStyle,
};
return (
<Row
<div
ref={setNodeRef}
style={style}
className="mx-0"
>
<Col className="extend-margin px-0">
<Card
style={style}
className="mx-0"
isClickable={isClickable}
onClick={onClick}
>
<ActionRow style={actionStyle}>
{actions}
{!disabled && (
<IconButtonWithTooltip
key="drag-to-reorder-icon"
ref={setActivatorNodeRef}
tooltipPlacement="top"
tooltipContent={intl.formatMessage(messages.tooltipContent)}
src={DragIndicator}
iconAs={Icon}
variant="light"
alt={intl.formatMessage(messages.tooltipContent)}
{...attributes}
{...listeners}
/>
)}
</ActionRow>
{children}
</Col>
{isDraggable && (
<button
ref={setActivatorNodeRef}
key="drag-to-reorder-icon"
aria-label={intl.formatMessage(messages.tooltipContent)}
className="btn-icon btn-icon-secondary btn-icon-md"
type="button"
{...attributes}
{...listeners}
>
<span className="btn-icon__icon-container">
<Icon src={DragIndicator} />
</span>
</button>
)}
</Row>
</Card>
</div>
);
};
SortableItem.defaultProps = {
componentStyle: null,
isDroppable: true,
isDraggable: true,
actions: null,
actionStyle: null,
isClickable: false,
onClick: null,
disabled: false,
};
SortableItem.propTypes = {
id: PropTypes.string.isRequired,
category: PropTypes.string.isRequired,
isDroppable: PropTypes.bool,
isDraggable: PropTypes.bool,
children: PropTypes.node.isRequired,
actions: PropTypes.node,
actionStyle: PropTypes.shape({}),
componentStyle: PropTypes.shape({}),
isClickable: PropTypes.bool,
onClick: PropTypes.func,
disabled: PropTypes.bool,
// injected
intl: intlShape.isRequired,
};

View File

@@ -0,0 +1,5 @@
import DraggableList from './DraggableList';
import SortableItem from './SortableItem';
export { SortableItem };
export default DraggableList;

View File

@@ -1,31 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
export const DragContext = React.createContext({ activeId: '', overId: '', children: undefined });
const DragContextProvider = ({ activeId, overId, children }) => {
const contextValue = React.useMemo(() => ({
activeId,
overId,
}), [activeId, overId]);
return (
<DragContext.Provider
value={contextValue}
>
{children}
</DragContext.Provider>
);
};
DragContextProvider.defaultProps = {
activeId: '',
overId: '',
};
DragContextProvider.propTypes = {
activeId: PropTypes.string,
overId: PropTypes.string,
children: PropTypes.node.isRequired,
};
export default DragContextProvider;

View File

@@ -1,362 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
DndContext,
closestCorners,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import DragContextProvider from './DragContextProvider';
import { COURSE_BLOCK_NAMES } from '../../constants';
import {
moveSubsectionOver,
moveUnitOver,
moveSubsection,
moveUnit,
dragHelpers,
} from './utils';
const DraggableList = ({
items,
setSections,
restoreSectionList,
handleSectionDragAndDrop,
handleSubsectionDragAndDrop,
handleUnitDragAndDrop,
children,
}) => {
const prevContainerInfo = React.useRef();
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const [activeId, setActiveId] = React.useState();
const [currentOverId, setCurrentOverId] = React.useState();
const findItemInfo = (id) => {
// search id in sections
const sectionIndex = items.findIndex((section) => section.id === id);
if (sectionIndex !== -1) {
return {
index: sectionIndex,
item: items[sectionIndex],
category: COURSE_BLOCK_NAMES.chapter.id,
parent: 'root',
};
}
// search id in subsections
for (let index = 0; index < items.length; index++) {
const section = items[index];
const subsectionIndex = section.childInfo.children.findIndex((subsection) => subsection.id === id);
if (subsectionIndex !== -1) {
return {
index: subsectionIndex,
item: section.childInfo.children[subsectionIndex],
category: COURSE_BLOCK_NAMES.sequential.id,
parentIndex: index,
parent: section,
};
}
}
// search id in units
for (let index = 0; index < items.length; index++) {
const section = items[index];
for (let subIndex = 0; subIndex < section.childInfo.children.length; subIndex++) {
const subsection = section.childInfo.children[subIndex];
const unitIndex = subsection.childInfo.children.findIndex((unit) => unit.id === id);
if (unitIndex !== -1) {
return {
index: unitIndex,
item: subsection.childInfo.children[unitIndex],
category: COURSE_BLOCK_NAMES.vertical.id,
parentIndex: subIndex,
parent: subsection,
grandParentIndex: index,
grandParent: section,
};
}
}
}
return null;
};
// For reasons unknown, onDragEnd is not being triggered by dnd-kit while
// testing drag over functions. The main functions responsible to move units
// & subsections across parents are already tested as part of move blocks by
// index in CourseOutline.test.jsx, just these functions which determine the
// new index and parent are ignored.
// See https://github.com/openedx/frontend-app-course-authoring/pull/859#discussion_r1519199622
// for more details.
/* istanbul ignore next */
const subsectionDragOver = (active, over, activeInfo, overInfo) => {
if (
activeInfo.parent.id === overInfo.parent.id
|| activeInfo.parent.id === overInfo.item.id
|| (activeInfo.category === overInfo.category && !overInfo.parent.actions.childAddable)
|| (activeInfo.parent.category === overInfo.category && !overInfo.item.actions.childAddable)
) {
return;
}
// Find the new index for the item
let overSectionIndex;
let newIndex;
if (overInfo.category === COURSE_BLOCK_NAMES.chapter.id) {
// We're at the root droppable of a container
newIndex = overInfo.item.childInfo.children.length + 1;
overSectionIndex = overInfo.index;
setCurrentOverId(overInfo.item.id);
} else {
const modifier = dragHelpers.isBelowOverItem(active, over) ? 1 : 0;
newIndex = overInfo.index >= 0 ? overInfo.index + modifier : overInfo.item.childInfo.children.length + 1;
overSectionIndex = overInfo.parentIndex;
setCurrentOverId(overInfo.parent.id);
}
setSections((prev) => {
const [prevCopy] = moveSubsectionOver(
[...prev],
activeInfo.parentIndex,
activeInfo.index,
overSectionIndex,
newIndex,
);
return prevCopy;
});
if (prevContainerInfo.current === null || prevContainerInfo.current === undefined) {
prevContainerInfo.current = activeInfo.parent.id;
}
};
/* istanbul ignore next */
const unitDragOver = (active, over, activeInfo, overInfo) => {
if (
activeInfo.parent.id === overInfo.parent.id
|| activeInfo.parent.id === overInfo.item.id
|| (activeInfo.category === overInfo.category && !overInfo.parent.actions.childAddable)
|| (activeInfo.parent.category === overInfo.category && !overInfo.item.actions.childAddable)
) {
return;
}
let overSubsectionIndex;
let overSectionIndex;
// Find the indexes for the items
let newIndex;
if (overInfo.category === COURSE_BLOCK_NAMES.sequential.id) {
// We're at the root droppable of a container
newIndex = overInfo.item.childInfo.children.length + 1;
overSubsectionIndex = overInfo.index;
overSectionIndex = overInfo.parentIndex;
setCurrentOverId(overInfo.item.id);
} else {
const modifier = dragHelpers.isBelowOverItem(active, over) ? 1 : 0;
newIndex = overInfo.index >= 0 ? overInfo.index + modifier : overInfo.item.childInfo.children.length + 1;
overSubsectionIndex = overInfo.parentIndex;
overSectionIndex = overInfo.grandParentIndex;
setCurrentOverId(overInfo.parent.id);
}
setSections((prev) => {
const [prevCopy] = moveUnitOver(
[...prev],
activeInfo.grandParentIndex,
activeInfo.parentIndex,
activeInfo.index,
overSectionIndex,
overSubsectionIndex,
newIndex,
);
return prevCopy;
});
if (prevContainerInfo.current === null || prevContainerInfo.current === undefined) {
prevContainerInfo.current = activeInfo.grandParent.id;
}
};
/* istanbul ignore next */
const handleDragOver = (event) => {
const { active, over } = event;
if (!active || !over) {
return;
}
const { id } = active;
const { id: overId } = over;
// Find the containers
const activeInfo = findItemInfo(id);
const overInfo = findItemInfo(overId);
if (!activeInfo || !overInfo) {
return;
}
switch (activeInfo.category) {
case COURSE_BLOCK_NAMES.sequential.id:
subsectionDragOver(active, over, activeInfo, overInfo);
break;
case COURSE_BLOCK_NAMES.vertical.id:
unitDragOver(active, over, activeInfo, overInfo);
break;
default:
break;
}
};
const handleDragEnd = (event) => {
const { active, over } = event;
if (!active || !over) {
return;
}
setActiveId(null);
setCurrentOverId(null);
const { id } = active;
const { id: overId } = over;
const activeInfo = findItemInfo(id);
const overInfo = findItemInfo(overId);
if (!activeInfo || !overInfo) {
return;
}
if (
activeInfo.category !== overInfo.category
|| (activeInfo.parent !== 'root' && activeInfo.parentIndex !== overInfo.parentIndex)
) {
return;
}
if (activeInfo.index !== overInfo.index || prevContainerInfo.current) {
switch (activeInfo.category) {
case COURSE_BLOCK_NAMES.chapter.id:
setSections((prev) => {
const result = arrayMove(prev, activeInfo.index, overInfo.index);
handleSectionDragAndDrop(result.map(section => section.id), restoreSectionList);
return result;
});
break;
case COURSE_BLOCK_NAMES.sequential.id:
setSections((prev) => {
const [prevCopy, result] = moveSubsection(
[...prev],
activeInfo.parentIndex,
activeInfo.index,
overInfo.index,
);
handleSubsectionDragAndDrop(
activeInfo.parent.id,
prevContainerInfo.current,
result.map(subsection => subsection.id),
restoreSectionList,
);
return prevCopy;
});
break;
case COURSE_BLOCK_NAMES.vertical.id:
setSections((prev) => {
const [prevCopy, result] = moveUnit(
[...prev],
activeInfo.grandParentIndex,
activeInfo.parentIndex,
activeInfo.index,
overInfo.index,
);
handleUnitDragAndDrop(
activeInfo.grandParent.id,
prevContainerInfo.current,
activeInfo.parent.id,
result.map(unit => unit.id),
restoreSectionList,
);
return prevCopy;
});
break;
default:
break;
}
prevContainerInfo.current = null;
}
};
const handleDragStart = (event) => {
const { active } = event;
const { id } = active;
setActiveId(id);
};
const customClosestCorners = ({
active, droppableContainers, droppableRects, ...args
}) => {
const activeCategory = active.data?.current?.category;
const filteredContainers = droppableContainers.filter(
(container) => {
switch (activeCategory) {
case COURSE_BLOCK_NAMES.chapter.id:
return container.data?.current?.category === activeCategory;
case COURSE_BLOCK_NAMES.sequential.id:
return [activeCategory, COURSE_BLOCK_NAMES.chapter.id].includes(container.data?.current?.category);
case COURSE_BLOCK_NAMES.vertical.id:
return [activeCategory, COURSE_BLOCK_NAMES.sequential.id].includes(container.data?.current?.category);
default:
return true;
}
},
);
return closestCorners({
active, droppableContainers: filteredContainers, droppableRects, ...args,
});
};
return (
<DndContext
modifiers={[restrictToVerticalAxis]}
sensors={sensors}
collisionDetection={customClosestCorners}
onDragOver={handleDragOver}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<DragContextProvider activeId={activeId} overId={currentOverId}>
{children}
</DragContextProvider>
</DndContext>
);
};
DraggableList.propTypes = {
items: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
childInfo: PropTypes.shape({
children: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
childInfo: PropTypes.shape({
children: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
}),
).isRequired,
}).isRequired,
}),
).isRequired,
}).isRequired,
})).isRequired,
setSections: PropTypes.func.isRequired,
restoreSectionList: PropTypes.func.isRequired,
handleSectionDragAndDrop: PropTypes.func.isRequired,
handleSubsectionDragAndDrop: PropTypes.func.isRequired,
handleUnitDragAndDrop: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
};
export default DraggableList;

View File

@@ -1,5 +0,0 @@
.extend-margin {
.item-children {
margin-right: -2.75rem;
}
}

View File

@@ -1,331 +0,0 @@
import { arrayMove } from '@dnd-kit/sortable';
export const dragHelpers = {
copyBlockChildren: (block) => {
// eslint-disable-next-line no-param-reassign
block.childInfo = { ...block.childInfo };
// eslint-disable-next-line no-param-reassign
block.childInfo.children = [...block.childInfo.children];
return block;
},
setBlockChildren: (block, children) => {
// eslint-disable-next-line no-param-reassign
block.childInfo.children = children;
return block;
},
setBlockChild: (block, child, id) => {
// eslint-disable-next-line no-param-reassign
block.childInfo.children[id] = child;
return block;
},
insertChild: (block, child, index) => {
// eslint-disable-next-line no-param-reassign
block.childInfo.children = [
...block.childInfo.children.slice(0, index),
child,
...block.childInfo.children.slice(index, block.childInfo.children.length),
];
return block;
},
isBelowOverItem: (active, over) => over
&& active.rect.current.translated
&& active.rect.current.translated.top
> over.rect.top + over.rect.height,
};
export const moveSubsectionOver = (
prevCopy,
activeSectionIdx,
activeSubsectionIdx,
overSectionIdx,
newIndex,
) => {
let activeSection = dragHelpers.copyBlockChildren({ ...prevCopy[activeSectionIdx] });
let overSection = dragHelpers.copyBlockChildren({ ...prevCopy[overSectionIdx] });
const subsection = activeSection.childInfo.children[activeSubsectionIdx];
overSection = dragHelpers.insertChild(overSection, subsection, newIndex);
activeSection = dragHelpers.setBlockChildren(
activeSection,
activeSection.childInfo.children.filter((item) => item.id !== subsection.id),
);
// eslint-disable-next-line no-param-reassign
prevCopy[activeSectionIdx] = activeSection;
// eslint-disable-next-line no-param-reassign
prevCopy[overSectionIdx] = overSection;
return [prevCopy, overSection.childInfo.children];
};
export const moveUnitOver = (
prevCopy,
activeSectionIdx,
activeSubsectionIdx,
activeUnitIdx,
overSectionIdx,
overSubsectionIdx,
newIndex,
) => {
const activeSection = dragHelpers.copyBlockChildren({ ...prevCopy[activeSectionIdx] });
let activeSubsection = dragHelpers.copyBlockChildren(
{ ...activeSection.childInfo.children[activeSubsectionIdx] },
);
let overSection = { ...prevCopy[overSectionIdx] };
if (overSection.id === activeSection.id) {
overSection = activeSection;
}
overSection = dragHelpers.copyBlockChildren(overSection);
let overSubsection = dragHelpers.copyBlockChildren(
{ ...overSection.childInfo.children[overSubsectionIdx] },
);
const unit = activeSubsection.childInfo.children[activeUnitIdx];
overSubsection = dragHelpers.insertChild(overSubsection, unit, newIndex);
overSection = dragHelpers.setBlockChild(overSection, overSubsection, overSubsectionIdx);
activeSubsection = dragHelpers.setBlockChildren(
activeSubsection,
activeSubsection.childInfo.children.filter((item) => item.id !== unit.id),
);
// eslint-disable-next-line no-param-reassign
prevCopy[activeSectionIdx] = dragHelpers.setBlockChild(activeSection, activeSubsection, activeSubsectionIdx);
// eslint-disable-next-line no-param-reassign
prevCopy[overSectionIdx] = overSection;
return [prevCopy, overSubsection.childInfo.children];
};
export const moveSubsection = (
prevCopy,
sectionIdx,
currentIdx,
newIdx,
) => {
let section = dragHelpers.copyBlockChildren({ ...prevCopy[sectionIdx] });
const result = arrayMove(section.childInfo.children, currentIdx, newIdx);
section = dragHelpers.setBlockChildren(section, result);
// eslint-disable-next-line no-param-reassign
prevCopy[sectionIdx] = section;
return [prevCopy, result];
};
export const moveUnit = (
prevCopy,
sectionIdx,
subsectionIdx,
currentIdx,
newIdx,
) => {
let section = dragHelpers.copyBlockChildren({ ...prevCopy[sectionIdx] });
let subsection = dragHelpers.copyBlockChildren({ ...section.childInfo.children[subsectionIdx] });
const result = arrayMove(subsection.childInfo.children, currentIdx, newIdx);
subsection = dragHelpers.setBlockChildren(subsection, result);
section = dragHelpers.setBlockChild(section, subsection, subsectionIdx);
// eslint-disable-next-line no-param-reassign
prevCopy[sectionIdx] = section;
return [prevCopy, result];
};
/**
* Check if section can be moved by given step.
* Inner function returns false if the new index after moving by given step
* is out of bounds of item length.
* If it is within bounds, returns draggable flag of the item in the new index.
* This helps us avoid moving the item to a position of unmovable item.
* @param {Array} items
* @returns {(id, step) => bool}
*/
export const canMoveSection = (sections) => (id, step) => {
const newId = id + step;
const indexCheck = newId >= 0 && newId < sections.length;
if (!indexCheck) {
return false;
}
const newItem = sections[newId];
return newItem.actions.draggable;
};
export const possibleSubsectionMoves = (sections, sectionIndex, section, subsections) => (index, step) => {
if (!subsections[index]?.actions?.draggable) {
return {};
}
if ((step === -1 && index >= 1) || (step === 1 && subsections.length - index >= 2)) {
// move subsection inside its own parent section
return {
fn: moveSubsection,
args: [
sections,
sectionIndex,
index,
index + step,
],
sectionId: section.id,
};
} if (step === -1 && index === 0 && sectionIndex > 0) {
// move subsection to last position of previous section
if (!sections[sectionIndex + step]?.actions?.childAddable) {
// return if previous section doesn't allow adding subsections
return {};
}
return {
fn: moveSubsectionOver,
args: [
sections,
sectionIndex,
index,
sectionIndex + step,
sections[sectionIndex + step].childInfo.children.length + 1,
],
sectionId: sections[sectionIndex + step].id,
};
} if (step === 1 && index === subsections.length - 1 && sectionIndex < sections.length - 1) {
// move subsection to first position of next section
if (!sections[sectionIndex + step]?.actions?.childAddable) {
// return if next section doesn't allow adding subsections
return {};
}
return {
fn: moveSubsectionOver,
args: [
sections,
sectionIndex,
index,
sectionIndex + step,
0,
],
sectionId: sections[sectionIndex + step].id,
};
}
return {};
};
export const possibleUnitMoves = (
sections,
sectionIndex,
subsectionIndex,
section,
subsection,
units,
) => (index, step) => {
if (!units[index].actions.draggable) {
return {};
}
if ((step === -1 && index >= 1) || (step === 1 && units.length - index >= 2)) {
return {
fn: moveUnit,
args: [
sections,
sectionIndex,
subsectionIndex,
index,
index + step,
],
sectionId: section.id,
subsectionId: subsection.id,
};
} if (step === -1 && index === 0) {
if (subsectionIndex > 0) {
// move unit to last position of previous subsection inside same section.
if (!sections[sectionIndex].childInfo.children[subsectionIndex + step]?.actions?.childAddable) {
// return if previous subsection doesn't allow adding subsections
return {};
}
return {
fn: moveUnitOver,
args: [
sections,
sectionIndex,
subsectionIndex,
index,
sectionIndex,
subsectionIndex + step,
sections[sectionIndex].childInfo.children[subsectionIndex + step].childInfo.children.length + 1,
],
sectionId: section.id,
subsectionId: sections[sectionIndex].childInfo.children[subsectionIndex + step].id,
};
} if (sectionIndex > 0) {
// move unit to last position of previous subsection inside previous section.
const newSectionIndex = sectionIndex + step;
if (sections[newSectionIndex].childInfo.children.length === 0) {
// return if previous section has no subsections.
return {};
}
const newSubsectionIndex = sections[newSectionIndex].childInfo.children.length - 1;
if (!sections[newSectionIndex].childInfo.children[newSubsectionIndex]?.actions?.childAddable) {
// return if previous subsection doesn't allow adding subsections
return {};
}
return {
fn: moveUnitOver,
args: [
sections,
sectionIndex,
subsectionIndex,
index,
newSectionIndex,
newSubsectionIndex,
sections[newSectionIndex].childInfo.children[newSubsectionIndex].childInfo.children.length + 1,
],
sectionId: sections[newSectionIndex].id,
subsectionId: sections[newSectionIndex].childInfo.children[newSubsectionIndex].id,
};
}
} else if (step === 1 && index === units.length - 1) {
if (subsectionIndex < sections[sectionIndex].childInfo.children.length - 1) {
// move unit to first position of next subsection inside same section.
if (!sections[sectionIndex].childInfo.children[subsectionIndex + step]?.actions?.childAddable) {
// return if next subsection doesn't allow adding subsections
return {};
}
return {
fn: moveUnitOver,
args: [
sections,
sectionIndex,
subsectionIndex,
index,
sectionIndex,
subsectionIndex + step,
0,
],
sectionId: section.id,
subsectionId: sections[sectionIndex].childInfo.children[subsectionIndex + step].id,
};
} if (sectionIndex < sections.length - 1) {
// move unit to first position of next subsection inside next section.
const newSectionIndex = sectionIndex + step;
if (sections[newSectionIndex].childInfo.children.length === 0) {
// return if next section has no subsections.
return {};
}
const newSubsectionIndex = 0;
if (!sections[newSectionIndex].childInfo.children[newSubsectionIndex]?.actions?.childAddable) {
// return if next subsection doesn't allow adding subsections
return {};
}
return {
fn: moveUnitOver,
args: [
sections,
sectionIndex,
subsectionIndex,
index,
newSectionIndex,
newSubsectionIndex,
0,
],
sectionId: sections[newSectionIndex].id,
subsectionId: sections[newSectionIndex].childInfo.children[newSubsectionIndex].id,
};
}
}
return {};
};

View File

@@ -11,6 +11,5 @@
@import "./tag-count/TagCount";
@import "./modal-dropzone/ModalDropzone";
@import "./configure-modal/ConfigureModal";
@import "./drag-helper/SortableItem";
@import "./block-type-utils";
@import "./modal-iframe"