Merge pull request #1649 from symonbaikov/feat/list-view-reorder

feat: allow re-ordering cards in list view

Conflicted only on the import block, where the icon button tooltips had added
a line. Both imports stay.
This commit is contained in:
Daniel Hiller
2026-09-17 02:03:25 +02:00
2 changed files with 111 additions and 11 deletions
@@ -46,6 +46,7 @@ const FiniteContent = React.memo(() => {
return ( return (
<View <View
cardIds={cardIds} cardIds={cardIds}
isReorderingEnabled={board.view === BoardViews.LIST}
onCardCreate={canAddCard ? handleCardCreate : undefined} onCardCreate={canAddCard ? handleCardCreate : undefined}
onCardPaste={canAddCard ? handleCardPaste : undefined} onCardPaste={canAddCard ? handleCardPaste : undefined}
/> />
+110 -11
View File
@@ -6,22 +6,39 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { shallowEqual, useSelector } from 'react-redux'; import { shallowEqual, useDispatch, useSelector, useStore } from 'react-redux';
import { useInView } from 'react-intersection-observer'; import { useInView } from 'react-intersection-observer';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Icon, Loader } from 'semantic-ui-react'; import { Button, Icon, Loader } from 'semantic-ui-react';
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { Tooltip } from '../../../lib/custom-ui'; import { Tooltip } from '../../../lib/custom-ui';
import selectors from '../../../selectors'; import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { BoardMembershipRoles } from '../../../constants/Enums'; import { BoardMembershipRoles } from '../../../constants/Enums';
import DroppableTypes from '../../../constants/DroppableTypes';
import parseDndId from '../../../utils/parse-dnd-id';
import { closePopup } from '../../../lib/popup';
import globalStyles from '../../../styles.module.scss';
import Card from '../../cards/Card'; import Card from '../../cards/Card';
import DraggableCard from '../../cards/DraggableCard';
import AddCard from '../../cards/AddCard'; import AddCard from '../../cards/AddCard';
import PlusMathIcon from '../../../assets/images/plus-math-icon.svg?react'; import PlusMathIcon from '../../../assets/images/plus-math-icon.svg?react';
import styles from './ListView.module.scss'; import styles from './ListView.module.scss';
const ListView = React.memo( const ListView = React.memo(
({ cardIds, isCardsFetching, isAllCardsFetched, onCardsFetch, onCardCreate, onCardPaste }) => { ({
cardIds,
isCardsFetching,
isAllCardsFetched,
isReorderingEnabled,
onCardsFetch,
onCardCreate,
onCardPaste,
}) => {
const store = useStore();
const dispatch = useDispatch();
const clipboard = useSelector(selectors.selectClipboard); const clipboard = useSelector(selectors.selectClipboard);
const { canAddCard, canPasteCard } = useSelector((state) => { const { canAddCard, canPasteCard } = useSelector((state) => {
@@ -54,6 +71,94 @@ const ListView = React.memo(
setIsAddCardOpened(false); setIsAddCardOpened(false);
}, []); }, []);
const handleDragStart = useCallback(() => {
document.body.classList.add(globalStyles.dragging);
closePopup();
}, []);
const handleDragEnd = useCallback(
({ draggableId, type, source, destination }) => {
document.body.classList.remove(globalStyles.dragging);
if (!destination || type !== DroppableTypes.CARD) {
return;
}
if (source.index === destination.index) {
return;
}
const cardId = parseDndId(draggableId);
const state = store.getState();
const getListId = (id) => {
const card = id ? selectors.selectCardById(state, id) : null;
return card ? card.listId : null;
};
const newOrder = cardIds.filter((id) => id !== cardId);
const prevId = destination.index > 0 ? newOrder[destination.index - 1] : null;
const nextId = destination.index < newOrder.length ? newOrder[destination.index] : null;
const targetListId = getListId(prevId) || getListId(nextId);
if (!targetListId) {
return;
}
let localIndex = 0;
for (let i = 0; i < destination.index; i += 1) {
if (getListId(newOrder[i]) === targetListId) {
localIndex += 1;
}
}
dispatch(entryActions.moveCard(cardId, targetListId, localIndex));
},
[cardIds, dispatch, store],
);
const renderCardsContent = () => {
if (cardIds.length === 0) {
return null;
}
if (!isReorderingEnabled) {
return (
<div className={classNames(styles.segment, styles.cards)}>
{cardIds.map((cardId, cardIndex) => (
<div key={cardId} className={styles.card}>
<Card isInline id={cardId} index={cardIndex} />
</div>
))}
</div>
);
}
return (
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<Droppable droppableId="list-view" type={DroppableTypes.CARD}>
{({ innerRef, droppableProps, placeholder }) => (
<div
{...droppableProps} // eslint-disable-line react/jsx-props-no-spreading
ref={innerRef}
className={classNames(styles.segment, styles.cards)}
>
{cardIds.map((cardId, cardIndex) => (
<DraggableCard
key={cardId}
isInline
id={cardId}
index={cardIndex}
className={styles.card}
/>
))}
{placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
{canAddCard && {canAddCard &&
@@ -88,15 +193,7 @@ const ListView = React.memo(
)} )}
</div> </div>
))} ))}
{cardIds.length > 0 && ( {renderCardsContent()}
<div className={classNames(styles.segment, styles.cards)}>
{cardIds.map((cardId, cardIndex) => (
<div key={cardId} className={styles.card}>
<Card isInline id={cardId} index={cardIndex} />
</div>
))}
</div>
)}
{isCardsFetching !== undefined && isAllCardsFetched !== undefined && ( {isCardsFetching !== undefined && isAllCardsFetched !== undefined && (
<div className={styles.loaderWrapper}> <div className={styles.loaderWrapper}>
{isCardsFetching ? ( {isCardsFetching ? (
@@ -115,6 +212,7 @@ ListView.propTypes = {
cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types cardIds: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
isCardsFetching: PropTypes.bool, isCardsFetching: PropTypes.bool,
isAllCardsFetched: PropTypes.bool, isAllCardsFetched: PropTypes.bool,
isReorderingEnabled: PropTypes.bool,
onCardsFetch: PropTypes.func, onCardsFetch: PropTypes.func,
onCardCreate: PropTypes.func, onCardCreate: PropTypes.func,
onCardPaste: PropTypes.func, onCardPaste: PropTypes.func,
@@ -123,6 +221,7 @@ ListView.propTypes = {
ListView.defaultProps = { ListView.defaultProps = {
isCardsFetching: undefined, isCardsFetching: undefined,
isAllCardsFetched: undefined, isAllCardsFetched: undefined,
isReorderingEnabled: false,
onCardsFetch: undefined, onCardsFetch: undefined,
onCardCreate: undefined, onCardCreate: undefined,
onCardPaste: undefined, onCardPaste: undefined,