feat: raw deals dnd between statuses
This commit is contained in:
@ -3,12 +3,12 @@
|
||||
import React from "react";
|
||||
import { ScrollArea } from "@mantine/core";
|
||||
import Board from "@/app/deals/components/Board/Board";
|
||||
import useBoards from "@/app/deals/hooks/useBoards";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import SortableDnd from "@/components/SortableDnd";
|
||||
import { BoardSchema } from "@/types/BoardSchema";
|
||||
|
||||
const Boards = () => {
|
||||
const { boards, setBoards } = useBoards();
|
||||
const { boards } = useBoardsContext();
|
||||
|
||||
const renderBoard = (board: BoardSchema) => {
|
||||
return <Board board={board} />;
|
||||
|
||||
12
src/app/deals/components/DealCard/DealCard.tsx
Normal file
12
src/app/deals/components/DealCard/DealCard.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Card } from "@mantine/core";
|
||||
import { DealSchema } from "@/types/DealSchema";
|
||||
|
||||
type Props = {
|
||||
deal: DealSchema;
|
||||
};
|
||||
|
||||
const DealCard = ({ deal }: Props) => {
|
||||
return <Card>{deal.name}</Card>;
|
||||
};
|
||||
|
||||
export default DealCard;
|
||||
37
src/app/deals/components/SortableItem/SortableItem.tsx
Normal file
37
src/app/deals/components/SortableItem/SortableItem.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
type SortableTaskItemProps = {
|
||||
children: React.ReactNode;
|
||||
id: string;
|
||||
};
|
||||
|
||||
const SortableItem = ({ children, id }: SortableTaskItemProps) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortableItem;
|
||||
48
src/app/deals/components/StatusColumn/StatusColumn.tsx
Normal file
48
src/app/deals/components/StatusColumn/StatusColumn.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { Box, Text } from "@mantine/core";
|
||||
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||
import { DealSchema } from "@/types/DealSchema";
|
||||
import { sortByLexorank } from "@/utils/lexorank";
|
||||
|
||||
type BoardSectionProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
deals: DealSchema[];
|
||||
};
|
||||
|
||||
const StatusColumn = ({ id, title, deals }: BoardSectionProps) => {
|
||||
const { setNodeRef } = useDroppable({ id });
|
||||
const [sortedDeals, setSortedDeals] = useState<DealSchema[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSortedDeals(sortByLexorank(deals));
|
||||
}, [deals]);
|
||||
|
||||
return (
|
||||
<Box style={{ backgroundColor: "#eee", padding: 2 }}>
|
||||
<Text>{title}</Text>
|
||||
<SortableContext
|
||||
id={id}
|
||||
items={sortedDeals}
|
||||
strategy={verticalListSortingStrategy}>
|
||||
<div ref={setNodeRef}>
|
||||
{sortedDeals.map(deal => (
|
||||
<Box key={deal.id}>
|
||||
<SortableItem id={deal.id}>
|
||||
<DealCard deal={deal} />
|
||||
</SortableItem>
|
||||
</Box>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusColumn;
|
||||
25
src/app/deals/components/StatusColumns/StatusColumns.tsx
Normal file
25
src/app/deals/components/StatusColumns/StatusColumns.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import StatusColumnsDnd from "@/app/deals/components/StatusColumnsDnd/StatusColumnsDnd";
|
||||
|
||||
const StatusColumns = () => {
|
||||
const onDealDragEnd = (
|
||||
dealId: number,
|
||||
statusId: number,
|
||||
lexorank?: string
|
||||
) => {
|
||||
// Send request to server
|
||||
console.log(
|
||||
"onDealDragEnd. dealId =",
|
||||
dealId,
|
||||
", statusId =",
|
||||
statusId,
|
||||
", lexorank =",
|
||||
lexorank
|
||||
);
|
||||
};
|
||||
|
||||
return <StatusColumnsDnd onDealDragEnd={onDealDragEnd} />;
|
||||
};
|
||||
|
||||
export default StatusColumns;
|
||||
210
src/app/deals/components/StatusColumnsDnd/StatusColumnsDnd.tsx
Normal file
210
src/app/deals/components/StatusColumnsDnd/StatusColumnsDnd.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
"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 { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
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 } = useBoardsContext();
|
||||
const [activeDealId, setActiveDealId] = useState<null | number>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
const handleDragStart = ({ active }: DragStartEvent) => {
|
||||
setActiveDealId(active.id as number);
|
||||
};
|
||||
|
||||
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) => {
|
||||
setActiveDealId(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);
|
||||
};
|
||||
|
||||
const deal = activeDealId
|
||||
? deals.find(deal => deal.id === activeDealId)
|
||||
: null;
|
||||
|
||||
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}>
|
||||
{deal ? <DealCard deal={deal} /> : null}
|
||||
</DragOverlay>
|
||||
</Group>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusColumnsDnd;
|
||||
Reference in New Issue
Block a user