refactor: refactoring of deals and statuses dnd
This commit is contained in:
37
src/app/deals/components/DndOverlay/DndOverlay.tsx
Normal file
37
src/app/deals/components/DndOverlay/DndOverlay.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/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 { StatusSchema } from "@/types/StatusSchema";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
activeDeal: DealSchema | null;
|
||||||
|
activeStatus: StatusSchema | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DndOverlay = ({ activeStatus, activeDeal }: Props) => {
|
||||||
|
const { deals } = useStatusesContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragOverlay dropAnimation={defaultDropAnimation}>
|
||||||
|
<div style={{ cursor: "grabbing" }}>
|
||||||
|
{activeDeal ? (
|
||||||
|
<DealCard deal={activeDeal} />
|
||||||
|
) : activeStatus ? (
|
||||||
|
<StatusColumn
|
||||||
|
id={`${activeStatus.id}-status`}
|
||||||
|
status={activeStatus}
|
||||||
|
deals={deals.filter(
|
||||||
|
deal => deal.statusId === activeStatus.id
|
||||||
|
)}
|
||||||
|
isDragging
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</DragOverlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DndOverlay;
|
||||||
@ -2,12 +2,12 @@ import React from "react";
|
|||||||
import { useSortable } from "@dnd-kit/sortable";
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
type SortableTaskItemProps = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SortableItem = ({ children, id }: SortableTaskItemProps) => {
|
const SortableItem = ({ children, id }: Props) => {
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
|
|||||||
@ -10,14 +10,14 @@ import { DealSchema } from "@/types/DealSchema";
|
|||||||
import { StatusSchema } from "@/types/StatusSchema";
|
import { StatusSchema } from "@/types/StatusSchema";
|
||||||
import { sortByLexorank } from "@/utils/lexorank";
|
import { sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
type BoardSectionProps = {
|
type Props = {
|
||||||
id: string;
|
id: string;
|
||||||
status: StatusSchema;
|
status: StatusSchema;
|
||||||
deals: DealSchema[];
|
deals: DealSchema[];
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusColumn = ({ id, status, deals, isDragging }: BoardSectionProps) => {
|
const StatusColumn = ({ id, status, deals, isDragging }: Props) => {
|
||||||
const { setNodeRef } = useDroppable({ id });
|
const { setNodeRef } = useDroppable({ id });
|
||||||
const sortedDeals = useMemo(
|
const sortedDeals = useMemo(
|
||||||
() => sortByLexorank(deals.filter(deal => deal.statusId === status.id)),
|
() => sortByLexorank(deals.filter(deal => deal.statusId === status.id)),
|
||||||
|
|||||||
@ -1,35 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { FC, useMemo, useState } from "react";
|
import React, { FC } from "react";
|
||||||
import {
|
import { closestCorners, DndContext } from "@dnd-kit/core";
|
||||||
closestCorners,
|
|
||||||
defaultDropAnimation,
|
|
||||||
DndContext,
|
|
||||||
DragOverEvent,
|
|
||||||
DragOverlay,
|
|
||||||
DragStartEvent,
|
|
||||||
KeyboardSensor,
|
|
||||||
Over,
|
|
||||||
PointerSensor,
|
|
||||||
TouchSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
} from "@dnd-kit/core";
|
|
||||||
import {
|
import {
|
||||||
horizontalListSortingStrategy,
|
horizontalListSortingStrategy,
|
||||||
SortableContext,
|
SortableContext,
|
||||||
sortableKeyboardCoordinates,
|
|
||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { LexoRank } from "lexorank";
|
|
||||||
import { throttle } from "lodash";
|
|
||||||
import { Group, ScrollArea } from "@mantine/core";
|
import { Group, ScrollArea } from "@mantine/core";
|
||||||
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
import DndOverlay from "@/app/deals/components/DndOverlay/DndOverlay";
|
||||||
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
|
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
|
||||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import useDnd from "@/app/deals/hooks/useDnd";
|
||||||
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||||
import { DealSchema } from "@/types/DealSchema";
|
import useDndSensors from "../../hooks/useSensors";
|
||||||
import { StatusSchema } from "@/types/StatusSchema";
|
|
||||||
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDealDragEnd: (
|
onDealDragEnd: (
|
||||||
@ -41,270 +24,18 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const StatusColumnsDnd: FC<Props> = props => {
|
const StatusColumnsDnd: FC<Props> = props => {
|
||||||
const { statuses, deals, setDeals, setStatuses } = useStatusesContext();
|
const { deals } = useStatusesContext();
|
||||||
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
|
||||||
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
|
||||||
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
|
||||||
|
|
||||||
const throttledSetStatuses = useMemo(
|
const {
|
||||||
() => throttle(setStatuses, 200),
|
sortedStatuses,
|
||||||
[setStatuses]
|
handleDragStart,
|
||||||
);
|
handleDragOver,
|
||||||
const throttledSetDeals = useMemo(
|
handleDragEnd,
|
||||||
() => throttle(setDeals, 200),
|
activeStatus,
|
||||||
[setDeals]
|
activeDeal,
|
||||||
);
|
} = useDnd(props);
|
||||||
|
|
||||||
const sensorOptions = {
|
const sensors = useDndSensors();
|
||||||
activationConstraint: {
|
|
||||||
distance: 5,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const sensors = useSensors(
|
|
||||||
useSensor(PointerSensor, sensorOptions),
|
|
||||||
useSensor(KeyboardSensor, {
|
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
|
||||||
}),
|
|
||||||
useSensor(TouchSensor, sensorOptions)
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDragStart = ({ active }: DragStartEvent) => {
|
|
||||||
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) => {
|
|
||||||
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) return;
|
|
||||||
const activeId = active.id as string | number;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const { overStatusId, newLexorank } = getDropTarget(
|
|
||||||
over.id,
|
|
||||||
activeDealId,
|
|
||||||
activeStatusId
|
|
||||||
);
|
|
||||||
if (!overStatusId) return;
|
|
||||||
|
|
||||||
throttledSetDeals(deals =>
|
|
||||||
deals.map(deal =>
|
|
||||||
deal.id === activeDealId
|
|
||||||
? {
|
|
||||||
...deal,
|
|
||||||
statusId: overStatusId,
|
|
||||||
rank: newLexorank || deal.rank,
|
|
||||||
}
|
|
||||||
: deal
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
throttledSetStatuses(statuses =>
|
|
||||||
statuses.map(status =>
|
|
||||||
status.id === activeStatusId
|
|
||||||
? { ...status, rank: newRank }
|
|
||||||
: status
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 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);
|
|
||||||
setActiveStatus(null);
|
|
||||||
if (!over) return;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const { overStatusId, newLexorank } = getDropTarget(
|
|
||||||
over.id,
|
|
||||||
activeDealId,
|
|
||||||
activeStatusId
|
|
||||||
);
|
|
||||||
if (!overStatusId) return;
|
|
||||||
|
|
||||||
props.onDealDragEnd(activeDealId, overStatusId, newLexorank);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
@ -335,24 +66,10 @@ const StatusColumnsDnd: FC<Props> = props => {
|
|||||||
/>
|
/>
|
||||||
</SortableItem>
|
</SortableItem>
|
||||||
))}
|
))}
|
||||||
<DragOverlay dropAnimation={defaultDropAnimation}>
|
<DndOverlay
|
||||||
<div style={{ cursor: "grabbing" }}>
|
activeStatus={activeStatus}
|
||||||
{activeDeal ? (
|
activeDeal={activeDeal}
|
||||||
<DealCard deal={activeDeal} />
|
|
||||||
) : activeStatus ? (
|
|
||||||
<StatusColumn
|
|
||||||
id={`${activeStatus.id}-status`}
|
|
||||||
status={activeStatus}
|
|
||||||
deals={deals.filter(
|
|
||||||
deal =>
|
|
||||||
deal.statusId ===
|
|
||||||
activeStatus.id
|
|
||||||
)}
|
|
||||||
isDragging
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</DragOverlay>
|
|
||||||
</Group>
|
</Group>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|||||||
226
src/app/deals/hooks/useDnd.ts
Normal file
226
src/app/deals/hooks/useDnd.ts
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { DragOverEvent, DragStartEvent, Over } from "@dnd-kit/core";
|
||||||
|
import { throttle } from "lodash";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import useGetNewRank from "@/app/deals/hooks/useGetNewRank";
|
||||||
|
import { getStatusId, isStatusId } from "@/app/deals/utils/statusId";
|
||||||
|
import { DealSchema } from "@/types/DealSchema";
|
||||||
|
import { StatusSchema } from "@/types/StatusSchema";
|
||||||
|
import { sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDealDragEnd: (
|
||||||
|
dealId: number,
|
||||||
|
statusId: number,
|
||||||
|
lexorank?: string
|
||||||
|
) => void;
|
||||||
|
onStatusDragEnd: (statusId: number, lexorank: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDnd = (props: Props) => {
|
||||||
|
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
||||||
|
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
||||||
|
const { statuses, deals, setDeals, setStatuses } = useStatusesContext();
|
||||||
|
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getNewRankForSameStatus,
|
||||||
|
getNewRankForAnotherStatus,
|
||||||
|
getNewStatusRank,
|
||||||
|
} = useGetNewRank();
|
||||||
|
|
||||||
|
const throttledSetStatuses = useMemo(
|
||||||
|
() => throttle(setStatuses, 200),
|
||||||
|
[setStatuses]
|
||||||
|
);
|
||||||
|
const throttledSetDeals = useMemo(
|
||||||
|
() => throttle(setDeals, 200),
|
||||||
|
[setDeals]
|
||||||
|
);
|
||||||
|
|
||||||
|
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) return;
|
||||||
|
const activeId = active.id as string | number;
|
||||||
|
|
||||||
|
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
const { overStatusId, newLexorank } = getDropTarget(
|
||||||
|
over.id,
|
||||||
|
activeDealId,
|
||||||
|
activeStatusId
|
||||||
|
);
|
||||||
|
if (!overStatusId) return;
|
||||||
|
|
||||||
|
throttledSetDeals(deals =>
|
||||||
|
deals.map(deal =>
|
||||||
|
deal.id === activeDealId
|
||||||
|
? {
|
||||||
|
...deal,
|
||||||
|
statusId: overStatusId,
|
||||||
|
rank: newLexorank || deal.rank,
|
||||||
|
}
|
||||||
|
: deal
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleColumnDragOver = (activeId: string, over: Over) => {
|
||||||
|
const activeStatusId = getStatusId(activeId);
|
||||||
|
let overStatusId: number;
|
||||||
|
|
||||||
|
if (typeof over.id === "string" && isStatusId(over.id)) {
|
||||||
|
overStatusId = getStatusId(over.id);
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
throttledSetStatuses(statuses =>
|
||||||
|
statuses.map(status =>
|
||||||
|
status.id === activeStatusId
|
||||||
|
? { ...status, rank: newRank }
|
||||||
|
: status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDropTarget = (
|
||||||
|
overId: string | number,
|
||||||
|
activeDealId: number,
|
||||||
|
activeStatusId: number
|
||||||
|
) => {
|
||||||
|
if (typeof overId === "string") {
|
||||||
|
return {
|
||||||
|
overStatusId: getStatusId(overId),
|
||||||
|
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 handleDragEnd = ({ active, over }: DragOverEvent) => {
|
||||||
|
setActiveDeal(null);
|
||||||
|
setActiveStatus(null);
|
||||||
|
if (!over) return;
|
||||||
|
|
||||||
|
const activeId: string | number = active.id;
|
||||||
|
|
||||||
|
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||||
|
handleStatusColumnDragEnd(activeId, over);
|
||||||
|
} else {
|
||||||
|
handleDealDragEnd(activeId, over);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStatusColumnDragEnd = (activeId: string, over: Over) => {
|
||||||
|
const activeStatusId = getStatusId(activeId);
|
||||||
|
let overStatusId: number;
|
||||||
|
|
||||||
|
if (typeof over.id === "string" && isStatusId(over.id)) {
|
||||||
|
overStatusId = getStatusId(over.id);
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
const { overStatusId, newLexorank } = getDropTarget(
|
||||||
|
over.id,
|
||||||
|
activeDealId,
|
||||||
|
activeStatusId
|
||||||
|
);
|
||||||
|
if (!overStatusId) return;
|
||||||
|
|
||||||
|
props.onDealDragEnd(activeDealId, overStatusId, newLexorank);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragStart = ({ active }: DragStartEvent) => {
|
||||||
|
const activeId = active.id as string | number;
|
||||||
|
|
||||||
|
if (typeof activeId === "string" && isStatusId(activeId)) {
|
||||||
|
const statusId = getStatusId(activeId);
|
||||||
|
setActiveStatus(
|
||||||
|
statuses.find(status => status.id === statusId) ?? null
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setActiveDeal(
|
||||||
|
deals.find(deal => deal.id === (activeId as number)) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
sortedStatuses,
|
||||||
|
handleDragStart,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragEnd,
|
||||||
|
activeStatus,
|
||||||
|
activeDeal,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDnd;
|
||||||
78
src/app/deals/hooks/useGetNewRank.ts
Normal file
78
src/app/deals/hooks/useGetNewRank.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { LexoRank } from "lexorank";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import { DealSchema } from "@/types/DealSchema";
|
||||||
|
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
|
const useGetNewRank = () => {
|
||||||
|
const { deals, statuses } = useStatusesContext();
|
||||||
|
|
||||||
|
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 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();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getNewRankForSameStatus,
|
||||||
|
getNewRankForAnotherStatus,
|
||||||
|
getNewStatusRank,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGetNewRank;
|
||||||
26
src/app/deals/hooks/useSensors.ts
Normal file
26
src/app/deals/hooks/useSensors.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
KeyboardSensor,
|
||||||
|
PointerSensor,
|
||||||
|
TouchSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core";
|
||||||
|
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
||||||
|
|
||||||
|
const useDndSensors = () => {
|
||||||
|
const sensorOptions = {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return useSensors(
|
||||||
|
useSensor(PointerSensor, sensorOptions),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
}),
|
||||||
|
useSensor(TouchSensor, sensorOptions)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDndSensors;
|
||||||
6
src/app/deals/utils/statusId.ts
Normal file
6
src/app/deals/utils/statusId.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const STATUS_POSTFIX = "-status";
|
||||||
|
|
||||||
|
export const isStatusId = (rawId: string) => rawId.endsWith(STATUS_POSTFIX);
|
||||||
|
|
||||||
|
export const getStatusId = (rawId: string) =>
|
||||||
|
Number(rawId.replace(STATUS_POSTFIX, ""));
|
||||||
Reference in New Issue
Block a user