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

@@ -0,0 +1,100 @@
import React from 'react';
import PropTypes from 'prop-types';
import { intlShape, injectIntl } from '@edx/frontend-platform/i18n';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import {
ActionRow, Card, Icon, IconButtonWithTooltip,
} from '@openedx/paragon';
import { DragIndicator } from '@openedx/paragon/icons';
import messages from './messages';
const SortableItem = ({
id,
componentStyle,
actions,
actionStyle,
children,
isClickable,
onClick,
disabled,
// injected
intl,
}) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
setActivatorNodeRef,
isDragging,
} = useSortable({
id,
animateLayoutChanges: () => false,
disabled: {
draggable: disabled,
},
});
const style = {
transform: CSS.Translate.toString(transform),
zIndex: isDragging ? 200 : undefined,
transition,
...componentStyle,
};
return (
<div
ref={setNodeRef}
>
<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}
</Card>
</div>
);
};
SortableItem.defaultProps = {
componentStyle: null,
actions: null,
actionStyle: null,
isClickable: false,
onClick: null,
disabled: false,
};
SortableItem.propTypes = {
id: PropTypes.string.isRequired,
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,
};
export default injectIntl(SortableItem);

View File

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

View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
tooltipContent: {
id: 'authoring.draggableList.tooltip.content',
defaultMessage: 'Drag to reorder',
description: 'Tooltip content for drag indicator icon',
},
});
export default messages;