Use DnDKit for ranking tasks

This commit is contained in:
AbdBarho
2022-12-29 22:10:21 +01:00
parent 491b1f6b12
commit ece0227aec
5 changed files with 31525 additions and 4965 deletions
+31457 -4896
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -16,6 +16,7 @@
},
"dependencies": {
"@chakra-ui/react": "^2.4.4",
"@dnd-kit/sortable": "^7.0.1",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@headlessui/react": "^1.7.7",
@@ -58,11 +59,11 @@
"@storybook/testing-library": "^0.0.13",
"@types/node": "18.11.17",
"@types/react": "18.0.26",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"babel-loader": "^8.3.0",
"cypress": "^12.2.0",
"cypress-image-diff-js": "^1.23.0",
"eslint-plugin-storybook": "^0.6.8",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"prettier": "2.8.1",
"prisma": "^4.7.1",
"typescript": "4.9.4"
+46 -33
View File
@@ -1,4 +1,8 @@
import { DndContext, PointerSensor, TouchSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
import { ReactNode, useEffect, useState } from "react";
import { SortableContext, arrayMove, verticalListSortingStrategy } from "@dnd-kit/sortable";
import type { DragEndEvent } from "@dnd-kit/core/dist/types/events";
import { Flex } from "@chakra-ui/react";
import { SortableItem } from "./SortableItem";
export interface SortableProps {
@@ -6,43 +10,52 @@ export interface SortableProps {
onChange: (newSortedIndices: number[]) => void;
}
export const Sortable = ({ items, onChange }) => {
const [sortOrder, setSortOrder] = useState<number[]>([]);
interface SortableItems {
id: number;
originalIndex: number;
item: ReactNode;
}
const update = (newRanking: number[]) => {
setSortOrder(newRanking);
onChange(newRanking);
};
export const Sortable = ({ items, onChange }: SortableProps) => {
const [itemsWithIds, setItemsWithIds] = useState<SortableItems[]>([]);
useEffect(() => {
const indices = Array.from({ length: items.length }).map((_, i) => i);
setSortOrder(indices);
onChange(indices);
}, [items, onChange]);
setItemsWithIds(
items.map((item, idx) => ({
item,
id: idx + 1, // +1 because dndtoolkit has problem with "falsy" ids
originalIndex: idx,
}))
);
}, [items]);
const sensors = useSensors(useSensor(PointerSensor), useSensor(TouchSensor));
return (
<ul className="flex flex-col gap-4">
{sortOrder.map((rank, i) => (
<SortableItem
key={`${rank}`}
canIncrement={i > 0}
onIncrement={() => {
const newRanking = sortOrder.slice();
const newIdx = i - 1;
[newRanking[i], newRanking[newIdx]] = [newRanking[newIdx], newRanking[i]];
update(newRanking);
}}
canDecrement={i < sortOrder.length - 1}
onDecrement={() => {
const newRanking = sortOrder.slice();
const newIdx = i + 1;
[newRanking[i], newRanking[newIdx]] = [newRanking[newIdx], newRanking[i]];
update(newRanking);
}}
>
{items[rank]}
</SortableItem>
))}
</ul>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={itemsWithIds} strategy={verticalListSortingStrategy}>
<Flex direction="column" gap={2}>
{itemsWithIds.map(({ id, item }) => (
<SortableItem key={id} id={id}>
{item}
</SortableItem>
))}
</Flex>
</SortableContext>
</DndContext>
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (active.id === over.id) {
return;
}
setItemsWithIds((items) => {
const oldIndex = items.findIndex((x) => x.id === active.id);
const newIndex = items.findIndex((x) => x.id === over.id);
const newArray = arrayMove(items, oldIndex, newIndex);
onChange(newArray.map((item) => item.originalIndex));
return newArray;
});
}
};
@@ -1,40 +1,25 @@
import { ArrowUpIcon, ArrowDownIcon } from "@heroicons/react/20/solid";
import { Button } from "@chakra-ui/react";
import clsx from "clsx";
import { CSS } from "@dnd-kit/utilities";
import { PropsWithChildren } from "react";
import { useSortable } from "@dnd-kit/sortable";
export interface SortableItemProps {
canIncrement: boolean;
canDecrement: boolean;
onIncrement: () => void;
onDecrement: () => void;
children: React.ReactNode;
}
export const SortableItem = ({ children, id }: PropsWithChildren<{ id: number }>) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
touchAction: "none",
};
export const SortableItem = ({ canIncrement, canDecrement, onIncrement, onDecrement, children }: SortableItemProps) => {
return (
<li className="grid grid-cols-[min-content_1fr] items-center rounded-lg shadow-md gap-x-2 p-2">
<ArrowButton active={canIncrement} onClick={onIncrement}>
<ArrowUpIcon width={28} />
</ArrowButton>
<span style={{ gridRow: "span 2" }}>{children}</span>
<ArrowButton active={canDecrement} onClick={onDecrement}>
<ArrowDownIcon width={28} />
</ArrowButton>
<li
className="rounded-lg shadow-md p-4 bg-white hover:bg-slate-50"
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
>
{children}
</li>
);
};
interface ArrowButtonProps {
active: boolean;
onClick: () => void;
children: React.ReactNode;
}
const ArrowButton = ({ children, active, onClick }: ArrowButtonProps) => {
return (
<Button justifyContent="center" variant="ghost" onClick={onClick} disabled={!active}>
{children}
</Button>
);
};
+1 -1
View File
@@ -4,7 +4,7 @@ declare global {
var prisma: PrismaClient | undefined;
}
const client = new PrismaClient();
const client = globalThis.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalThis.prisma = client;
}