Added basic locations page to easily move spools between locations
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
HighlightOutlined,
|
||||
HomeOutlined,
|
||||
QuestionOutlined,
|
||||
TableOutlined,
|
||||
ToolOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
@@ -148,6 +149,14 @@ function App() {
|
||||
icon: <UserOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "locations",
|
||||
list: "/locations",
|
||||
meta: {
|
||||
canDelete: false,
|
||||
icon: <TableOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "settings",
|
||||
list: "/settings",
|
||||
@@ -222,6 +231,7 @@ function App() {
|
||||
</Route>
|
||||
<Route path="/settings/*" element={<LoadablePage name="settings" />} />
|
||||
<Route path="/help" element={<LoadablePage name="help" />} />
|
||||
<Route path="/locations" element={<LoadablePage name="locations" />} />
|
||||
<Route path="*" element={<ErrorComponent />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
18
client/src/pages/locations/functions.ts
Normal file
18
client/src/pages/locations/functions.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getAPIURL } from "../../utils/url";
|
||||
import { ISpool } from "../spools/model";
|
||||
|
||||
export async function setSpoolLocation(spool_id: number, location: string | null): Promise<ISpool> {
|
||||
const response = await fetch(getAPIURL() + "/spool/" + spool_id, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
location: location,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
151
client/src/pages/locations/index.tsx
Normal file
151
client/src/pages/locations/index.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { IResourceComponentsProps, useInvalidate, useList, useTranslate } from "@refinedev/core";
|
||||
import { theme } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React from "react";
|
||||
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
|
||||
import SpoolIcon from "../../components/spoolIcon";
|
||||
import { ISpool } from "../spools/model";
|
||||
import { setSpoolLocation } from "./functions";
|
||||
import "./locations.css";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
const { useToken } = theme;
|
||||
|
||||
function SpoolCard({ spool }: { spool: ISpool }) {
|
||||
const { token } = useToken();
|
||||
const [{ opacity }, dragRef] = useDrag(
|
||||
() => ({
|
||||
type: "spool",
|
||||
item: spool,
|
||||
collect: (monitor) => ({
|
||||
opacity: monitor.isDragging() ? 0.5 : 1,
|
||||
}),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const colorObj = spool.filament.multi_color_hexes
|
||||
? {
|
||||
colors: spool.filament.multi_color_hexes.split(","),
|
||||
vertical: spool.filament.multi_color_direction === "longitudinal",
|
||||
}
|
||||
: spool.filament.color_hex || "#000000";
|
||||
|
||||
let filament_name: string;
|
||||
if (spool.filament.vendor && "name" in spool.filament.vendor) {
|
||||
filament_name = `${spool.filament.vendor.name} - ${spool.filament.name}`;
|
||||
} else {
|
||||
filament_name = spool.filament.name ?? spool.filament.id.toString();
|
||||
}
|
||||
|
||||
const style = {
|
||||
opacity,
|
||||
backgroundColor: token.colorBgContainerDisabled,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="spool" ref={dragRef} style={style}>
|
||||
<SpoolIcon color={colorObj} />
|
||||
<div className="info">
|
||||
<div className="title">
|
||||
#{spool.id} {filament_name}
|
||||
</div>
|
||||
<div className="subtitle">
|
||||
{spool.remaining_weight} / {spool.filament.weight} g
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LocationContainer({ title, spools }: { title: string; spools: ISpool[] }) {
|
||||
const { token } = useToken();
|
||||
const invalidate = useInvalidate();
|
||||
|
||||
const [, drop] = useDrop(() => ({
|
||||
accept: "spool",
|
||||
drop: (item: ISpool) => {
|
||||
setSpoolLocation(item.id, title).then(() => {
|
||||
invalidate({
|
||||
resource: "spool",
|
||||
id: item.id,
|
||||
invalidates: ["list", "detail"],
|
||||
});
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const style = {
|
||||
backgroundColor: token.colorBgContainer,
|
||||
borderRadius: token.borderRadiusLG,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="loc-container">
|
||||
<h3>{title}</h3>
|
||||
<div className="loc-spools" ref={drop} style={style}>
|
||||
{spools.map((spool) => (
|
||||
<SpoolCard key={spool.id} spool={spool} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Locations: React.FC<IResourceComponentsProps> = () => {
|
||||
const t = useTranslate();
|
||||
|
||||
const { data, isLoading, isError } = useList<ISpool>({
|
||||
resource: "spool",
|
||||
meta: {
|
||||
queryParams: {
|
||||
["allow_archived"]: false,
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
mode: "off",
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div>Failed to load spools</div>;
|
||||
}
|
||||
|
||||
const spools = data?.data ?? [];
|
||||
spools.sort((a, b) => a.id - b.id);
|
||||
|
||||
// Locations is all the unique locations of the spools
|
||||
const spoolLocations: Record<string, ISpool[]> = {};
|
||||
spools.forEach((spool) => {
|
||||
const loc = spool.location ?? t("spool.no_location");
|
||||
if (!spoolLocations[loc]) {
|
||||
spoolLocations[loc] = [];
|
||||
}
|
||||
spoolLocations[loc].push(spool);
|
||||
});
|
||||
|
||||
const locationsList = Object.keys(spoolLocations);
|
||||
locationsList.sort((a, b) => {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
const containers = locationsList.map((loc) => {
|
||||
return <LocationContainer key={loc} title={loc} spools={spoolLocations[loc]} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<div className="loc-metacontainer">{containers}</div>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Locations;
|
||||
40
client/src/pages/locations/locations.css
Normal file
40
client/src/pages/locations/locations.css
Normal file
@@ -0,0 +1,40 @@
|
||||
.loc-metacontainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.loc-container {
|
||||
padding: 1em;
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.loc-container .loc-spools {
|
||||
padding: 0.2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
max-height: 100em;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.loc-container .spool {
|
||||
padding: 0.5em 0.5em 0.5em 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.loc-container .spool .info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.loc-container .spool .info .title {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.loc-container .spool .info .subtitle {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
Reference in New Issue
Block a user