209 lines
6.2 KiB
TypeScript
209 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import React, { FC, useState } from "react";
|
|
import {
|
|
closestCorners,
|
|
defaultDropAnimation,
|
|
DndContext,
|
|
DragOverEvent,
|
|
DragOverlay,
|
|
DragStartEvent,
|
|
KeyboardSensor,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
} from "@dnd-kit/core";
|
|
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
|
import { LexoRank } from "lexorank";
|
|
import { Group } 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 { DealSchema } from "@/types/DealSchema";
|
|
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
|
|
|
type Props = {
|
|
onDealDragEnd: (
|
|
dealId: number,
|
|
statusId: number,
|
|
lexorank?: string
|
|
) => void;
|
|
};
|
|
|
|
const StatusColumnsDnd: FC<Props> = props => {
|
|
const { statuses, deals, setDeals } = useStatusesContext();
|
|
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor),
|
|
useSensor(KeyboardSensor, {
|
|
coordinateGetter: sortableKeyboardCoordinates,
|
|
})
|
|
);
|
|
|
|
const handleDragStart = ({ active }: DragStartEvent) => {
|
|
setActiveDeal(
|
|
deals.find(deal => deal.id === (active.id as number)) ?? null
|
|
);
|
|
};
|
|
|
|
const getStatusByDealId = (dealId: number) => {
|
|
const deal = deals.find(deal => deal.id === dealId);
|
|
if (!deal) return;
|
|
return statuses.find(status => status.id === deal.statusId);
|
|
};
|
|
|
|
const handleDragOver = ({ active, over }: DragOverEvent) => {
|
|
if (!over?.id) return;
|
|
|
|
const activeDealId = Number(active.id);
|
|
const activeStatusId = getStatusByDealId(activeDealId)?.id;
|
|
if (!activeStatusId) return;
|
|
|
|
const { overStatusId, newLexorank } = getDropTarget(
|
|
over.id,
|
|
activeDealId,
|
|
activeStatusId
|
|
);
|
|
if (!overStatusId) return;
|
|
|
|
setDeals(deals =>
|
|
deals.map(deal =>
|
|
deal.id === activeDealId
|
|
? {
|
|
...deal,
|
|
statusId: overStatusId,
|
|
rank: newLexorank || deal.rank,
|
|
}
|
|
: deal
|
|
)
|
|
);
|
|
};
|
|
|
|
const getDropTarget = (
|
|
overId: string | number,
|
|
activeDealId: number,
|
|
activeStatusId: number
|
|
) => {
|
|
if (typeof overId === "string") {
|
|
return {
|
|
overStatusId: Number(overId.replace(/-status$/, "")),
|
|
newLexorank: undefined,
|
|
};
|
|
}
|
|
|
|
const overDealId = Number(overId);
|
|
const overStatusId = getStatusByDealId(overDealId)?.id;
|
|
|
|
if (!overStatusId || activeDealId === overDealId) {
|
|
return { overStatusId: undefined, newLexorank: undefined };
|
|
}
|
|
|
|
const statusDeals = sortByLexorank(
|
|
deals.filter(deal => deal.statusId === overStatusId)
|
|
);
|
|
const overDealIndex = statusDeals.findIndex(
|
|
deal => deal.id === overDealId
|
|
);
|
|
|
|
let newLexorank;
|
|
if (activeStatusId === overStatusId) {
|
|
newLexorank = getNewRankForSameStatus(
|
|
statusDeals,
|
|
overDealIndex,
|
|
activeDealId
|
|
);
|
|
} else {
|
|
newLexorank = getNewRankForAnotherStatus(
|
|
statusDeals,
|
|
overDealIndex
|
|
);
|
|
}
|
|
|
|
return { overStatusId, newLexorank };
|
|
};
|
|
|
|
const getNewRankForSameStatus = (
|
|
statusDeals: DealSchema[],
|
|
overDealIndex: number,
|
|
activeDealId: number
|
|
) => {
|
|
const activeDealIndex = statusDeals.findIndex(
|
|
deal => deal.id === activeDealId
|
|
);
|
|
const [leftIndex, rightIndex] =
|
|
overDealIndex < activeDealIndex
|
|
? [overDealIndex - 1, overDealIndex]
|
|
: [overDealIndex, overDealIndex + 1];
|
|
|
|
const leftLexorank =
|
|
leftIndex >= 0 ? LexoRank.parse(deals[leftIndex].rank) : null;
|
|
const rightLexorank =
|
|
rightIndex < deals.length
|
|
? LexoRank.parse(deals[rightIndex].rank)
|
|
: null;
|
|
|
|
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
|
};
|
|
|
|
const getNewRankForAnotherStatus = (
|
|
statusDeals: DealSchema[],
|
|
overDealIndex: number
|
|
) => {
|
|
const leftLexorank =
|
|
overDealIndex > 0
|
|
? LexoRank.parse(statusDeals[overDealIndex - 1].rank)
|
|
: null;
|
|
const rightLexorank = LexoRank.parse(statusDeals[overDealIndex].rank);
|
|
|
|
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
|
};
|
|
|
|
const handleDragEnd = ({ active, over }: DragOverEvent) => {
|
|
setActiveDeal(null);
|
|
if (!over?.id) return;
|
|
|
|
const activeDealId = Number(active.id);
|
|
const activeStatusId = getStatusByDealId(activeDealId)?.id;
|
|
if (!activeStatusId) return;
|
|
|
|
const { overStatusId, newLexorank } = getDropTarget(
|
|
over.id,
|
|
activeDealId,
|
|
activeStatusId
|
|
);
|
|
if (!overStatusId) return;
|
|
|
|
props.onDealDragEnd(activeDealId, overStatusId, newLexorank);
|
|
};
|
|
|
|
return (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCorners}
|
|
onDragStart={handleDragStart}
|
|
onDragOver={handleDragOver}
|
|
onDragEnd={handleDragEnd}>
|
|
<Group
|
|
gap={"xs"}
|
|
justify={"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}>
|
|
{activeDeal ? <DealCard deal={activeDeal} /> : null}
|
|
</DragOverlay>
|
|
</Group>
|
|
</DndContext>
|
|
);
|
|
};
|
|
|
|
export default StatusColumnsDnd;
|