Final touches on locations page
This commit is contained in:
41
client/package-lock.json
generated
41
client/package-lock.json
generated
@@ -37,10 +37,12 @@
|
|||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"react-to-print": "^2.15.1",
|
"react-to-print": "^2.15.1",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"vite-plugin-svgr": "^4.3.0"
|
"vite-plugin-svgr": "^4.3.0",
|
||||||
|
"zustand": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@refinedev/cli": "^2.16.39",
|
"@refinedev/cli": "^2.16.39",
|
||||||
|
"@simbathesailor/use-what-changed": "^2.0.0",
|
||||||
"@types/node": "^22.9.1",
|
"@types/node": "^22.9.1",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
@@ -2643,6 +2645,15 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@simbathesailor/use-what-changed": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@simbathesailor/use-what-changed/-/use-what-changed-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ulBNrPSvfho9UN6zS2fii3AsdEcp2fMaKeqUZZeCNPaZbB6aXyTUhpEN9atjMAbu/eyK3AY8L4SYJUG62Ekocw==",
|
||||||
|
"dev": true,
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sindresorhus/is": {
|
"node_modules/@sindresorhus/is": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||||
@@ -10320,6 +10331,34 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zwitch": {
|
"node_modules/zwitch": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz",
|
||||||
|
|||||||
@@ -36,10 +36,12 @@
|
|||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"react-to-print": "^2.15.1",
|
"react-to-print": "^2.15.1",
|
||||||
"uuid": "^11.0.3",
|
"uuid": "^11.0.3",
|
||||||
"vite-plugin-svgr": "^4.3.0"
|
"vite-plugin-svgr": "^4.3.0",
|
||||||
|
"zustand": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@refinedev/cli": "^2.16.39",
|
"@refinedev/cli": "^2.16.39",
|
||||||
|
"@simbathesailor/use-what-changed": "^2.0.0",
|
||||||
"@types/node": "^22.9.1",
|
"@types/node": "^22.9.1",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
|
|||||||
@@ -4,15 +4,11 @@ import { useRef, useState } from "react";
|
|||||||
import { useDrag, useDrop } from "react-dnd";
|
import { useDrag, useDrop } from "react-dnd";
|
||||||
|
|
||||||
import { DeleteOutlined } from "@ant-design/icons";
|
import { DeleteOutlined } from "@ant-design/icons";
|
||||||
|
import { useUpdate } from "@refinedev/core";
|
||||||
import { ISpool } from "../../spools/model";
|
import { ISpool } from "../../spools/model";
|
||||||
import { ItemTypes } from "../itemTypes";
|
import { DragItem, ItemTypes, SpoolDragItem } from "../dnd";
|
||||||
import { SpoolList } from "./spoolList";
|
import { SpoolList } from "./spoolList";
|
||||||
|
|
||||||
interface DragItem {
|
|
||||||
index: number;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Location({
|
export function Location({
|
||||||
index,
|
index,
|
||||||
title,
|
title,
|
||||||
@@ -36,10 +32,24 @@ export function Location({
|
|||||||
}) {
|
}) {
|
||||||
const [editTitle, setEditTitle] = useState(false);
|
const [editTitle, setEditTitle] = useState(false);
|
||||||
const [newTitle, setNewTitle] = useState(title);
|
const [newTitle, setNewTitle] = useState(title);
|
||||||
|
const { mutate: updateSpool } = useUpdate({
|
||||||
|
resource: "spool",
|
||||||
|
mutationMode: "optimistic",
|
||||||
|
successNotification: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const moveSpoolLocation = (spool_id: number, location: string) => {
|
||||||
|
updateSpool({
|
||||||
|
id: spool_id,
|
||||||
|
values: {
|
||||||
|
location: location,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
|
const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
|
||||||
accept: ItemTypes.CONTAINER,
|
accept: [ItemTypes.CONTAINER, ItemTypes.SPOOL],
|
||||||
collect(monitor) {
|
collect(monitor) {
|
||||||
return {
|
return {
|
||||||
handlerId: monitor.getHandlerId(),
|
handlerId: monitor.getHandlerId(),
|
||||||
@@ -49,6 +59,21 @@ export function Location({
|
|||||||
if (!ref.current) {
|
if (!ref.current) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("spool" in item) {
|
||||||
|
// Only allow dropping spools on the container if it's empty.
|
||||||
|
if (spools.length > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spoolitem = item as SpoolDragItem;
|
||||||
|
if (spoolitem.spool.location !== title) {
|
||||||
|
moveSpoolLocation(spoolitem.spool.id, title);
|
||||||
|
spoolitem.spool.location = title;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const dragIndex = item.index;
|
const dragIndex = item.index;
|
||||||
const hoverIndex = index;
|
const hoverIndex = index;
|
||||||
|
|
||||||
@@ -127,12 +152,7 @@ export function Location({
|
|||||||
)}
|
)}
|
||||||
{showDelete && <Button icon={<DeleteOutlined />} size="small" type="text" onClick={onDelete} />}
|
{showDelete && <Button icon={<DeleteOutlined />} size="small" type="text" onClick={onDelete} />}
|
||||||
</h3>
|
</h3>
|
||||||
<SpoolList
|
<SpoolList spools={spools} spoolOrder={locationSpoolOrder} setSpoolOrder={setLocationSpoolOrder} />
|
||||||
location={title}
|
|
||||||
spools={spools}
|
|
||||||
spoolOrder={locationSpoolOrder}
|
|
||||||
setSpoolOrder={setLocationSpoolOrder}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,25 @@ import { Button } from "antd";
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useSetSetting } from "../../../utils/querySettings";
|
import { useSetSetting } from "../../../utils/querySettings";
|
||||||
import { ISpool } from "../../spools/model";
|
import { ISpool } from "../../spools/model";
|
||||||
import { renameSpoolLocation, useLocations, useLocationsSpoolOrders } from "../functions";
|
import { useLocations, useLocationsSpoolOrders, useRenameSpoolLocation } from "../functions";
|
||||||
import { Location } from "./location";
|
import { Location } from "./location";
|
||||||
|
|
||||||
export function LocationContainer() {
|
export function LocationContainer() {
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const invalidate = useInvalidate();
|
const invalidate = useInvalidate();
|
||||||
|
const renameSpoolLocation = useRenameSpoolLocation();
|
||||||
|
|
||||||
const settingsLocations = useLocations();
|
const settingsLocations = useLocations();
|
||||||
const setLocationsSetting = useSetSetting<string[]>("locations");
|
const setLocationsSetting = useSetSetting<string[]>("locations");
|
||||||
|
|
||||||
const locationsSpoolOrders = useLocationsSpoolOrders();
|
const locationsSpoolOrders = useLocationsSpoolOrders();
|
||||||
console.log("locationsSpoolOrders", locationsSpoolOrders);
|
|
||||||
const setLocationsSpoolOrders = useSetSetting<Record<string, number[]>>("locations_spoolorders");
|
const setLocationsSpoolOrders = useSetSetting<Record<string, number[]>>("locations_spoolorders");
|
||||||
|
|
||||||
const { data, isLoading, isError } = useList<ISpool>({
|
const {
|
||||||
|
data: spoolData,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useList<ISpool>({
|
||||||
resource: "spool",
|
resource: "spool",
|
||||||
meta: {
|
meta: {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
@@ -30,12 +34,11 @@ export function LocationContainer() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Grab spools and sort by ID
|
|
||||||
const spools = data?.data ?? [];
|
|
||||||
spools.sort((a, b) => a.id - b.id);
|
|
||||||
|
|
||||||
// Group spools by location
|
// Group spools by location
|
||||||
const spoolLocations = useMemo(() => {
|
const spoolLocations = (() => {
|
||||||
|
const spools = spoolData?.data ?? [];
|
||||||
|
spools.sort((a, b) => a.id - b.id);
|
||||||
|
|
||||||
const grouped: Record<string, ISpool[]> = {};
|
const grouped: Record<string, ISpool[]> = {};
|
||||||
spools.forEach((spool) => {
|
spools.forEach((spool) => {
|
||||||
const loc = spool.location ?? t("spool.no_location");
|
const loc = spool.location ?? t("spool.no_location");
|
||||||
@@ -44,14 +47,32 @@ export function LocationContainer() {
|
|||||||
}
|
}
|
||||||
grouped[loc].push(spool);
|
grouped[loc].push(spool);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sort spools in the locations by the spool order
|
||||||
|
for (const loc of Object.keys(grouped)) {
|
||||||
|
if (!locationsSpoolOrders[loc]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
grouped[loc].sort((a, b) => {
|
||||||
|
let aidx = locationsSpoolOrders[loc].indexOf(a.id);
|
||||||
|
if (aidx === -1) {
|
||||||
|
aidx = 999999;
|
||||||
|
}
|
||||||
|
let bidx = locationsSpoolOrders[loc].indexOf(b.id);
|
||||||
|
if (bidx === -1) {
|
||||||
|
bidx = 999999;
|
||||||
|
}
|
||||||
|
return aidx - bidx;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return grouped;
|
return grouped;
|
||||||
}, [spools]);
|
})();
|
||||||
|
|
||||||
// Create list of locations that's sorted
|
// Create list of locations that's sorted
|
||||||
const locationsList = useMemo(() => {
|
const locationsList = useMemo(() => {
|
||||||
// Start with the locations setting
|
// Start with the locations setting
|
||||||
let allLocs = settingsLocations;
|
let allLocs = settingsLocations;
|
||||||
console.log("settingsLocations", settingsLocations);
|
|
||||||
|
|
||||||
// Add any missing locations from the spools
|
// Add any missing locations from the spools
|
||||||
for (const loc of Object.keys(spoolLocations)) {
|
for (const loc of Object.keys(spoolLocations)) {
|
||||||
@@ -77,8 +98,7 @@ export function LocationContainer() {
|
|||||||
|
|
||||||
// Update all spool locations in the database
|
// Update all spool locations in the database
|
||||||
if (spoolLocations[location] && spoolLocations[location].length > 0) {
|
if (spoolLocations[location] && spoolLocations[location].length > 0) {
|
||||||
await renameSpoolLocation(location, newTitle);
|
renameSpoolLocation.mutate({ old: location, new: newTitle });
|
||||||
await invalidate({ resource: "spool", invalidates: ["list", "detail"] });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the value in the settings
|
// Update the value in the settings
|
||||||
|
|||||||
@@ -1,30 +1,118 @@
|
|||||||
import { useNavigation, useTranslate } from "@refinedev/core";
|
import { useNavigation, useTranslate, useUpdate } from "@refinedev/core";
|
||||||
import { Button, theme } from "antd";
|
import { Button, theme } from "antd";
|
||||||
import { useDrag } from "react-dnd";
|
import type { Identifier, XYCoord } from "dnd-core";
|
||||||
|
import { useDrag, useDrop } from "react-dnd";
|
||||||
|
|
||||||
import { EditOutlined, EyeOutlined } from "@ant-design/icons";
|
import { EditOutlined, EyeOutlined } from "@ant-design/icons";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import SpoolIcon from "../../../components/spoolIcon";
|
import SpoolIcon from "../../../components/spoolIcon";
|
||||||
import { ISpool } from "../../spools/model";
|
import { ISpool } from "../../spools/model";
|
||||||
import { ItemTypes } from "../itemTypes";
|
import { ItemTypes, SpoolDragItem, useCurrentDraggedSpool } from "../dnd";
|
||||||
|
|
||||||
const { useToken } = theme;
|
const { useToken } = theme;
|
||||||
|
|
||||||
export function SpoolCard({ spool }: { spool: ISpool }) {
|
export function SpoolCard({
|
||||||
|
index,
|
||||||
|
spool,
|
||||||
|
moveSpoolOrder,
|
||||||
|
}: {
|
||||||
|
index: number;
|
||||||
|
spool: ISpool;
|
||||||
|
moveSpoolOrder: (dragIndex: number, hoverIndex: number) => void;
|
||||||
|
}) {
|
||||||
const { token } = useToken();
|
const { token } = useToken();
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
const [{ opacity }, dragRef] = useDrag(
|
|
||||||
() => ({
|
|
||||||
type: ItemTypes.SPOOL,
|
|
||||||
item: spool,
|
|
||||||
collect: (monitor) => ({
|
|
||||||
opacity: monitor.isDragging() ? 0.5 : 1,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const { editUrl, showUrl } = useNavigation();
|
const { editUrl, showUrl } = useNavigation();
|
||||||
|
|
||||||
|
// Using a global state for this, because the drag handlers are reset when the spool changes location
|
||||||
|
const { draggedSpoolId, setDraggedSpoolId } = useCurrentDraggedSpool();
|
||||||
|
|
||||||
|
const { mutate: updateSpool } = useUpdate({
|
||||||
|
resource: "spool",
|
||||||
|
mutationMode: "optimistic",
|
||||||
|
successNotification: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const moveSpoolLocation = (spool_id: number, location: string) => {
|
||||||
|
updateSpool({
|
||||||
|
id: spool_id,
|
||||||
|
values: {
|
||||||
|
location: location,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [{ handlerId }, drop] = useDrop<SpoolDragItem, void, { handlerId: Identifier | null }>({
|
||||||
|
accept: ItemTypes.SPOOL,
|
||||||
|
collect(monitor) {
|
||||||
|
return {
|
||||||
|
handlerId: monitor.getHandlerId(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hover(item, monitor) {
|
||||||
|
if (!ref.current || item.spool.id === spool.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.spool.location !== spool.location && spool.location) {
|
||||||
|
moveSpoolLocation(item.spool.id, spool.location);
|
||||||
|
item.spool.location = spool.location;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragIndex = item.index;
|
||||||
|
const hoverIndex = index;
|
||||||
|
|
||||||
|
// Determine rectangle on screen
|
||||||
|
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Get horizontal middle
|
||||||
|
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||||
|
|
||||||
|
// Determine mouse position
|
||||||
|
const clientOffset = monitor.getClientOffset();
|
||||||
|
|
||||||
|
// Get pixels to the top
|
||||||
|
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
|
||||||
|
|
||||||
|
// Dragging downwards
|
||||||
|
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dragging upwards
|
||||||
|
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time to actually perform the action
|
||||||
|
moveSpoolOrder(item.spool.id, hoverIndex);
|
||||||
|
|
||||||
|
item.index = hoverIndex;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [{ isDragging }, drag] = useDrag({
|
||||||
|
type: ItemTypes.SPOOL,
|
||||||
|
item: () => {
|
||||||
|
return { spool, index };
|
||||||
|
},
|
||||||
|
collect: (monitor: any) => ({
|
||||||
|
isDragging: monitor.isDragging(),
|
||||||
|
}),
|
||||||
|
end() {
|
||||||
|
setDraggedSpoolId(-1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDragging) {
|
||||||
|
setDraggedSpoolId(spool.id);
|
||||||
|
}
|
||||||
|
}, [isDragging]);
|
||||||
|
|
||||||
const colorObj = spool.filament.multi_color_hexes
|
const colorObj = spool.filament.multi_color_hexes
|
||||||
? {
|
? {
|
||||||
colors: spool.filament.multi_color_hexes.split(","),
|
colors: spool.filament.multi_color_hexes.split(","),
|
||||||
@@ -39,13 +127,15 @@ export function SpoolCard({ spool }: { spool: ISpool }) {
|
|||||||
filament_name = spool.filament.name ?? spool.filament.id.toString();
|
filament_name = spool.filament.name ?? spool.filament.id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const opacity = draggedSpoolId === spool.id ? 0 : 1;
|
||||||
const style = {
|
const style = {
|
||||||
opacity,
|
opacity,
|
||||||
backgroundColor: token.colorBgContainerDisabled,
|
backgroundColor: token.colorBgContainerDisabled,
|
||||||
};
|
};
|
||||||
|
drag(drop(ref));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="spool" ref={dragRef} style={style}>
|
<div className="spool" ref={ref} style={style} data-handler-id={handlerId}>
|
||||||
<SpoolIcon color={colorObj} />
|
<SpoolIcon color={colorObj} />
|
||||||
<div className="info">
|
<div className="info">
|
||||||
<div className="title">
|
<div className="title">
|
||||||
|
|||||||
@@ -1,39 +1,19 @@
|
|||||||
import { useInvalidate } from "@refinedev/core";
|
|
||||||
import { useDrop } from "react-dnd";
|
|
||||||
|
|
||||||
import { theme } from "antd";
|
import { theme } from "antd";
|
||||||
import { ISpool } from "../../spools/model";
|
import { ISpool } from "../../spools/model";
|
||||||
import { ItemTypes } from "../itemTypes";
|
|
||||||
import { setSpoolLocation } from "./../functions";
|
|
||||||
import { SpoolCard } from "./spoolCard";
|
import { SpoolCard } from "./spoolCard";
|
||||||
|
|
||||||
const { useToken } = theme;
|
const { useToken } = theme;
|
||||||
|
|
||||||
export function SpoolList({
|
export function SpoolList({
|
||||||
location,
|
|
||||||
spools,
|
spools,
|
||||||
spoolOrder,
|
spoolOrder,
|
||||||
setSpoolOrder,
|
setSpoolOrder,
|
||||||
}: {
|
}: {
|
||||||
location: string;
|
|
||||||
spools: ISpool[];
|
spools: ISpool[];
|
||||||
spoolOrder: number[];
|
spoolOrder: number[];
|
||||||
setSpoolOrder: (spoolOrder: number[]) => void;
|
setSpoolOrder: (spoolOrder: number[]) => void;
|
||||||
}) {
|
}) {
|
||||||
const { token } = useToken();
|
const { token } = useToken();
|
||||||
const invalidate = useInvalidate();
|
|
||||||
const [, spoolDrop] = useDrop(() => ({
|
|
||||||
accept: ItemTypes.SPOOL,
|
|
||||||
drop: async (item: ISpool) => {
|
|
||||||
if (item.location === location) return;
|
|
||||||
await setSpoolLocation(item.id, location);
|
|
||||||
await invalidate({
|
|
||||||
resource: "spool",
|
|
||||||
id: item.id,
|
|
||||||
invalidates: ["list", "detail"],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Make sure all spools are in the spoolOrders array
|
// Make sure all spools are in the spoolOrders array
|
||||||
const finalSpoolOrder = [...spoolOrder].filter((id) => spools.find((spool) => spool.id === id)); // Remove any spools that are not in the spools array
|
const finalSpoolOrder = [...spoolOrder].filter((id) => spools.find((spool) => spool.id === id)); // Remove any spools that are not in the spools array
|
||||||
@@ -41,8 +21,23 @@ export function SpoolList({
|
|||||||
if (!finalSpoolOrder.includes(spool.id)) finalSpoolOrder.push(spool.id);
|
if (!finalSpoolOrder.includes(spool.id)) finalSpoolOrder.push(spool.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reorder the spools based on the spoolOrder array
|
const moveSpoolOrder = (spool_id: number, hoverIndex: number) => {
|
||||||
const reorderedSpools = finalSpoolOrder.map((id) => spools.find((spool) => spool.id === id)!);
|
// Move spool spool_id to position hoverIndex
|
||||||
|
let curIdx = finalSpoolOrder.indexOf(spool_id);
|
||||||
|
if (curIdx === -1) {
|
||||||
|
// Spool is missing from spool order array, add it to the end of the array
|
||||||
|
finalSpoolOrder.push(spool_id);
|
||||||
|
curIdx = finalSpoolOrder.length - 1;
|
||||||
|
} else if (curIdx === hoverIndex) {
|
||||||
|
// Spool is already in the right position
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSpoolOrder = [...finalSpoolOrder];
|
||||||
|
newSpoolOrder.splice(curIdx, 1);
|
||||||
|
newSpoolOrder.splice(hoverIndex, 0, finalSpoolOrder[curIdx]);
|
||||||
|
setSpoolOrder(newSpoolOrder);
|
||||||
|
};
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
backgroundColor: token.colorBgContainer,
|
backgroundColor: token.colorBgContainer,
|
||||||
@@ -50,9 +45,9 @@ export function SpoolList({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="loc-spools" ref={spoolDrop} style={style}>
|
<div className="loc-spools" style={style}>
|
||||||
{reorderedSpools.map((spool) => (
|
{spools.map((spool, idx) => (
|
||||||
<SpoolCard key={spool.id} spool={spool} />
|
<SpoolCard key={spool.id} index={idx} spool={spool} moveSpoolOrder={moveSpoolOrder} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
29
client/src/pages/locations/dnd.ts
Normal file
29
client/src/pages/locations/dnd.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { ISpool } from "../spools/model";
|
||||||
|
|
||||||
|
export const ItemTypes = {
|
||||||
|
SPOOL: "spool",
|
||||||
|
CONTAINER: "spool-container",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DragItem {
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpoolDragItem extends DragItem {
|
||||||
|
spool: ISpool;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContainerDragItem extends DragItem {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CurrentDraggedSpool {
|
||||||
|
draggedSpoolId: number;
|
||||||
|
setDraggedSpoolId: (spoolid: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCurrentDraggedSpool = create<CurrentDraggedSpool>((set) => ({
|
||||||
|
draggedSpoolId: -1,
|
||||||
|
setDraggedSpoolId: (spoolid: number) => set({ draggedSpoolId: spoolid }),
|
||||||
|
}));
|
||||||
@@ -1,38 +1,77 @@
|
|||||||
|
import { GetListResponse } from "@refinedev/core";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useGetSetting } from "../../utils/querySettings";
|
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";
|
||||||
|
|
||||||
export async function setSpoolLocation(spool_id: number, location: string | null): Promise<ISpool> {
|
interface LocationRename {
|
||||||
const response = await fetch(getAPIURL() + "/spool/" + spool_id, {
|
old: string;
|
||||||
method: "PATCH",
|
new: string;
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
location: location,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Network response was not ok");
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renameSpoolLocation(location: string, newName: string): Promise<string> {
|
export function useRenameSpoolLocation() {
|
||||||
const response = await fetch(getAPIURL() + "/location/" + location, {
|
const queryClient = useQueryClient();
|
||||||
method: "PATCH",
|
const queryKey = ["default", "spool"];
|
||||||
headers: {
|
const queryKeyList = ["default", "spool", "list"];
|
||||||
"Content-Type": "application/json",
|
|
||||||
|
return useMutation<string, unknown, LocationRename, undefined>({
|
||||||
|
mutationFn: async (value) => {
|
||||||
|
const response = await fetch(getAPIURL() + "/location/" + value.old, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: value.new,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
return await response.text();
|
||||||
|
},
|
||||||
|
onMutate: async (value) => {
|
||||||
|
await queryClient.cancelQueries(queryKeyList);
|
||||||
|
|
||||||
|
// Optimistically update all spools with matching location to the new one
|
||||||
|
queryClient.setQueriesData<GetListResponse<ISpool>>({ queryKey: queryKeyList }, (old) => {
|
||||||
|
if (old) {
|
||||||
|
return {
|
||||||
|
data: old.data.map((spool) => {
|
||||||
|
if (spool.location === value.old) {
|
||||||
|
return { ...spool, location: value.new };
|
||||||
|
}
|
||||||
|
return spool;
|
||||||
|
}),
|
||||||
|
total: old.total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return old;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (_error, value, _context) => {
|
||||||
|
// Mutation failed, reset spools with matching location to the old one
|
||||||
|
queryClient.setQueriesData<GetListResponse<ISpool>>({ queryKey: queryKeyList }, (old) => {
|
||||||
|
if (old) {
|
||||||
|
return {
|
||||||
|
data: old.data.map((spool) => {
|
||||||
|
if (spool.location === value.new) {
|
||||||
|
return { ...spool, location: value.old };
|
||||||
|
}
|
||||||
|
return spool;
|
||||||
|
}),
|
||||||
|
total: old.total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return old;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (_data, _value) => {
|
||||||
|
// Mutation succeeded, refetch
|
||||||
|
queryClient.invalidateQueries(queryKey);
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
|
||||||
name: newName,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Network response was not ok");
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLocations(): string[] {
|
export function useLocations(): string[] {
|
||||||
@@ -42,7 +81,9 @@ export function useLocations(): string[] {
|
|||||||
if (!query.data) return [];
|
if (!query.data) return [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(query.data.value) as string[];
|
let data = (JSON.parse(query.data.value) ?? []) as string[];
|
||||||
|
data = data.filter((location) => location != null && location.length > 0);
|
||||||
|
return data;
|
||||||
} catch {
|
} catch {
|
||||||
console.warn("Failed to parse locations", query.data.value);
|
console.warn("Failed to parse locations", query.data.value);
|
||||||
return [];
|
return [];
|
||||||
@@ -57,7 +98,7 @@ export function useLocationsSpoolOrders(): Record<string, number[]> {
|
|||||||
if (!query.data) return {};
|
if (!query.data) return {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(query.data.value) as Record<string, number[]>;
|
return (JSON.parse(query.data.value) ?? {}) as Record<string, number[]>;
|
||||||
} catch {
|
} catch {
|
||||||
console.warn("Failed to parse locations spool orders", query.data.value);
|
console.warn("Failed to parse locations spool orders", query.data.value);
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export const ItemTypes = {
|
|
||||||
SPOOL: "spool",
|
|
||||||
CONTAINER: "spool-container",
|
|
||||||
};
|
|
||||||
@@ -13,10 +13,18 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
font-size: 21px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loc-container h3 input {
|
||||||
|
font-size: 21px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.loc-container .loc-spools {
|
.loc-container .loc-spools {
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user