Locations can now be drag-dropped around

This commit is contained in:
Donkie
2024-11-20 20:12:08 +01:00
parent 3dc0082f0e
commit 8d6f2636c5
2 changed files with 159 additions and 59 deletions

View File

@@ -1,3 +1,5 @@
import { useMemo } from "react";
import { useGetSetting } from "../../utils/querySettings";
import { getAPIURL } from "../../utils/url"; import { getAPIURL } from "../../utils/url";
import { ISpool } from "../spools/model"; import { ISpool } from "../spools/model";
@@ -16,3 +18,18 @@ export async function setSpoolLocation(spool_id: number, location: string | null
} }
return response.json(); 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]);
}

View File

@@ -2,20 +2,26 @@ import { IResourceComponentsProps, useInvalidate, useList, useNavigation, useTra
import { Button, theme } from "antd"; import { Button, theme } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; 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 { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend"; import { HTML5Backend } from "react-dnd-html5-backend";
import { DeleteOutlined, EditOutlined, EyeOutlined, PlusOutlined } from "@ant-design/icons"; import { DeleteOutlined, EditOutlined, EyeOutlined, PlusOutlined } from "@ant-design/icons";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import SpoolIcon from "../../components/spoolIcon"; import SpoolIcon from "../../components/spoolIcon";
import { useGetSetting, useSetSetting } from "../../utils/querySettings"; import { useSetSetting } from "../../utils/querySettings";
import { ISpool } from "../spools/model"; import { ISpool } from "../spools/model";
import { setSpoolLocation } from "./functions"; import { setSpoolLocation, useLocations } from "./functions";
import "./locations.css"; import "./locations.css";
dayjs.extend(utc); dayjs.extend(utc);
const ItemTypes = {
SPOOL: "spool",
CONTAINER: "spool-container",
};
const { useToken } = theme; const { useToken } = theme;
function SpoolCard({ spool }: { spool: ISpool }) { function SpoolCard({ spool }: { spool: ISpool }) {
@@ -23,7 +29,7 @@ function SpoolCard({ spool }: { spool: ISpool }) {
const t = useTranslate(); const t = useTranslate();
const [{ opacity }, dragRef] = useDrag( const [{ opacity }, dragRef] = useDrag(
() => ({ () => ({
type: "spool", type: ItemTypes.SPOOL,
item: spool, item: spool,
collect: (monitor) => ({ collect: (monitor) => ({
opacity: monitor.isDragging() ? 0.5 : 1, opacity: monitor.isDragging() ? 0.5 : 1,
@@ -82,24 +88,13 @@ function SpoolCard({ spool }: { spool: ISpool }) {
); );
} }
function LocationContainer({ function SpoolList({ location, spools }: { location: string; spools: ISpool[] }) {
title,
spools,
showDelete,
onDelete,
}: {
title: string;
spools: ISpool[];
showDelete?: boolean;
onDelete?: () => void;
}) {
const { token } = useToken(); const { token } = useToken();
const invalidate = useInvalidate(); const invalidate = useInvalidate();
const [, spoolDrop] = useDrop(() => ({
const [, drop] = useDrop(() => ({ accept: ItemTypes.SPOOL,
accept: "spool",
drop: (item: ISpool) => { drop: (item: ISpool) => {
setSpoolLocation(item.id, title).then(() => { setSpoolLocation(item.id, location).then(() => {
invalidate({ invalidate({
resource: "spool", resource: "spool",
id: item.id, id: item.id,
@@ -115,36 +110,108 @@ function LocationContainer({
}; };
return ( return (
<div className="loc-container"> <div className="loc-spools" ref={spoolDrop} style={style}>
<h3> {spools.map((spool) => (
<span>{title}</span> <SpoolCard key={spool.id} spool={spool} />
{showDelete && <Button icon={<DeleteOutlined />} size="small" type="text" onClick={onDelete} />} ))}
</h3>
<div className="loc-spools" ref={drop} style={style}>
{spools.map((spool) => (
<SpoolCard key={spool.id} spool={spool} />
))}
</div>
</div> </div>
); );
} }
function useLocations(): string[] { interface DragItem {
const query = useGetSetting("locations"); index: number;
title: string;
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]);
} }
export const Locations: React.FC<IResourceComponentsProps> = () => { 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<HTMLDivElement>(null);
const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
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 (
<div className="loc-container" ref={ref} style={{ opacity }} data-handler-id={handlerId}>
<h3>
<span>{title}</span>
{showDelete && <Button icon={<DeleteOutlined />} size="small" type="text" onClick={onDelete} />}
</h3>
<SpoolList location={title} spools={spools} />
</div>
);
}
function LocationMetaContainer() {
const t = useTranslate(); const t = useTranslate();
const settingsLocations = useLocations(); const settingsLocations = useLocations();
@@ -195,18 +262,28 @@ export const Locations: React.FC<IResourceComponentsProps> = () => {
return allLocs; return allLocs;
}, [spoolLocations, settingsLocations]); }, [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 // Create containers
const containers = locationsList.map((loc) => { const containers = locationsList.map((loc, idx) => {
const spools = spoolLocations[loc] ?? []; const spools = spoolLocations[loc] ?? [];
return ( return (
<LocationContainer <LocationContainer
key={loc} key={loc}
index={idx}
title={loc} title={loc}
spools={spools} spools={spools}
showDelete={spools.length == 0} showDelete={spools.length == 0}
onDelete={() => { onDelete={() => {
setLocationsSetting.mutate(locationsList.filter((l) => l !== loc)); setLocationsSetting.mutate(locationsList.filter((l) => l !== loc));
}} }}
moveLocation={moveLocation}
/> />
); );
}); });
@@ -229,22 +306,28 @@ export const Locations: React.FC<IResourceComponentsProps> = () => {
} }
return ( return (
<DndProvider backend={HTML5Backend}> <div className="loc-metacontainer">
<div className="loc-metacontainer"> {containers}
{containers} <div className="newLocContainer">
<div className="newLocContainer"> <Button
<Button type="dashed"
type="dashed" shape="circle"
shape="circle" icon={<PlusOutlined />}
icon={<PlusOutlined />} size="large"
size="large" style={{
style={{ margin: "1em",
margin: "1em", }}
}} onClick={() => setLocationsSetting.mutate([...settingsLocations, "New Location"])}
onClick={() => setLocationsSetting.mutate([...settingsLocations, "New Location"])} />
/>
</div>
</div> </div>
</div>
);
}
export const Locations: React.FC<IResourceComponentsProps> = () => {
return (
<DndProvider backend={HTML5Backend}>
<LocationMetaContainer />
</DndProvider> </DndProvider>
); );
}; };