From 586af488da810b1da77f00000b81ef2b5e4e1a8d Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Fri, 1 Aug 2025 17:50:27 +0400 Subject: [PATCH] feat: raw statuses dnd --- .../components/StatusColumn/StatusColumn.tsx | 15 +- .../StatusColumnsDnd/StatusColumnsDnd.tsx | 191 +++++++++++++++--- src/components/SortableDnd/SortableItem.tsx | 2 +- 3 files changed, 175 insertions(+), 33 deletions(-) diff --git a/src/app/deals/components/StatusColumn/StatusColumn.tsx b/src/app/deals/components/StatusColumn/StatusColumn.tsx index a22041d..8a1d380 100644 --- a/src/app/deals/components/StatusColumn/StatusColumn.tsx +++ b/src/app/deals/components/StatusColumn/StatusColumn.tsx @@ -14,13 +14,15 @@ type BoardSectionProps = { id: string; title: string; deals: DealSchema[]; + isDragging?: boolean; }; -const StatusColumn = ({ id, title, deals }: BoardSectionProps) => { +const StatusColumn = ({ id, title, deals, isDragging }: BoardSectionProps) => { const { setNodeRef } = useDroppable({ id }); - const sortedDeals = useMemo(() => sortByLexorank(deals), [deals]); + console.log("rerender"); + return ( { width: "15vw", minWidth: 150, }}> - {title} + + {title} + void; + onStatusDragEnd?: (statusId: number, newRank: string) => void; }; const StatusColumnsDnd: FC = props => { - const { statuses, deals, setDeals } = useStatusesContext(); + const { statuses, deals, setDeals, setStatuses } = useStatusesContext(); const [activeDeal, setActiveDeal] = useState(null); + const [activeStatus, setActiveStatus] = useState(null); + + const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]); const throttledSetDeals = useMemo( () => throttle(setDeals, 200), [setDeals] @@ -47,9 +58,18 @@ const StatusColumnsDnd: FC = 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 => { }; 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 => { ); }; + 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 => { 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 => { onDragStart={handleDragStart} onDragOver={handleDragOver} onDragEnd={handleDragEnd}> - - {statuses.map(status => ( - deal.statusId === status.id - )} - /> - ))} - -
- {activeDeal ? : null} -
-
-
+ `${status.id}-status`)} + strategy={horizontalListSortingStrategy}> + + {sortedStatuses.map(status => ( + + deal.statusId === status.id + )} + isDragging={activeStatus?.id === status.id} + /> + + ))} + +
+ {activeDeal ? ( + + ) : activeStatus ? ( + + deal.statusId === + activeStatus.id + )} + isDragging + /> + ) : null} +
+
+
+
); diff --git a/src/components/SortableDnd/SortableItem.tsx b/src/components/SortableDnd/SortableItem.tsx index 9208746..2f3e3c5 100644 --- a/src/components/SortableDnd/SortableItem.tsx +++ b/src/components/SortableDnd/SortableItem.tsx @@ -5,7 +5,7 @@ import DragHandle from "@/components/SortableDnd/DragHandle"; import SortableItemContext from "./SortableItemContext"; type Props = { - id: number; + id: number | string; itemStyle?: CSSProperties; };