feat: raw statuses dnd

This commit is contained in:
2025-08-01 17:50:27 +04:00
parent 943b2d63f5
commit 586af488da
3 changed files with 175 additions and 33 deletions

View File

@ -9,18 +9,25 @@ import {
DragOverlay,
DragStartEvent,
KeyboardSensor,
Over,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
import {
horizontalListSortingStrategy,
SortableContext,
sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import { LexoRank } from "lexorank";
import { throttle } from "lodash";
import { Group, ScrollArea } from "@mantine/core";
import DealCard from "@/app/deals/components/DealCard/DealCard";
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
import { SortableItem } from "@/components/SortableDnd/SortableItem";
import { DealSchema } from "@/types/DealSchema";
import { StatusSchema } from "@/types/StatusSchema";
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
type Props = {
@ -29,11 +36,15 @@ type Props = {
statusId: number,
lexorank?: string
) => void;
onStatusDragEnd?: (statusId: number, newRank: string) => void;
};
const StatusColumnsDnd: FC<Props> = props => {
const { statuses, deals, setDeals } = useStatusesContext();
const { statuses, deals, setDeals, setStatuses } = useStatusesContext();
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
const throttledSetDeals = useMemo(
() => throttle(setDeals, 200),
[setDeals]
@ -47,9 +58,18 @@ const StatusColumnsDnd: FC<Props> = props => {
);
const handleDragStart = ({ active }: DragStartEvent) => {
setActiveDeal(
deals.find(deal => deal.id === (active.id as number)) ?? null
);
const activeId = active.id as string | number;
if (typeof activeId === "string" && activeId.endsWith("-status")) {
const statusId = Number(activeId.replace("-status", ""));
setActiveStatus(
statuses.find(status => status.id === statusId) ?? null
);
} else {
setActiveDeal(
deals.find(deal => deal.id === (activeId as number)) ?? null
);
}
};
const getStatusByDealId = (dealId: number) => {
@ -59,9 +79,18 @@ const StatusColumnsDnd: FC<Props> = props => {
};
const handleDragOver = ({ active, over }: DragOverEvent) => {
if (!over?.id) return;
if (!over) return;
const activeId = active.id as string | number;
const activeDealId = Number(active.id);
if (typeof activeId === "string" && activeId.endsWith("-status")) {
handleColumnDragOver(activeId, over);
} else {
handleDealDragOver(activeId, over);
}
};
const handleDealDragOver = (activeId: string | number, over: Over) => {
const activeDealId = Number(activeId);
const activeStatusId = getStatusByDealId(activeDealId)?.id;
if (!activeStatusId) return;
@ -85,6 +114,32 @@ const StatusColumnsDnd: FC<Props> = props => {
);
};
const handleColumnDragOver = (activeId: string, over: Over) => {
const activeStatusId = Number(activeId.replace("-status", ""));
let overStatusId: number;
if (typeof over.id === "string" && over.id.endsWith("-status")) {
overStatusId = Number(over.id.replace("-status", ""));
} else {
const deal = deals.find(deal => deal.id === over.id);
if (!deal) return;
overStatusId = deal.statusId;
}
if (!overStatusId || activeStatusId === overStatusId) return;
const newRank = getNewStatusRank(activeStatusId, overStatusId);
if (!newRank) return;
setStatuses(statuses =>
statuses.map(status =>
status.id === activeStatusId
? { ...status, rank: newRank }
: status
)
);
};
const getDropTarget = (
overId: string | number,
activeDealId: number,
@ -164,11 +219,68 @@ const StatusColumnsDnd: FC<Props> = props => {
return getNewLexorank(leftLexorank, rightLexorank).toString();
};
const getNewStatusRank = (activeStatusId: number, overStatusId: number) => {
const sortedStatusList = sortByLexorank(statuses);
const overIndex = sortedStatusList.findIndex(
s => s.id === overStatusId
);
const activeIndex = sortedStatusList.findIndex(
s => s.id === activeStatusId
);
if (overIndex === -1 || activeIndex === -1) return null;
const [leftIndex, rightIndex] =
overIndex < activeIndex
? [overIndex - 1, overIndex]
: [overIndex, overIndex + 1];
const leftLexorank =
leftIndex >= 0 ? LexoRank.parse(statuses[leftIndex].rank) : null;
const rightLexorank =
rightIndex < statuses.length
? LexoRank.parse(statuses[rightIndex].rank)
: null;
return getNewLexorank(leftLexorank, rightLexorank).toString();
};
const handleDragEnd = ({ active, over }: DragOverEvent) => {
setActiveDeal(null);
if (!over?.id) return;
setActiveStatus(null);
if (!over) return;
const activeDealId = Number(active.id);
const activeId: string | number = active.id;
if (typeof activeId === "string" && activeId.endsWith("-status")) {
handleStatusColumnDragEnd(activeId, over);
} else {
handleDealDragEnd(activeId, over);
}
};
const handleStatusColumnDragEnd = (activeId: string, over: Over) => {
const activeStatusId = Number(activeId.replace("-status", ""));
let overStatusId: number;
if (typeof over.id === "string" && over.id.endsWith("-status")) {
overStatusId = Number(over.id.replace("-status", ""));
} else {
const deal = deals.find(deal => deal.statusId === over.id);
if (!deal) return;
overStatusId = deal.statusId;
}
if (!overStatusId || activeStatusId === overStatusId) return;
const newRank = getNewStatusRank(activeStatusId, overStatusId);
if (!newRank) return;
props.onStatusDragEnd?.(activeStatusId, newRank);
};
const handleDealDragEnd = (activeId: number | string, over: Over) => {
const activeDealId = Number(activeId);
const activeStatusId = getStatusByDealId(activeDealId)?.id;
if (!activeStatusId) return;
@ -192,26 +304,47 @@ const StatusColumnsDnd: FC<Props> = props => {
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}>
<Group
gap={"xs"}
wrap={"nowrap"}
align={"start"}>
{statuses.map(status => (
<StatusColumn
key={status.id}
id={`${status.id}-status`}
title={status.name}
deals={deals.filter(
deal => deal.statusId === status.id
)}
/>
))}
<DragOverlay dropAnimation={defaultDropAnimation}>
<div style={{ cursor: "grabbing" }}>
{activeDeal ? <DealCard deal={activeDeal} /> : null}
</div>
</DragOverlay>
</Group>
<SortableContext
items={sortedStatuses.map(status => `${status.id}-status`)}
strategy={horizontalListSortingStrategy}>
<Group
gap={"xs"}
wrap={"nowrap"}
align={"start"}>
{sortedStatuses.map(status => (
<SortableItem
key={status.id}
id={`${status.id}-status`}>
<StatusColumn
id={`${status.id}-status`}
title={status.name}
deals={deals.filter(
deal => deal.statusId === status.id
)}
isDragging={activeStatus?.id === status.id}
/>
</SortableItem>
))}
<DragOverlay dropAnimation={defaultDropAnimation}>
<div style={{ cursor: "grabbing" }}>
{activeDeal ? (
<DealCard deal={activeDeal} />
) : activeStatus ? (
<StatusColumn
id={`${activeStatus.id}-status`}
title={activeStatus.name}
deals={deals.filter(
deal =>
deal.statusId ===
activeStatus.id
)}
isDragging
/>
) : null}
</div>
</DragOverlay>
</Group>
</SortableContext>
</DndContext>
</ScrollArea>
);