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 };
+}