mirror of
https://github.com/docmost/docmost.git
synced 2026-07-25 09:04:43 +10:00
feat(bases): reorder kanban columns via drag, persisting choiceOrder
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@/features/base/queries/base-row-query";
|
} from "@/features/base/queries/base-row-query";
|
||||||
import { resolveCardDrop } from "@/features/base/hooks/resolve-card-drop";
|
import { resolveCardDrop } from "@/features/base/hooks/resolve-card-drop";
|
||||||
import type { CardDropPayload } from "@/features/base/hooks/use-kanban-card-drag";
|
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 { triggerPostMoveFlash } from "@atlaskit/pragmatic-drag-and-drop-flourish/trigger-post-move-flash";
|
||||||
import * as liveRegion from "@atlaskit/pragmatic-drag-and-drop-live-region";
|
import * as liveRegion from "@atlaskit/pragmatic-drag-and-drop-live-region";
|
||||||
import { KanbanColumn } from "./kanban-column";
|
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) {
|
if (!isGroupable) {
|
||||||
return <KanbanEmptyState base={base} onPick={handlePickProperty} />;
|
return <KanbanEmptyState base={base} onPick={handlePickProperty} />;
|
||||||
}
|
}
|
||||||
@@ -153,6 +182,7 @@ export function BaseKanban({
|
|||||||
onCardClick={onCardClick}
|
onCardClick={onCardClick}
|
||||||
onAddCard={handleAddCard}
|
onAddCard={handleAddCard}
|
||||||
onCardDrop={handleCardDrop}
|
onCardDrop={handleCardDrop}
|
||||||
|
onColumnReorder={handleColumnReorder}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
import { Badge, Text } from "@mantine/core";
|
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";
|
import classes from "@/features/base/styles/kanban.module.css";
|
||||||
|
|
||||||
type KanbanColumnHeaderProps = {
|
type KanbanColumnHeaderProps = {
|
||||||
|
columnKey: string;
|
||||||
name: string;
|
name: string;
|
||||||
color: string | null;
|
color: string | null;
|
||||||
count: number;
|
count: number;
|
||||||
|
onReorderDrop: (payload: ColumnReorderPayload) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function KanbanColumnHeader({
|
export function KanbanColumnHeader({
|
||||||
|
columnKey,
|
||||||
name,
|
name,
|
||||||
color,
|
color,
|
||||||
count,
|
count,
|
||||||
|
onReorderDrop,
|
||||||
}: KanbanColumnHeaderProps) {
|
}: KanbanColumnHeaderProps) {
|
||||||
|
const { ref, isDragging, closestEdge } = useKanbanColumnReorder({
|
||||||
|
columnKey,
|
||||||
|
onDrop: onReorderDrop,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<div className={classes.columnHeader}>
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={classes.columnHeader}
|
||||||
|
data-dragging={isDragging || undefined}
|
||||||
|
>
|
||||||
<div className={classes.columnHeaderLeft}>
|
<div className={classes.columnHeaderLeft}>
|
||||||
{color ? (
|
{color ? (
|
||||||
<Badge color={color} variant="light" size="sm">
|
<Badge color={color} variant="light" size="sm">
|
||||||
@@ -26,6 +43,7 @@ export function KanbanColumnHeader({
|
|||||||
)}
|
)}
|
||||||
<span className={classes.columnCount}>{count}</span>
|
<span className={classes.columnCount}>{count}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{closestEdge && <BaseDropEdgeIndicator edge={closestEdge} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { KanbanCard } from "./kanban-card";
|
|||||||
import { KanbanColumnHeader } from "./kanban-column-header";
|
import { KanbanColumnHeader } from "./kanban-column-header";
|
||||||
import { KanbanAddCardButton } from "./kanban-add-card-button";
|
import { KanbanAddCardButton } from "./kanban-add-card-button";
|
||||||
import type { CardDropPayload } from "@/features/base/hooks/use-kanban-card-drag";
|
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 { useKanbanColumnDrop } from "@/features/base/hooks/use-kanban-column-drop";
|
||||||
import classes from "@/features/base/styles/kanban.module.css";
|
import classes from "@/features/base/styles/kanban.module.css";
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ type KanbanColumnProps = {
|
|||||||
onCardClick: (rowId: string) => void;
|
onCardClick: (rowId: string) => void;
|
||||||
onAddCard: (columnKey: string) => void;
|
onAddCard: (columnKey: string) => void;
|
||||||
onCardDrop: (payload: CardDropPayload) => void;
|
onCardDrop: (payload: CardDropPayload) => void;
|
||||||
|
onColumnReorder: (payload: ColumnReorderPayload) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function KanbanColumn({
|
export function KanbanColumn({
|
||||||
@@ -21,6 +23,7 @@ export function KanbanColumn({
|
|||||||
onCardClick,
|
onCardClick,
|
||||||
onAddCard,
|
onAddCard,
|
||||||
onCardDrop,
|
onCardDrop,
|
||||||
|
onColumnReorder,
|
||||||
}: KanbanColumnProps) {
|
}: KanbanColumnProps) {
|
||||||
const { ref: bodyRef, isOver } = useKanbanColumnDrop({
|
const { ref: bodyRef, isOver } = useKanbanColumnDrop({
|
||||||
columnKey: column.key,
|
columnKey: column.key,
|
||||||
@@ -30,9 +33,11 @@ export function KanbanColumn({
|
|||||||
return (
|
return (
|
||||||
<div className={classes.column} data-column-key={column.key}>
|
<div className={classes.column} data-column-key={column.key}>
|
||||||
<KanbanColumnHeader
|
<KanbanColumnHeader
|
||||||
|
columnKey={column.key}
|
||||||
name={column.name}
|
name={column.name}
|
||||||
color={column.color}
|
color={column.color}
|
||||||
count={column.rows.length}
|
count={column.rows.length}
|
||||||
|
onReorderDrop={onColumnReorder}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref={bodyRef}
|
ref={bodyRef}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
||||||
|
import {
|
||||||
|
draggable,
|
||||||
|
dropTargetForElements,
|
||||||
|
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||||
|
import {
|
||||||
|
attachClosestEdge,
|
||||||
|
extractClosestEdge,
|
||||||
|
type Edge,
|
||||||
|
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
||||||
|
|
||||||
|
export type ColumnReorderPayload = {
|
||||||
|
draggedColumnKey: string;
|
||||||
|
targetColumnKey: string;
|
||||||
|
edge: Edge;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useKanbanColumnReorder({
|
||||||
|
columnKey,
|
||||||
|
onDrop,
|
||||||
|
}: {
|
||||||
|
columnKey: string;
|
||||||
|
onDrop: (payload: ColumnReorderPayload) => void;
|
||||||
|
}) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [closestEdge, setClosestEdge] = useState<Edge | null>(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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user