feat: raw statuses dnd
This commit is contained in:
@ -14,13 +14,15 @@ type BoardSectionProps = {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
deals: DealSchema[];
|
deals: DealSchema[];
|
||||||
|
isDragging?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusColumn = ({ id, title, deals }: BoardSectionProps) => {
|
const StatusColumn = ({ id, title, deals, isDragging }: BoardSectionProps) => {
|
||||||
const { setNodeRef } = useDroppable({ id });
|
const { setNodeRef } = useDroppable({ id });
|
||||||
|
|
||||||
const sortedDeals = useMemo(() => sortByLexorank(deals), [deals]);
|
const sortedDeals = useMemo(() => sortByLexorank(deals), [deals]);
|
||||||
|
|
||||||
|
console.log("rerender");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
@ -29,7 +31,14 @@ const StatusColumn = ({ id, title, deals }: BoardSectionProps) => {
|
|||||||
width: "15vw",
|
width: "15vw",
|
||||||
minWidth: 150,
|
minWidth: 150,
|
||||||
}}>
|
}}>
|
||||||
<Text>{title}</Text>
|
<Text
|
||||||
|
style={{
|
||||||
|
cursor: "grab",
|
||||||
|
userSelect: "none",
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
}}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
id={id}
|
id={id}
|
||||||
items={sortedDeals}
|
items={sortedDeals}
|
||||||
|
|||||||
@ -9,18 +9,25 @@ import {
|
|||||||
DragOverlay,
|
DragOverlay,
|
||||||
DragStartEvent,
|
DragStartEvent,
|
||||||
KeyboardSensor,
|
KeyboardSensor,
|
||||||
|
Over,
|
||||||
PointerSensor,
|
PointerSensor,
|
||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from "@dnd-kit/core";
|
} from "@dnd-kit/core";
|
||||||
import { sortableKeyboardCoordinates } from "@dnd-kit/sortable";
|
import {
|
||||||
|
horizontalListSortingStrategy,
|
||||||
|
SortableContext,
|
||||||
|
sortableKeyboardCoordinates,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
import { LexoRank } from "lexorank";
|
import { LexoRank } from "lexorank";
|
||||||
import { throttle } from "lodash";
|
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 DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||||
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 { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||||
import { DealSchema } from "@/types/DealSchema";
|
import { DealSchema } from "@/types/DealSchema";
|
||||||
|
import { StatusSchema } from "@/types/StatusSchema";
|
||||||
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
import { getNewLexorank, sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -29,11 +36,15 @@ type Props = {
|
|||||||
statusId: number,
|
statusId: number,
|
||||||
lexorank?: string
|
lexorank?: string
|
||||||
) => void;
|
) => void;
|
||||||
|
onStatusDragEnd?: (statusId: number, newRank: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatusColumnsDnd: FC<Props> = props => {
|
const StatusColumnsDnd: FC<Props> = props => {
|
||||||
const { statuses, deals, setDeals } = useStatusesContext();
|
const { statuses, deals, setDeals, setStatuses } = useStatusesContext();
|
||||||
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
const [activeDeal, setActiveDeal] = useState<DealSchema | null>(null);
|
||||||
|
const [activeStatus, setActiveStatus] = useState<StatusSchema | null>(null);
|
||||||
|
|
||||||
|
const sortedStatuses = useMemo(() => sortByLexorank(statuses), [statuses]);
|
||||||
const throttledSetDeals = useMemo(
|
const throttledSetDeals = useMemo(
|
||||||
() => throttle(setDeals, 200),
|
() => throttle(setDeals, 200),
|
||||||
[setDeals]
|
[setDeals]
|
||||||
@ -47,9 +58,18 @@ const StatusColumnsDnd: FC<Props> = props => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleDragStart = ({ active }: DragStartEvent) => {
|
const handleDragStart = ({ active }: DragStartEvent) => {
|
||||||
setActiveDeal(
|
const activeId = active.id as string | number;
|
||||||
deals.find(deal => deal.id === (active.id as number)) ?? null
|
|
||||||
);
|
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 getStatusByDealId = (dealId: number) => {
|
||||||
@ -59,9 +79,18 @@ const StatusColumnsDnd: FC<Props> = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = ({ active, over }: DragOverEvent) => {
|
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;
|
const activeStatusId = getStatusByDealId(activeDealId)?.id;
|
||||||
if (!activeStatusId) return;
|
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 = (
|
const getDropTarget = (
|
||||||
overId: string | number,
|
overId: string | number,
|
||||||
activeDealId: number,
|
activeDealId: number,
|
||||||
@ -164,11 +219,68 @@ const StatusColumnsDnd: FC<Props> = props => {
|
|||||||
return getNewLexorank(leftLexorank, rightLexorank).toString();
|
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) => {
|
const handleDragEnd = ({ active, over }: DragOverEvent) => {
|
||||||
setActiveDeal(null);
|
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;
|
const activeStatusId = getStatusByDealId(activeDealId)?.id;
|
||||||
if (!activeStatusId) return;
|
if (!activeStatusId) return;
|
||||||
|
|
||||||
@ -192,26 +304,47 @@ const StatusColumnsDnd: FC<Props> = props => {
|
|||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragEnd={handleDragEnd}>
|
onDragEnd={handleDragEnd}>
|
||||||
<Group
|
<SortableContext
|
||||||
gap={"xs"}
|
items={sortedStatuses.map(status => `${status.id}-status`)}
|
||||||
wrap={"nowrap"}
|
strategy={horizontalListSortingStrategy}>
|
||||||
align={"start"}>
|
<Group
|
||||||
{statuses.map(status => (
|
gap={"xs"}
|
||||||
<StatusColumn
|
wrap={"nowrap"}
|
||||||
key={status.id}
|
align={"start"}>
|
||||||
id={`${status.id}-status`}
|
{sortedStatuses.map(status => (
|
||||||
title={status.name}
|
<SortableItem
|
||||||
deals={deals.filter(
|
key={status.id}
|
||||||
deal => deal.statusId === status.id
|
id={`${status.id}-status`}>
|
||||||
)}
|
<StatusColumn
|
||||||
/>
|
id={`${status.id}-status`}
|
||||||
))}
|
title={status.name}
|
||||||
<DragOverlay dropAnimation={defaultDropAnimation}>
|
deals={deals.filter(
|
||||||
<div style={{ cursor: "grabbing" }}>
|
deal => deal.statusId === status.id
|
||||||
{activeDeal ? <DealCard deal={activeDeal} /> : null}
|
)}
|
||||||
</div>
|
isDragging={activeStatus?.id === status.id}
|
||||||
</DragOverlay>
|
/>
|
||||||
</Group>
|
</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>
|
</DndContext>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import DragHandle from "@/components/SortableDnd/DragHandle";
|
|||||||
import SortableItemContext from "./SortableItemContext";
|
import SortableItemContext from "./SortableItemContext";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: number;
|
id: number | string;
|
||||||
itemStyle?: CSSProperties;
|
itemStyle?: CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user