feat: raw statuses dnd
This commit is contained in:
@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user