reorder list column
This commit is contained in:
parent
78ce18f4d3
commit
090090b353
8 changed files with 317 additions and 58 deletions
|
|
@ -56,8 +56,8 @@ class TestingConfig(Config):
|
||||||
|
|
||||||
# Conservative connection pool settings for testing
|
# Conservative connection pool settings for testing
|
||||||
SQLALCHEMY_ENGINE_OPTIONS = {
|
SQLALCHEMY_ENGINE_OPTIONS = {
|
||||||
"pool_size": 20, # Only one connection in the pool
|
"pool_size": 1, # Only one connection in the pool
|
||||||
"max_overflow": 10, # No overflow connections allowed
|
"max_overflow": 0, # No overflow connections allowed
|
||||||
"pool_timeout": 30,
|
"pool_timeout": 30,
|
||||||
"pool_recycle": 3600, # Recycle after 1 hour
|
"pool_recycle": 3600, # Recycle after 1 hour
|
||||||
"pool_pre_ping": True, # Verify connections before using
|
"pool_pre_ping": True, # Verify connections before using
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
from flask_pydantic import validate
|
from flask_pydantic import validate
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.decorators import load_board_owned, load_list_owned
|
from app.decorators import load_board_owned, load_list_owned
|
||||||
from app.models import List
|
from app.models import Board, List
|
||||||
from app.schemas import ListCreateRequest
|
from app.schemas import ListCreateRequest
|
||||||
|
from app.services.list_position_service import ListPositionService
|
||||||
|
|
||||||
from . import kanban_bp
|
from . import kanban_bp
|
||||||
|
|
||||||
|
|
@ -37,9 +40,21 @@ def update_list(list_id, lst, body: ListCreateRequest):
|
||||||
lst.name = body.name
|
lst.name = body.name
|
||||||
if request.json.get("closed") is not None:
|
if request.json.get("closed") is not None:
|
||||||
lst.closed = request.json.get("closed")
|
lst.closed = request.json.get("closed")
|
||||||
lst.pos = body.pos
|
|
||||||
|
|
||||||
db.session.commit()
|
# Track if position is changing
|
||||||
|
old_position = lst.pos
|
||||||
|
new_position = body.pos
|
||||||
|
|
||||||
|
if old_position != new_position:
|
||||||
|
# Use ListPositionService to reorder lists
|
||||||
|
ListPositionService.reorder_lists(lst.board_id, list_id, new_position)
|
||||||
|
else:
|
||||||
|
lst.pos = new_position
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Update board activity timestamp
|
||||||
|
board = db.session.get(Board, lst.board_id)
|
||||||
|
board.date_last_activity = datetime.now(UTC)
|
||||||
|
|
||||||
return lst.to_dict(), 200
|
return lst.to_dict(), 200
|
||||||
|
|
||||||
|
|
|
||||||
76
backend/app/services/list_position_service.py
Normal file
76
backend/app/services/list_position_service.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
"""Service for managing list positioning and reordering"""
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models import List
|
||||||
|
|
||||||
|
|
||||||
|
class ListPositionService:
|
||||||
|
"""Service for handling list position management"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reorder_lists(board_id: int, moved_list_id: int, new_position: float) -> None:
|
||||||
|
"""
|
||||||
|
Reorder all lists in a board when one list is moved to a new position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of board containing lists
|
||||||
|
moved_list_id: The ID of list being moved
|
||||||
|
new_position: The new position for moved list
|
||||||
|
"""
|
||||||
|
# Get all lists in board, ordered by their current position
|
||||||
|
all_lists = List.query.filter_by(board_id=board_id).order_by(List.pos).all()
|
||||||
|
|
||||||
|
# Find moved list in board
|
||||||
|
moved_list = None
|
||||||
|
other_lists = []
|
||||||
|
|
||||||
|
for lst in all_lists:
|
||||||
|
if lst.id == moved_list_id:
|
||||||
|
moved_list = lst
|
||||||
|
else:
|
||||||
|
other_lists.append(lst)
|
||||||
|
|
||||||
|
if not moved_list:
|
||||||
|
return # List not found in this board
|
||||||
|
|
||||||
|
# Insert moved list at the new position
|
||||||
|
other_lists.insert(int(new_position), moved_list)
|
||||||
|
|
||||||
|
# Update positions for all lists to ensure unique, sequential positions
|
||||||
|
for index, lst in enumerate(other_lists):
|
||||||
|
lst.pos = float(index)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_next_position(board_id: int) -> float:
|
||||||
|
"""
|
||||||
|
Get next available position in a board.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of board
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The next available position (float)
|
||||||
|
"""
|
||||||
|
last_list = (
|
||||||
|
List.query.filter_by(board_id=board_id).order_by(List.pos.desc()).first()
|
||||||
|
)
|
||||||
|
|
||||||
|
return float(last_list.pos + 1) if last_list else 0.0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ensure_unique_positions(board_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Ensure all lists in a board have unique, sequential positions.
|
||||||
|
Useful for data cleanup.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of board to fix
|
||||||
|
"""
|
||||||
|
lists = List.query.filter_by(board_id=board_id).order_by(List.pos).all()
|
||||||
|
|
||||||
|
for index, lst in enumerate(lists):
|
||||||
|
lst.pos = float(index)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
@ -11,7 +11,7 @@ log = logging.getLogger(__name__)
|
||||||
fake = Faker()
|
fake = Faker()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="function")
|
||||||
def app():
|
def app():
|
||||||
"""Create application for testing with PostgreSQL database (session scope)"""
|
"""Create application for testing with PostgreSQL database (session scope)"""
|
||||||
app = create_app(config_name="test")
|
app = create_app(config_name="test")
|
||||||
|
|
@ -35,6 +35,7 @@ def app():
|
||||||
# Cleanup after all tests
|
# Cleanup after all tests
|
||||||
db.session.remove()
|
db.session.remove()
|
||||||
db.drop_all()
|
db.drop_all()
|
||||||
|
db.engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ class TestListRoutes:
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
def test_update_list_success(self, client, db_session, regular_user, auth_headers):
|
def test_update_list_success(self, client, db_session, regular_user, auth_headers):
|
||||||
"""Test updating a list"""
|
"""Test updating a list name (position stays same for single list)"""
|
||||||
board = Board(name="Test Board", user_id=regular_user.id)
|
board = Board(name="Test Board", user_id=regular_user.id)
|
||||||
db_session.add(board)
|
db_session.add(board)
|
||||||
db_session.flush()
|
db_session.flush()
|
||||||
|
|
@ -80,13 +80,13 @@ class TestListRoutes:
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/api/lists/{lst.id}",
|
f"/api/lists/{lst.id}",
|
||||||
headers=auth_headers,
|
headers=auth_headers,
|
||||||
json={"name": "Updated Name", "pos": 1},
|
json={"name": "Updated Name", "pos": 0},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.get_json()
|
data = response.get_json()
|
||||||
assert data["name"] == "Updated Name"
|
assert data["name"] == "Updated Name"
|
||||||
assert data["pos"] == 1
|
assert data["pos"] == 0
|
||||||
|
|
||||||
def test_update_list_not_found(self, client, db_session, auth_headers):
|
def test_update_list_not_found(self, client, db_session, auth_headers):
|
||||||
"""Test updating a non-existent list"""
|
"""Test updating a non-existent list"""
|
||||||
|
|
@ -149,3 +149,71 @@ class TestListRoutes:
|
||||||
deleted_card = db.session.get(Card, card.id)
|
deleted_card = db.session.get(Card, card.id)
|
||||||
assert deleted_list is None
|
assert deleted_list is None
|
||||||
assert deleted_card is None
|
assert deleted_card is None
|
||||||
|
|
||||||
|
def test_update_list_position_reorders_others(
|
||||||
|
self, client, db_session, regular_user, auth_headers
|
||||||
|
):
|
||||||
|
"""Test updating list position reorders other lists in the board"""
|
||||||
|
board = Board(name="Test Board", user_id=regular_user.id)
|
||||||
|
db_session.add(board)
|
||||||
|
db_session.flush()
|
||||||
|
|
||||||
|
# Create 3 lists in sequential positions
|
||||||
|
list1 = List(name="List 1", board_id=board.id, pos=0)
|
||||||
|
list2 = List(name="List 2", board_id=board.id, pos=1)
|
||||||
|
list3 = List(name="List 3", board_id=board.id, pos=2)
|
||||||
|
db_session.add(list1)
|
||||||
|
db_session.add(list2)
|
||||||
|
db_session.add(list3)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Move list3 from position 2 to position 0 (front)
|
||||||
|
response = client.put(
|
||||||
|
f"/api/lists/{list3.id}",
|
||||||
|
headers=auth_headers,
|
||||||
|
json={"name": "List 3", "pos": 0},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Verify all lists have unique, sequential positions
|
||||||
|
updated_lists = List.query.filter_by(board_id=board.id).order_by(List.pos).all()
|
||||||
|
assert len(updated_lists) == 3
|
||||||
|
assert updated_lists[0].id == list3.id
|
||||||
|
assert updated_lists[0].pos == 0.0
|
||||||
|
assert updated_lists[1].id == list1.id
|
||||||
|
assert updated_lists[1].pos == 1.0
|
||||||
|
assert updated_lists[2].id == list2.id
|
||||||
|
assert updated_lists[2].pos == 2.0
|
||||||
|
|
||||||
|
def test_update_list_position_no_change(
|
||||||
|
self, client, db_session, regular_user, auth_headers
|
||||||
|
):
|
||||||
|
"""Test updating list with same position doesn't reorder others"""
|
||||||
|
board = Board(name="Test Board", user_id=regular_user.id)
|
||||||
|
db_session.add(board)
|
||||||
|
db_session.flush()
|
||||||
|
|
||||||
|
list1 = List(name="List 1", board_id=board.id, pos=0)
|
||||||
|
list2 = List(name="List 2", board_id=board.id, pos=1)
|
||||||
|
db_session.add(list1)
|
||||||
|
db_session.add(list2)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
original_pos1 = list1.pos
|
||||||
|
original_pos2 = list2.pos
|
||||||
|
|
||||||
|
# Update list2 but keep same position
|
||||||
|
response = client.put(
|
||||||
|
f"/api/lists/{list2.id}",
|
||||||
|
headers=auth_headers,
|
||||||
|
json={"name": "Updated List 2", "pos": original_pos2},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Verify positions unchanged
|
||||||
|
updated_list1 = db.session.get(List, list1.id)
|
||||||
|
updated_list2 = db.session.get(List, list2.id)
|
||||||
|
assert updated_list1.pos == original_pos1
|
||||||
|
assert updated_list2.pos == original_pos2
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,17 @@ import Edit2Icon from '../icons/Edit2Icon';
|
||||||
import Trash2Icon from '../icons/Trash2Icon';
|
import Trash2Icon from '../icons/Trash2Icon';
|
||||||
import { useModal } from '../../context/modals/useModal';
|
import { useModal } from '../../context/modals/useModal';
|
||||||
|
|
||||||
interface KanbanColumnProps {
|
export interface KanbanColumnProps {
|
||||||
list: ListWithCards;
|
list: ListWithCards;
|
||||||
cards: CardType[];
|
cards: CardType[];
|
||||||
onOpenCardModal: (card: CardType) => void;
|
onOpenCardModal: (card: CardType) => void;
|
||||||
onCardCreate: (data: { name: string; description?: string }) => Promise<void>;
|
onCardCreate: (data: { name: string; description?: string }) => Promise<void>;
|
||||||
onListEdit?: (name: string) => Promise<void>;
|
onListEdit?: (name: string) => Promise<void>;
|
||||||
onListDelete?: () => Promise<void>;
|
onListDelete?: () => Promise<void>;
|
||||||
|
dragHandleProps?: {
|
||||||
|
attributes: any;
|
||||||
|
listeners: any;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KanbanColumn({
|
export function KanbanColumn({
|
||||||
|
|
@ -25,6 +29,7 @@ export function KanbanColumn({
|
||||||
onCardCreate,
|
onCardCreate,
|
||||||
onListEdit,
|
onListEdit,
|
||||||
onListDelete,
|
onListDelete,
|
||||||
|
dragHandleProps,
|
||||||
}: KanbanColumnProps) {
|
}: KanbanColumnProps) {
|
||||||
const { setNodeRef, isOver } = useDroppable({
|
const { setNodeRef, isOver } = useDroppable({
|
||||||
id: `LIST_${list.id}`,
|
id: `LIST_${list.id}`,
|
||||||
|
|
@ -66,7 +71,31 @@ export function KanbanColumn({
|
||||||
<div className="bg-gray-800 rounded-lg p-4 min-w-[300px] max-w-[300px] border border-gray-700 flex flex-col">
|
<div className="bg-gray-800 rounded-lg p-4 min-w-[300px] max-w-[300px] border border-gray-700 flex flex-col">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<h2 className="text-white font-bold text-lg">{list.name}</h2>
|
<div className="flex items-center gap-2 flex-1">
|
||||||
|
{/* Drag Handle Icon */}
|
||||||
|
<div {...dragHandleProps?.attributes} {...dragHandleProps?.listeners}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-gray-500 cursor-grab hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<circle cx="9" cy="12" r="1"></circle>
|
||||||
|
<circle cx="9" cy="5" r="1"></circle>
|
||||||
|
<circle cx="9" cy="19" r="1"></circle>
|
||||||
|
<circle cx="15" cy="12" r="1"></circle>
|
||||||
|
<circle cx="15" cy="5" r="1"></circle>
|
||||||
|
<circle cx="15" cy="19" r="1"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-white font-bold text-lg">{list.name}</h2>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{onListEdit && (
|
{onListEdit && (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
25
frontend/src/components/kanban/SortableKanbanColumn.tsx
Normal file
25
frontend/src/components/kanban/SortableKanbanColumn.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
|
import { KanbanColumn, KanbanColumnProps } from './KanbanColumn';
|
||||||
|
|
||||||
|
export function SortableKanbanColumn(props: KanbanColumnProps) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
||||||
|
id: `COLUMN_${props.list.id}`,
|
||||||
|
data: {
|
||||||
|
type: 'column',
|
||||||
|
columnId: props.list.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = transform
|
||||||
|
? {
|
||||||
|
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef} style={style}>
|
||||||
|
<KanbanColumn {...props} dragHandleProps={{ attributes, listeners }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ import { useParams, Link } from 'react-router-dom';
|
||||||
import { useBoard } from '../hooks/useBoard';
|
import { useBoard } from '../hooks/useBoard';
|
||||||
import { useCardMutations } from '../hooks/useCardMutations';
|
import { useCardMutations } from '../hooks/useCardMutations';
|
||||||
import { useListMutations } from '../hooks/useListMutations';
|
import { useListMutations } from '../hooks/useListMutations';
|
||||||
import { KanbanColumn } from '../components/kanban/KanbanColumn';
|
import { SortableKanbanColumn } from '../components/kanban/SortableKanbanColumn';
|
||||||
import { CreateListModal } from '../components/kanban/CreateListModal';
|
import { CreateListModal } from '../components/kanban/CreateListModal';
|
||||||
import { CardPreviewModal } from '../components/CardPreviewModal';
|
import { CardPreviewModal } from '../components/CardPreviewModal';
|
||||||
import { useModal } from '../context/modals/useModal';
|
import { useModal } from '../context/modals/useModal';
|
||||||
|
|
@ -17,7 +17,8 @@ import {
|
||||||
useSensors,
|
useSensors,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { Card as CardType } from '../types/kanban';
|
import { SortableContext, horizontalListSortingStrategy } from '@dnd-kit/sortable';
|
||||||
|
import { Card as CardType, ListWithCards } from '../types/kanban';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export function BoardDetail() {
|
export function BoardDetail() {
|
||||||
|
|
@ -28,6 +29,7 @@ export function BoardDetail() {
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
|
|
||||||
const [activeCard, setActiveCard] = useState<CardType | null>(null);
|
const [activeCard, setActiveCard] = useState<CardType | null>(null);
|
||||||
|
const [activeList, setActiveList] = useState<ListWithCards | null>(null);
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
|
|
@ -39,18 +41,23 @@ export function BoardDetail() {
|
||||||
|
|
||||||
const handleDragStart = (event: DragStartEvent) => {
|
const handleDragStart = (event: DragStartEvent) => {
|
||||||
const { active } = event;
|
const { active } = event;
|
||||||
|
const [activeType, activeIdStr] = (active.id as string).split('_');
|
||||||
|
|
||||||
const activeIdStr = (active.id as string).split('_')[1];
|
if (activeType === 'COLUMN') {
|
||||||
|
// Dragging a column
|
||||||
const cardId = parseInt(activeIdStr as string);
|
const listId = parseInt(activeIdStr);
|
||||||
|
const list = board?.lists.find((l) => l.id === listId);
|
||||||
if (board) {
|
if (list) {
|
||||||
const card = board.lists.flatMap((list) => list.cards).find((c) => c.id === cardId);
|
setActiveList(list);
|
||||||
|
}
|
||||||
// console.log('---handleDragStart', event, card)
|
} else if (activeType === 'CARD') {
|
||||||
// console.log('---handleDragStart.board', board)
|
// Dragging a card
|
||||||
if (card) {
|
const cardId = parseInt(activeIdStr);
|
||||||
setActiveCard(card);
|
if (board) {
|
||||||
|
const card = board.lists.flatMap((list) => list.cards).find((c) => c.id === cardId);
|
||||||
|
if (card) {
|
||||||
|
setActiveCard(card);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -85,19 +92,60 @@ export function BoardDetail() {
|
||||||
const handleDragEnd = async (event: DragEndEvent) => {
|
const handleDragEnd = async (event: DragEndEvent) => {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
setActiveCard(null);
|
setActiveCard(null);
|
||||||
|
setActiveList(null);
|
||||||
|
|
||||||
if (!over || !board) return;
|
if (!over || !board) return;
|
||||||
|
|
||||||
// console.log('--------------over', over)
|
const [activeType, activeIdStr] = (active.id as string).split('_');
|
||||||
// console.log('--------------board', board)
|
|
||||||
const [overType, overIdStr] = (over.id as string).split('_');
|
const [overType, overIdStr] = (over.id as string).split('_');
|
||||||
const overId = parseInt(overIdStr, 10);
|
|
||||||
const activeIdStr = (active.id as string).split('_')[1];
|
|
||||||
const activeId = parseInt(activeIdStr, 10);
|
const activeId = parseInt(activeIdStr, 10);
|
||||||
// debugger
|
const overId = parseInt(overIdStr, 10);
|
||||||
|
|
||||||
if (active.id === over.id) return;
|
if (active.id === over.id) return;
|
||||||
|
|
||||||
|
// Handle column reordering
|
||||||
|
if (activeType === 'COLUMN') {
|
||||||
|
// todo find over column id,
|
||||||
|
|
||||||
|
let overListIndex = -1;
|
||||||
|
const activeList = board.lists.find((l) => l.id === activeId);
|
||||||
|
if (overType === 'CARD') {
|
||||||
|
overListIndex = board.lists.findIndex((l) => {
|
||||||
|
const foundIndex = l.cards.findIndex((card) => card.id === overId);
|
||||||
|
return foundIndex >= 0;
|
||||||
|
});
|
||||||
|
} else if (overType === 'LIST') {
|
||||||
|
overListIndex = board.lists.findIndex((l) => l.id === overId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('-------active.id', active.id)
|
||||||
|
// console.log('-------overType.id', overType)
|
||||||
|
// console.log('-------overListIndex', overListIndex)
|
||||||
|
|
||||||
|
const activeListIndex = board.lists.findIndex((l) => l.id === activeId);
|
||||||
|
// overListIndex = board.lists.findIndex((l) => l.id === overId);
|
||||||
|
|
||||||
|
if (activeListIndex === -1 || overListIndex === -1 || !activeList) return;
|
||||||
|
|
||||||
|
// Calculate new positions for all lists
|
||||||
|
const reorderedLists = [...board.lists];
|
||||||
|
const [movedList] = reorderedLists.splice(activeListIndex, 1);
|
||||||
|
reorderedLists.splice(overListIndex, 0, movedList);
|
||||||
|
|
||||||
|
await updateList(activeList.id, { name: activeList.name, pos: overListIndex });
|
||||||
|
|
||||||
|
// // Update all list positions
|
||||||
|
// for (let i = 0; i < reorderedLists.length; i++) {
|
||||||
|
// const list = reorderedLists[i];
|
||||||
|
// if (list.pos !== i) {
|
||||||
|
// await updateList(list.id, { name: list.name, pos: i });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle card reordering (existing logic)
|
||||||
// Find active card
|
// Find active card
|
||||||
let activeCard: CardType | undefined;
|
let activeCard: CardType | undefined;
|
||||||
let activeList: (typeof board.lists)[0] | undefined;
|
let activeList: (typeof board.lists)[0] | undefined;
|
||||||
|
|
@ -114,8 +162,7 @@ export function BoardDetail() {
|
||||||
if (!activeCard || !activeList) return;
|
if (!activeCard || !activeList) return;
|
||||||
|
|
||||||
// Check if we're dropping on a list or a card
|
// Check if we're dropping on a list or a card
|
||||||
// debugger
|
if (overType.toLowerCase() === 'list') {
|
||||||
if (overType.toLocaleLowerCase() === 'list') {
|
|
||||||
const overList = board.lists.find((list) => list.id === overId);
|
const overList = board.lists.find((list) => list.id === overId);
|
||||||
// Dropping on a list - append to end
|
// Dropping on a list - append to end
|
||||||
|
|
||||||
|
|
@ -143,20 +190,6 @@ export function BoardDetail() {
|
||||||
// Calculate new position
|
// Calculate new position
|
||||||
const overCardIndex = overListContainingCard.cards.findIndex((c) => c.id === overId);
|
const overCardIndex = overListContainingCard.cards.findIndex((c) => c.id === overId);
|
||||||
|
|
||||||
// // If dropping on to same list and after of same card, do nothing
|
|
||||||
// if (
|
|
||||||
// overListContainingCard.id === activeList.id &&
|
|
||||||
// overCardIndex === activeList.cards.findIndex((c) => c.id === activeId) + 1
|
|
||||||
// ) {
|
|
||||||
|
|
||||||
// console.log('--------------over', over)
|
|
||||||
// console.log('--------------board', board)
|
|
||||||
// console.log('--------------activeCard', activeCard)
|
|
||||||
// console.log('--------------overListContainingCard', overListContainingCard)
|
|
||||||
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
await moveCard(activeCard, activeList.id, overListContainingCard.id, overCardIndex);
|
await moveCard(activeCard, activeList.id, overListContainingCard.id, overCardIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -234,25 +267,37 @@ export function BoardDetail() {
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<div className="flex gap-4 overflow-x-auto pb-4">
|
<SortableContext
|
||||||
{board.lists.map((list) => (
|
items={board.lists.map((list) => `COLUMN_${list.id}`)}
|
||||||
<KanbanColumn
|
strategy={horizontalListSortingStrategy}
|
||||||
key={list.id}
|
>
|
||||||
list={list}
|
<div className="flex gap-4 overflow-x-auto pb-4">
|
||||||
cards={list.cards}
|
{board.lists.map((list) => (
|
||||||
onOpenCardModal={handleOpenCardModal}
|
<SortableKanbanColumn
|
||||||
onCardCreate={handleAddCard(list.id)}
|
key={list.id}
|
||||||
onListEdit={(name) => handleEditList(list.id, name)}
|
list={list}
|
||||||
onListDelete={() => handleDeleteList(list.id)}
|
cards={list.cards}
|
||||||
/>
|
onOpenCardModal={handleOpenCardModal}
|
||||||
))}
|
onCardCreate={handleAddCard(list.id)}
|
||||||
</div>
|
onListEdit={(name) => handleEditList(list.id, name)}
|
||||||
|
onListDelete={() => handleDeleteList(list.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
|
||||||
<DragOverlay>
|
<DragOverlay>
|
||||||
{activeCard ? (
|
{activeCard ? (
|
||||||
<div className="bg-gray-700 rounded-lg p-4 cursor-grabbing border border-gray-600 shadow-lg opacity-80">
|
<div className="bg-gray-700 rounded-lg p-4 cursor-grabbing border border-gray-600 shadow-lg opacity-80">
|
||||||
<h3 className="text-white font-medium">{activeCard.name}</h3>
|
<h3 className="text-white font-medium">{activeCard.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
) : activeList ? (
|
||||||
|
<div className="bg-gray-800 rounded-lg p-4 cursor-grabbing border border-gray-700 shadow-lg opacity-80 min-w-[300px] max-w-[300px]">
|
||||||
|
<h2 className="text-white font-bold text-lg">{activeList.name}</h2>
|
||||||
|
<span className="bg-gray-600 text-gray-300 text-xs px-2 py-1 rounded-full inline-block mt-2">
|
||||||
|
{activeList.cards.length} cards
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue