diff --git a/apps/client/src/features/base/components/views/kanban/base-kanban.tsx b/apps/client/src/features/base/components/views/kanban/base-kanban.tsx index a36efb131..2460d9c87 100644 --- a/apps/client/src/features/base/components/views/kanban/base-kanban.tsx +++ b/apps/client/src/features/base/components/views/kanban/base-kanban.tsx @@ -14,6 +14,7 @@ import { } from "@/features/base/queries/base-row-query"; import { resolveCardDrop } from "@/features/base/hooks/resolve-card-drop"; import type { CardDropPayload } from "@/features/base/hooks/use-kanban-card-drag"; +import type { ColumnReorderPayload } from "@/features/base/hooks/use-kanban-column-reorder"; import { triggerPostMoveFlash } from "@atlaskit/pragmatic-drag-and-drop-flourish/trigger-post-move-flash"; import * as liveRegion from "@atlaskit/pragmatic-drag-and-drop-live-region"; import { KanbanColumn } from "./kanban-column"; @@ -139,6 +140,34 @@ export function BaseKanban({ }); }; + const handleColumnReorder = useCallback( + (payload: ColumnReorderPayload) => { + if (!effectiveView) return; + const current = columns.map((c) => c.key); + const fromIdx = current.indexOf(payload.draggedColumnKey); + const toIdx = current.indexOf(payload.targetColumnKey); + if (fromIdx === -1 || toIdx === -1) return; + const next = current.slice(); + next.splice(fromIdx, 1); + const insertAt = + payload.edge === "left" + ? toIdx > fromIdx + ? toIdx - 1 + : toIdx + : toIdx > fromIdx + ? toIdx + : toIdx + 1; + next.splice(insertAt, 0, payload.draggedColumnKey); + + updateViewMutation.mutate({ + viewId: effectiveView.id, + pageId: base.id, + config: { ...effectiveView.config, choiceOrder: next }, + }); + }, + [base.id, columns, effectiveView, updateViewMutation], + ); + if (!isGroupable) { return ; } @@ -153,6 +182,7 @@ export function BaseKanban({ onCardClick={onCardClick} onAddCard={handleAddCard} onCardDrop={handleCardDrop} + onColumnReorder={handleColumnReorder} /> ))} diff --git a/apps/client/src/features/base/components/views/kanban/kanban-column-header.tsx b/apps/client/src/features/base/components/views/kanban/kanban-column-header.tsx index cb3d7d35e..863581f87 100644 --- a/apps/client/src/features/base/components/views/kanban/kanban-column-header.tsx +++ b/apps/client/src/features/base/components/views/kanban/kanban-column-header.tsx @@ -1,19 +1,36 @@ import { Badge, Text } from "@mantine/core"; +import { + useKanbanColumnReorder, + type ColumnReorderPayload, +} from "@/features/base/hooks/use-kanban-column-reorder"; +import { BaseDropEdgeIndicator } from "@/features/base/components/grid/base-drop-edge-indicator"; import classes from "@/features/base/styles/kanban.module.css"; type KanbanColumnHeaderProps = { + columnKey: string; name: string; color: string | null; count: number; + onReorderDrop: (payload: ColumnReorderPayload) => void; }; export function KanbanColumnHeader({ + columnKey, name, color, count, + onReorderDrop, }: KanbanColumnHeaderProps) { + const { ref, isDragging, closestEdge } = useKanbanColumnReorder({ + columnKey, + onDrop: onReorderDrop, + }); return ( -
+
{color ? ( @@ -26,6 +43,7 @@ export function KanbanColumnHeader({ )} {count}
+ {closestEdge && }
); } diff --git a/apps/client/src/features/base/components/views/kanban/kanban-column.tsx b/apps/client/src/features/base/components/views/kanban/kanban-column.tsx index 1c8b28da9..52c798282 100644 --- a/apps/client/src/features/base/components/views/kanban/kanban-column.tsx +++ b/apps/client/src/features/base/components/views/kanban/kanban-column.tsx @@ -4,6 +4,7 @@ import { KanbanCard } from "./kanban-card"; import { KanbanColumnHeader } from "./kanban-column-header"; import { KanbanAddCardButton } from "./kanban-add-card-button"; import type { CardDropPayload } from "@/features/base/hooks/use-kanban-card-drag"; +import type { ColumnReorderPayload } from "@/features/base/hooks/use-kanban-column-reorder"; import { useKanbanColumnDrop } from "@/features/base/hooks/use-kanban-column-drop"; import classes from "@/features/base/styles/kanban.module.css"; @@ -13,6 +14,7 @@ type KanbanColumnProps = { onCardClick: (rowId: string) => void; onAddCard: (columnKey: string) => void; onCardDrop: (payload: CardDropPayload) => void; + onColumnReorder: (payload: ColumnReorderPayload) => void; }; export function KanbanColumn({ @@ -21,6 +23,7 @@ export function KanbanColumn({ onCardClick, onAddCard, onCardDrop, + onColumnReorder, }: KanbanColumnProps) { const { ref: bodyRef, isOver } = useKanbanColumnDrop({ columnKey: column.key, @@ -30,9 +33,11 @@ export function KanbanColumn({ return (
void; +}) { + const ref = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [closestEdge, setClosestEdge] = useState(null); + // Keep onDrop fresh without re-registering the effect each render. + const onDropRef = useRef(onDrop); + onDropRef.current = onDrop; + + useEffect(() => { + const el = ref.current; + if (!el) return; + return combine( + draggable({ + element: el, + getInitialData: () => ({ type: "base-kanban-column", columnKey }), + onDragStart: () => setIsDragging(true), + onDrop: () => setIsDragging(false), + }), + dropTargetForElements({ + element: el, + canDrop: ({ source }) => + source.data.type === "base-kanban-column" && + source.data.columnKey !== columnKey, + getData: ({ input, element }) => + attachClosestEdge( + { type: "base-kanban-column-target", columnKey }, + { input, element, allowedEdges: ["left", "right"] }, + ), + onDrag: ({ self }) => setClosestEdge(extractClosestEdge(self.data)), + onDragLeave: () => setClosestEdge(null), + onDrop: ({ source, self }) => { + setClosestEdge(null); + const edge = extractClosestEdge(self.data); + if (!edge || source.data.type !== "base-kanban-column") return; + onDropRef.current({ + draggedColumnKey: source.data.columnKey as string, + targetColumnKey: columnKey, + edge, + }); + }, + }), + ); + }, [columnKey]); + + return { ref, isDragging, closestEdge }; +}