From 8d6f2636c5810c40b7cd03d7d748e2478318f53d Mon Sep 17 00:00:00 2001 From: Donkie Date: Wed, 20 Nov 2024 20:12:08 +0100 Subject: [PATCH] Locations can now be drag-dropped around --- client/src/pages/locations/functions.ts | 17 ++ client/src/pages/locations/index.tsx | 201 +++++++++++++++++------- 2 files changed, 159 insertions(+), 59 deletions(-) diff --git a/client/src/pages/locations/functions.ts b/client/src/pages/locations/functions.ts index 4bb91af..a500400 100644 --- a/client/src/pages/locations/functions.ts +++ b/client/src/pages/locations/functions.ts @@ -1,3 +1,5 @@ +import { useMemo } from "react"; +import { useGetSetting } from "../../utils/querySettings"; import { getAPIURL } from "../../utils/url"; import { ISpool } from "../spools/model"; @@ -16,3 +18,18 @@ export async function setSpoolLocation(spool_id: number, location: string | null } return response.json(); } + +export function useLocations(): string[] { + const query = useGetSetting("locations"); + + return useMemo(() => { + if (!query.data) return []; + + try { + return JSON.parse(query.data.value) as string[]; + } catch { + console.warn("Failed to parse locations", query.data.value); + return []; + } + }, [query.data]); +} diff --git a/client/src/pages/locations/index.tsx b/client/src/pages/locations/index.tsx index 818c540..7083d7e 100644 --- a/client/src/pages/locations/index.tsx +++ b/client/src/pages/locations/index.tsx @@ -2,20 +2,26 @@ import { IResourceComponentsProps, useInvalidate, useList, useNavigation, useTra import { Button, theme } from "antd"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; -import React, { useEffect, useMemo } from "react"; +import type { Identifier, XYCoord } from "dnd-core"; +import React, { useEffect, useMemo, useRef } from "react"; import { DndProvider, useDrag, useDrop } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { DeleteOutlined, EditOutlined, EyeOutlined, PlusOutlined } from "@ant-design/icons"; import { Link } from "react-router-dom"; import SpoolIcon from "../../components/spoolIcon"; -import { useGetSetting, useSetSetting } from "../../utils/querySettings"; +import { useSetSetting } from "../../utils/querySettings"; import { ISpool } from "../spools/model"; -import { setSpoolLocation } from "./functions"; +import { setSpoolLocation, useLocations } from "./functions"; import "./locations.css"; dayjs.extend(utc); +const ItemTypes = { + SPOOL: "spool", + CONTAINER: "spool-container", +}; + const { useToken } = theme; function SpoolCard({ spool }: { spool: ISpool }) { @@ -23,7 +29,7 @@ function SpoolCard({ spool }: { spool: ISpool }) { const t = useTranslate(); const [{ opacity }, dragRef] = useDrag( () => ({ - type: "spool", + type: ItemTypes.SPOOL, item: spool, collect: (monitor) => ({ opacity: monitor.isDragging() ? 0.5 : 1, @@ -82,24 +88,13 @@ function SpoolCard({ spool }: { spool: ISpool }) { ); } -function LocationContainer({ - title, - spools, - showDelete, - onDelete, -}: { - title: string; - spools: ISpool[]; - showDelete?: boolean; - onDelete?: () => void; -}) { +function SpoolList({ location, spools }: { location: string; spools: ISpool[] }) { const { token } = useToken(); const invalidate = useInvalidate(); - - const [, drop] = useDrop(() => ({ - accept: "spool", + const [, spoolDrop] = useDrop(() => ({ + accept: ItemTypes.SPOOL, drop: (item: ISpool) => { - setSpoolLocation(item.id, title).then(() => { + setSpoolLocation(item.id, location).then(() => { invalidate({ resource: "spool", id: item.id, @@ -115,36 +110,108 @@ function LocationContainer({ }; return ( -
-

- {title} - {showDelete &&

-
- {spools.map((spool) => ( - - ))} -
+
+ {spools.map((spool) => ( + + ))}
); } -function useLocations(): string[] { - const query = useGetSetting("locations"); - - return useMemo(() => { - if (!query.data) return []; - - try { - return JSON.parse(query.data.value) as string[]; - } catch { - console.warn("Failed to parse locations", query.data.value); - return []; - } - }, [query.data]); +interface DragItem { + index: number; + title: string; } -export const Locations: React.FC = () => { +function LocationContainer({ + index, + title, + spools, + showDelete, + onDelete, + moveLocation, +}: { + index: number; + title: string; + spools: ISpool[]; + showDelete?: boolean; + onDelete?: () => void; + moveLocation: (dragIndex: number, hoverIndex: number) => void; +}) { + const ref = useRef(null); + const [{ handlerId }, drop] = useDrop({ + accept: ItemTypes.CONTAINER, + collect(monitor) { + return { + handlerId: monitor.getHandlerId(), + }; + }, + hover(item, monitor) { + if (!ref.current) { + return null; + } + const dragIndex = item.index; + const hoverIndex = index; + + // Don't replace items with themselves + if (dragIndex === hoverIndex) { + return; + } + + // Determine rectangle on screen + const hoverBoundingRect = ref.current?.getBoundingClientRect(); + + // Get horizontal middle + const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2; + + // Determine mouse position + const clientOffset = monitor.getClientOffset(); + + // Get pixels to the left + const hoverClientX = (clientOffset as XYCoord).x - hoverBoundingRect.left; + + // Dragging downwards + if (dragIndex < hoverIndex && hoverClientX < hoverMiddleX) { + return; + } + + // Dragging upwards + if (dragIndex > hoverIndex && hoverClientX > hoverMiddleX) { + return; + } + + // Time to actually perform the action + moveLocation(dragIndex, hoverIndex); + + item.index = hoverIndex; + }, + }); + + const [{ isDragging }, drag] = useDrag({ + type: ItemTypes.CONTAINER, + item: () => { + return { title, index }; + }, + collect: (monitor: any) => ({ + isDragging: monitor.isDragging(), + }), + }); + + const opacity = isDragging ? 0 : 1; + drag(drop(ref)); + + return ( +
+

+ {title} + {showDelete &&

+ +
+ ); +} + +function LocationMetaContainer() { const t = useTranslate(); const settingsLocations = useLocations(); @@ -195,18 +262,28 @@ export const Locations: React.FC = () => { return allLocs; }, [spoolLocations, settingsLocations]); + const moveLocation = (dragIndex: number, hoverIndex: number) => { + const newLocs = [...locationsList]; + newLocs.splice(dragIndex, 1); + newLocs.splice(hoverIndex, 0, locationsList[dragIndex]); + console.log("newLocs", newLocs); + setLocationsSetting.mutate(newLocs); + }; + // Create containers - const containers = locationsList.map((loc) => { + const containers = locationsList.map((loc, idx) => { const spools = spoolLocations[loc] ?? []; return ( { setLocationsSetting.mutate(locationsList.filter((l) => l !== loc)); }} + moveLocation={moveLocation} /> ); }); @@ -229,22 +306,28 @@ export const Locations: React.FC = () => { } return ( - -
- {containers} -
-
+
+ {containers} +
+
+
+ ); +} + +export const Locations: React.FC = () => { + return ( + + ); };