Merge branch 'locationspage' into master

This commit is contained in:
Donkie
2024-11-24 15:05:01 +01:00
22 changed files with 2423 additions and 875 deletions

2205
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,49 +7,53 @@
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.4.0", "@ant-design/icons": "^5.5.1",
"@loadable/component": "^5.16.4", "@loadable/component": "^5.16.4",
"@refinedev/antd": "^5.42.0", "@refinedev/antd": "^5.44.0",
"@refinedev/core": "^4.53.0", "@refinedev/core": "^4.56.0",
"@refinedev/kbar": "^1.3.12", "@refinedev/kbar": "^1.3.12",
"@refinedev/react-router-v6": "^4.5.11", "@refinedev/react-router-v6": "^4.6.0",
"@refinedev/simple-rest": "^5.0.8", "@refinedev/simple-rest": "^5.0.8",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^4.36.1", "@tanstack/react-query-devtools": "^4.36.1",
"@types/loadable__component": "^5.13.9", "@types/loadable__component": "^5.13.9",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.13",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@yudiel/react-qr-scanner": "^1.2.10", "@yudiel/react-qr-scanner": "^1.2.10",
"antd": "^5.20.0", "antd": "^5.22.1",
"axios": "^1.7.7", "axios": "^1.7.7",
"flag-icons": "^7.2.3", "flag-icons": "^7.2.3",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"i18next": "^23.12.2", "i18next": "^23.16.6",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2", "i18next-http-backend": "^2.6.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^18.3.1", "react": "^18.3.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-i18next": "^15.0.0", "react-i18next": "^15.1.1",
"react-router-dom": "^6.26.0", "react-router-dom": "^6.28.0",
"react-to-print": "^2.15.1", "react-to-print": "^2.15.1",
"uuid": "^10.0.0", "uuid": "^11.0.3",
"vite-plugin-svgr": "^4.2.0" "vite-plugin-svgr": "^4.3.0",
"zustand": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@refinedev/cli": "^2.16.36", "@refinedev/cli": "^2.16.39",
"@types/node": "^20.14.6", "@simbathesailor/use-what-changed": "^2.0.0",
"@types/react": "^18.3.3", "@types/node": "^22.9.1",
"@types/react-dom": "^18.3.0", "@types/react": "^18.3.12",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@types/react-dom": "^18.3.1",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.15.0",
"@vitejs/plugin-react": "^4.3.1", "@typescript-eslint/parser": "^8.15.0",
"@vitejs/plugin-react": "^4.3.3",
"eslint": "^9.8.0", "eslint": "^9.8.0",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.9", "eslint-plugin-react-refresh": "^0.4.9",
"typescript": "^5.5.4", "typescript": "^5.6.3",
"vite": "^5.4.6", "vite": "^5.4.11",
"vite-plugin-mkcert": "^1.17.5" "vite-plugin-mkcert": "^1.17.6"
}, },
"scripts": { "scripts": {
"dev": "refine dev", "dev": "refine dev",

View File

@@ -208,6 +208,9 @@
}, },
"messages": { "messages": {
"archive": "Are you sure you want to archive this spool?" "archive": "Are you sure you want to archive this spool?"
},
"formats": {
"last_used": "Last used {{date}}"
} }
}, },
"filament": { "filament": {
@@ -356,6 +359,9 @@
"help": { "help": {
"list": "Help | Spoolman" "list": "Help | Spoolman"
}, },
"locations": {
"list": "Locations | Spoolman"
},
"filament": { "filament": {
"list": "Filaments | Spoolman", "list": "Filaments | Spoolman",
"show": "#{{id}} Show Filament | Spoolman", "show": "#{{id}} Show Filament | Spoolman",
@@ -377,5 +383,11 @@
"create": "Create Manufacturer | Spoolman", "create": "Create Manufacturer | Spoolman",
"clone": "#{{id}} Clone Manufacturer | Spoolman" "clone": "#{{id}} Clone Manufacturer | Spoolman"
} }
},
"locations": {
"locations": "Locations",
"new_location": "New Location",
"no_location": "No Location",
"no_locations_help": "This page lets you organize your spools in locations, add some spools to get started!"
} }
} }

View File

@@ -210,6 +210,9 @@
"measurement_type_label": "Mättyp", "measurement_type_label": "Mättyp",
"adjust_filament_value": "Mängd att konsumera", "adjust_filament_value": "Mängd att konsumera",
"adjust_filament_help": "Här kan du direkt lägga till eller subtrahera filament från spolen. Ett positivt värde kommer att konsumera filament, ett negativt värde kommer att lägga till det." "adjust_filament_help": "Här kan du direkt lägga till eller subtrahera filament från spolen. Ett positivt värde kommer att konsumera filament, ett negativt värde kommer att lägga till det."
},
"formats": {
"last_used": "Senast använd {{date}}"
} }
}, },
"filament": { "filament": {
@@ -376,5 +379,9 @@
"description": "<title>Hjälp</title><p>Här är några tips för att komma igång.</p><p>Spoolman innehåller 3 olika typer av data:</p><itemsHelp/><p>För att lägga till en ny spole i databasen måste du först skapa ett <filamentCreateLink>Filament</filamentCreateLink>-objekt för den. När det är klart kan du sedan skapa ett <spoolCreateLink>Spol</spoolCreateLink>-objekt för den individuella spolen. Om du sedan köper ytterligare spolar av samma filament kan du bara skapa ytterligare Spol-objekt och återanvända samma Filament-objekt.</p><p>Du kan valfritt också skapa ett <vendorCreateLink>Tillverkar</vendorCreateLink>-objekt för företaget som tillverkar filamentet om du vill spåra den informationen.</p><p>Du kan koppla andra 3D-skrivartjänster till Spoolman, som Moonraker, som sedan automatiskt kan spåra filamentanvändning och uppdatera Spol-objekten åt dig. Se <readmeLink>Spoolman README</readmeLink> för hur du gör det.</p>", "description": "<title>Hjälp</title><p>Här är några tips för att komma igång.</p><p>Spoolman innehåller 3 olika typer av data:</p><itemsHelp/><p>För att lägga till en ny spole i databasen måste du först skapa ett <filamentCreateLink>Filament</filamentCreateLink>-objekt för den. När det är klart kan du sedan skapa ett <spoolCreateLink>Spol</spoolCreateLink>-objekt för den individuella spolen. Om du sedan köper ytterligare spolar av samma filament kan du bara skapa ytterligare Spol-objekt och återanvända samma Filament-objekt.</p><p>Du kan valfritt också skapa ett <vendorCreateLink>Tillverkar</vendorCreateLink>-objekt för företaget som tillverkar filamentet om du vill spåra den informationen.</p><p>Du kan koppla andra 3D-skrivartjänster till Spoolman, som Moonraker, som sedan automatiskt kan spåra filamentanvändning och uppdatera Spol-objekten åt dig. Se <readmeLink>Spoolman README</readmeLink> för hur du gör det.</p>",
"help": "Hjälp" "help": "Hjälp"
}, },
"kofi": "Dricksa mig på Ko-fi" "kofi": "Dricksa mig på Ko-fi",
"locations": {
"new_location": "Ny plats",
"no_location": "Ingen plats"
}
} }

View File

@@ -10,6 +10,7 @@ import {
HighlightOutlined, HighlightOutlined,
HomeOutlined, HomeOutlined,
QuestionOutlined, QuestionOutlined,
TableOutlined,
ToolOutlined, ToolOutlined,
UserOutlined, UserOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
@@ -148,6 +149,14 @@ function App() {
icon: <UserOutlined />, icon: <UserOutlined />,
}, },
}, },
{
name: "locations",
list: "/locations",
meta: {
canDelete: false,
icon: <TableOutlined />,
},
},
{ {
name: "settings", name: "settings",
list: "/settings", list: "/settings",
@@ -222,6 +231,7 @@ function App() {
</Route> </Route>
<Route path="/settings/*" element={<LoadablePage name="settings" />} /> <Route path="/settings/*" element={<LoadablePage name="settings" />} />
<Route path="/help" element={<LoadablePage name="help" />} /> <Route path="/help" element={<LoadablePage name="help" />} />
<Route path="/locations" element={<LoadablePage name="locations" />} />
<Route path="*" element={<ErrorComponent />} /> <Route path="*" element={<ErrorComponent />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -61,8 +61,6 @@ const dataProvider = (
params: queryParams, params: queryParams,
}); });
// console.log(url, requestMethod, queryParams, data, headers)
return { return {
data, data,
total: parseInt(headers["x-total-count"]) ?? 100, total: parseInt(headers["x-total-count"]) ?? 100,

View File

@@ -1,3 +1,4 @@
import dayjs from "dayjs";
import i18n from "i18next"; import i18n from "i18next";
import detector from "i18next-browser-languagedetector"; import detector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend"; import Backend from "i18next-http-backend";
@@ -8,6 +9,7 @@ interface Language {
name: string; name: string;
countryCode: string; countryCode: string;
fullCode: string; fullCode: string;
djs: () => Promise<ILocale>;
} }
/** /**
@@ -16,107 +18,128 @@ interface Language {
* name: Name of the language in the list * name: Name of the language in the list
* countryCode: Country code of the country's flag to display for this language * countryCode: Country code of the country's flag to display for this language
* fullCode: Full language code, used for Ant Design's locale * fullCode: Full language code, used for Ant Design's locale
* djs: Function to load the dayjs locale, see https://github.com/iamkun/dayjs/tree/dev/src/locale for list of locales
*/ */
export const languages: { [key: string]: Language } = { export const languages: { [key: string]: Language } = {
["en"]: { ["en"]: {
name: "English", name: "English",
countryCode: "gb", countryCode: "gb",
fullCode: "en-GB", fullCode: "en-GB",
djs: () => import("dayjs/locale/en"),
}, },
["sv"]: { ["sv"]: {
name: "Svenska", name: "Svenska",
countryCode: "se", countryCode: "se",
fullCode: "sv-SE", fullCode: "sv-SE",
djs: () => import("dayjs/locale/sv"),
}, },
["de"]: { ["de"]: {
name: "Deutsch", name: "Deutsch",
countryCode: "de", countryCode: "de",
fullCode: "de-DE", fullCode: "de-DE",
djs: () => import("dayjs/locale/de"),
}, },
["es"]: { ["es"]: {
name: "Español", name: "Español",
countryCode: "es", countryCode: "es",
fullCode: "es-ES", fullCode: "es-ES",
djs: () => import("dayjs/locale/es"),
}, },
["zh"]: { ["zh"]: {
name: "简体中文", name: "简体中文",
countryCode: "cn", countryCode: "cn",
fullCode: "zh-CN", fullCode: "zh-CN",
djs: () => import("dayjs/locale/zh-cn"),
}, },
["zh-Hant"]: { ["zh-Hant"]: {
name: "繁體中文", name: "繁體中文",
countryCode: "cn", countryCode: "cn",
fullCode: "zh-TW", fullCode: "zh-TW",
djs: () => import("dayjs/locale/zh-hk"),
}, },
["pl"]: { ["pl"]: {
name: "Polski", name: "Polski",
countryCode: "pl", countryCode: "pl",
fullCode: "pl-PL", fullCode: "pl-PL",
djs: () => import("dayjs/locale/pl"),
}, },
["ru"]: { ["ru"]: {
name: "Русский", name: "Русский",
countryCode: "ru", countryCode: "ru",
fullCode: "ru-RU", fullCode: "ru-RU",
djs: () => import("dayjs/locale/ru"),
}, },
["cs"]: { ["cs"]: {
name: "Česky", name: "Česky",
countryCode: "cz", countryCode: "cz",
fullCode: "cs-CZ", fullCode: "cs-CZ",
djs: () => import("dayjs/locale/cs"),
}, },
["nb-NO"]: { ["nb-NO"]: {
name: "Norsk bokmål", name: "Norsk bokmål",
countryCode: "no", countryCode: "no",
fullCode: "nb-NO", fullCode: "nb-NO",
djs: () => import("dayjs/locale/nb"),
}, },
["nl"]: { ["nl"]: {
name: "Nederlands", name: "Nederlands",
countryCode: "nl", countryCode: "nl",
fullCode: "nl-NL", fullCode: "nl-NL",
djs: () => import("dayjs/locale/nl"),
}, },
["fr"]: { ["fr"]: {
name: "Français", name: "Français",
countryCode: "fr", countryCode: "fr",
fullCode: "fr-FR", fullCode: "fr-FR",
djs: () => import("dayjs/locale/fr"),
}, },
["hu"]: { ["hu"]: {
name: "Magyar", name: "Magyar",
countryCode: "hu", countryCode: "hu",
fullCode: "hu-HU", fullCode: "hu-HU",
djs: () => import("dayjs/locale/hu"),
}, },
["it"]: { ["it"]: {
name: "Italiano", name: "Italiano",
countryCode: "it", countryCode: "it",
fullCode: "it-IT", fullCode: "it-IT",
djs: () => import("dayjs/locale/it"),
}, },
["uk"]: { ["uk"]: {
name: "Українська", name: "Українська",
countryCode: "ua", countryCode: "ua",
fullCode: "uk-UA", fullCode: "uk-UA",
djs: () => import("dayjs/locale/uk"),
}, },
["el"]: { ["el"]: {
name: "Ελληνικά", name: "Ελληνικά",
countryCode: "gr", countryCode: "gr",
fullCode: "el-GR", fullCode: "el-GR",
djs: () => import("dayjs/locale/el"),
}, },
["da"]: { ["da"]: {
name: "Dansk", name: "Dansk",
countryCode: "dk", countryCode: "dk",
fullCode: "da-DK", fullCode: "da-DK",
djs: () => import("dayjs/locale/da"),
}, },
["pt"]: { ["pt"]: {
name: "Português", name: "Português",
countryCode: "pt", countryCode: "pt",
fullCode: "pt-PT", fullCode: "pt-PT",
djs: () => import("dayjs/locale/pt"),
}, },
["fa"]: { ["fa"]: {
name: "فارسی", name: "فارسی",
countryCode: "ir", countryCode: "ir",
fullCode: "fa-IR", fullCode: "fa-IR",
djs: () => import("dayjs/locale/fa"),
}, },
["ro"]: { ["ro"]: {
name: "Român", name: "Român",
countryCode: "ro", countryCode: "ro",
fullCode: "ro-RO", fullCode: "ro-RO",
djs: () => import("dayjs/locale/ro"),
}, },
}; };
@@ -134,4 +157,8 @@ i18n
fallbackLng: "en", fallbackLng: "en",
}); });
i18n.on("languageChanged", function (lng) {
languages[lng].djs().then((djs) => dayjs.locale(djs.name));
});
export default i18n; export default i18n;

View File

@@ -52,7 +52,6 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
// Update colorType state // Update colorType state
useEffect(() => { useEffect(() => {
console.log(formProps.initialValues?.multi_color_hexes);
if (formProps.initialValues?.multi_color_hexes) { if (formProps.initialValues?.multi_color_hexes) {
setColorType("multi"); setColorType("multi");
} else { } else {

View File

@@ -0,0 +1,181 @@
import { Button, Input, theme } from "antd";
import type { Identifier, XYCoord } from "dnd-core";
import { useRef, useState } from "react";
import { useDrag, useDrop } from "react-dnd";
import { DeleteOutlined } from "@ant-design/icons";
import { useTranslate, useUpdate } from "@refinedev/core";
import { ISpool } from "../../spools/model";
import { DragItem, ItemTypes, SpoolDragItem } from "../dnd";
import { EMPTYLOC } from "../functions";
import { SpoolList } from "./spoolList";
const { useToken } = theme;
export function Location({
index,
title,
spools,
showDelete,
onDelete,
moveLocation,
onEditTitle,
locationSpoolOrder,
setLocationSpoolOrder,
}: {
index: number;
title: string;
spools: ISpool[];
showDelete?: boolean;
onDelete?: () => void;
moveLocation: (dragIndex: number, hoverIndex: number) => void;
onEditTitle: (newTitle: string) => void;
locationSpoolOrder: number[];
setLocationSpoolOrder: (spoolOrder: number[]) => void;
}) {
const { token } = useToken();
const t = useTranslate();
const [editTitle, setEditTitle] = useState(false);
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 dropTypes = title == EMPTYLOC ? [ItemTypes.SPOOL] : [ItemTypes.CONTAINER, ItemTypes.SPOOL];
const ref = useRef<HTMLDivElement>(null);
const [{ handlerId }, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>({
accept: dropTypes,
collect(monitor) {
return {
handlerId: monitor.getHandlerId(),
};
},
hover(item, monitor) {
if (!ref.current) {
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 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,
canDrag: !editTitle && title != EMPTYLOC,
item: () => {
return { title, index };
},
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
});
const displayTitle = title == EMPTYLOC ? t("locations.no_location") : title;
const opacity = isDragging ? 0 : 1;
drag(drop(ref));
const canEditTitle = title != EMPTYLOC;
const titleStyle = {
color: canEditTitle ? undefined : token.colorTextTertiary,
};
return (
<div
className={"loc-container " + (title != EMPTYLOC ? "grabable" : "")}
ref={ref}
style={{ opacity }}
data-handler-id={handlerId}
>
<h3>
{editTitle ? (
<Input
autoFocus
variant="borderless"
value={newTitle}
onBlur={() => setEditTitle(false)}
onChange={(e) => setNewTitle(e.target.value)}
onPressEnter={() => {
setEditTitle(false);
return onEditTitle(newTitle);
}}
/>
) : (
<span
className={canEditTitle ? "editable" : ""}
onClick={() => {
if (!canEditTitle) return;
setNewTitle(title);
setEditTitle(true);
}}
style={titleStyle}
>
{displayTitle}
</span>
)}
{showDelete && <Button icon={<DeleteOutlined />} size="small" type="text" onClick={onDelete} />}
</h3>
<SpoolList spools={spools} spoolOrder={locationSpoolOrder} setSpoolOrder={setLocationSpoolOrder} />
</div>
);
}

View File

@@ -0,0 +1,200 @@
import { PlusOutlined } from "@ant-design/icons";
import { useList, useTranslate } from "@refinedev/core";
import { Button } from "antd";
import { useEffect, useMemo } from "react";
import { useSetSetting } from "../../../utils/querySettings";
import { ISpool } from "../../spools/model";
import { EMPTYLOC, useLocations, useLocationsSpoolOrders, useRenameSpoolLocation } from "../functions";
import { Location } from "./location";
export function LocationContainer() {
const t = useTranslate();
const renameSpoolLocation = useRenameSpoolLocation();
const settingsLocations = useLocations();
const setLocationsSetting = useSetSetting<string[]>("locations");
const locationsSpoolOrders = useLocationsSpoolOrders();
const setLocationsSpoolOrders = useSetSetting<Record<string, number[]>>("locations_spoolorders");
const {
data: spoolData,
isLoading,
isError,
} = useList<ISpool>({
resource: "spool",
meta: {
queryParams: {
["allow_archived"]: false,
},
},
pagination: {
mode: "off",
},
});
// Group spools by location
const spoolLocations = (() => {
const spools = spoolData?.data ?? [];
spools.sort((a, b) => a.id - b.id);
const grouped: Record<string, ISpool[]> = {};
spools.forEach((spool) => {
const loc = spool.location ?? EMPTYLOC;
if (!grouped[loc]) {
grouped[loc] = [];
}
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;
})();
// Create list of locations that's sorted
const locationsList = useMemo(() => {
// Start with the default loc
let allLocs = [];
if (EMPTYLOC in spoolLocations) {
allLocs.push(EMPTYLOC);
}
// Add from the locations setting
if (settingsLocations) allLocs.push(...settingsLocations);
// Add any missing locations from the spools
for (const loc of Object.keys(spoolLocations)) {
if (loc != EMPTYLOC && !allLocs.includes(loc)) {
allLocs.push(loc);
}
}
return allLocs;
}, [spoolLocations, settingsLocations]);
const moveLocation = (dragIndex: number, hoverIndex: number) => {
const newLocs = [...locationsList];
newLocs.splice(dragIndex, 1);
newLocs.splice(hoverIndex, 0, locationsList[dragIndex]);
setLocationsSetting.mutate(newLocs.filter((loc) => loc != EMPTYLOC));
};
const onEditTitle = async (location: string, newTitle: string) => {
if (location == "") return; // Can't edit the default location
if (newTitle == location) return; // No change
if (newTitle == "") return; // Can't have an empty location
if (locationsList.includes(newTitle)) return; // Location already exists
// Update all spool locations in the database
if (spoolLocations[location] && spoolLocations[location].length > 0) {
renameSpoolLocation.mutate({ old: location, new: newTitle });
}
// Update the value in the settings
const newLocs = [...locationsList].filter((loc) => loc != EMPTYLOC);
newLocs[locationsList.indexOf(location)] = newTitle;
setLocationsSetting.mutate(newLocs);
};
const setLocationSpoolOrder = (location: string, spoolOrder: number[]) => {
setLocationsSpoolOrders.mutate({
...locationsSpoolOrders,
[location]: spoolOrder,
});
};
// Create containers
const containers = locationsList.map((loc, idx) => {
const spools = spoolLocations[loc] ?? [];
const spoolOrder = locationsSpoolOrders[loc] ?? [];
return (
<Location
key={loc}
index={idx}
title={loc}
spools={spools}
showDelete={spools.length == 0}
onDelete={() => {
setLocationsSetting.mutate(locationsList.filter((l) => l !== loc));
}}
moveLocation={moveLocation}
onEditTitle={(newTitle: string) => onEditTitle(loc, newTitle)}
locationSpoolOrder={spoolOrder}
setLocationSpoolOrder={(spoolOrder: number[]) => setLocationSpoolOrder(loc, spoolOrder)}
/>
);
});
// Update locations settings so it always includes all spool locations
useEffect(() => {
// Check if they're not the same
const curLocList = locationsList.filter((l) => l != EMPTYLOC);
if (settingsLocations != null && JSON.stringify(curLocList) !== JSON.stringify(settingsLocations)) {
setLocationsSetting.mutate(curLocList);
}
}, [locationsList, settingsLocations, setLocationsSetting]);
if (isLoading) {
return <div>Loading...</div>;
}
if (isError) {
return <div>Failed to load spools</div>;
}
const addNewLocation = () => {
const baseLocationName = t("locations.new_location");
let newLocationName = baseLocationName;
const newLocs = [...locationsList];
let i = 1;
while (newLocs.includes(newLocationName)) {
newLocationName = baseLocationName + " " + i;
i++;
}
newLocs.push(newLocationName);
setLocationsSetting.mutate(newLocs);
};
return (
<div>
{!isLoading && spoolData.data.length == 0 && (
<div className="no-locations">{t("locations.no_locations_help")}</div>
)}
<div className="loc-metacontainer">
{containers}
<div className="newLocContainer">
<Button
type="dashed"
shape="circle"
icon={<PlusOutlined />}
size="large"
style={{
margin: "1em",
}}
onClick={addNewLocation}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,186 @@
import { useNavigation, useTranslate, useUpdate } from "@refinedev/core";
import { Button, theme } from "antd";
import type { Identifier, XYCoord } from "dnd-core";
import { useDrag, useDrop } from "react-dnd";
import { EditOutlined, EyeOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import utc from "dayjs/plugin/utc";
import { useEffect, useRef } from "react";
import { Link } from "react-router-dom";
import SpoolIcon from "../../../components/spoolIcon";
import { ISpool } from "../../spools/model";
import { ItemTypes, SpoolDragItem, useCurrentDraggedSpool } from "../dnd";
dayjs.extend(utc);
dayjs.extend(relativeTime);
const { useToken } = theme;
export function SpoolCard({
index,
spool,
moveSpoolOrder,
}: {
index: number;
spool: ISpool;
moveSpoolOrder: (dragIndex: number, hoverIndex: number) => void;
}) {
const { token } = useToken();
const t = useTranslate();
const { 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
? {
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 opacity = draggedSpoolId === spool.id ? 0 : 1;
const style = {
opacity,
backgroundColor: token.colorBgContainerDisabled,
};
drag(drop(ref));
function formatSubtitle(spool: ISpool) {
let str = "";
if (spool.filament.material) str += spool.filament.material + " - ";
if (spool.filament.weight) {
const remaining_weight = spool.remaining_weight ?? spool.filament.weight;
str += `${remaining_weight} / ${spool.filament.weight} g`;
}
if (spool.last_used) {
// Format like "last used X time ago"
const dt = dayjs(spool.last_used);
str += ` - ${t("spool.formats.last_used", { date: dt.fromNow() })}`;
}
return str;
}
return (
<div className="spool" ref={ref} style={style} data-handler-id={handlerId}>
<SpoolIcon color={colorObj} />
<div className="info">
<div className="title">
<span>
#{spool.id} {filament_name}
</span>
<div>
<Link to={`/spool/edit/${spool.id}?return=` + encodeURIComponent(window.location.pathname)}>
<Button icon={<EditOutlined />} title={t("buttons.edit")} size="small" type="text" />
</Link>
<Link to={showUrl("spool", spool.id)}>
<Button icon={<EyeOutlined />} title={t("buttons.show")} size="small" type="text" />
</Link>
</div>
</div>
<div
className="subtitle"
style={{
color: token.colorTextSecondary,
}}
>
{formatSubtitle(spool)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,54 @@
import { theme } from "antd";
import { ISpool } from "../../spools/model";
import { SpoolCard } from "./spoolCard";
const { useToken } = theme;
export function SpoolList({
spools,
spoolOrder,
setSpoolOrder,
}: {
spools: ISpool[];
spoolOrder: number[];
setSpoolOrder: (spoolOrder: number[]) => void;
}) {
const { token } = useToken();
// 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
spools.forEach((spool) => {
if (!finalSpoolOrder.includes(spool.id)) finalSpoolOrder.push(spool.id);
});
const moveSpoolOrder = (spool_id: number, hoverIndex: number) => {
// 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 = {
backgroundColor: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
};
return (
<div className="loc-spools" style={style}>
{spools.map((spool, idx) => (
<SpoolCard key={spool.id} index={idx} spool={spool} moveSpoolOrder={moveSpoolOrder} />
))}
</div>
);
}

View 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 }),
}));

View File

@@ -0,0 +1,109 @@
import { GetListResponse } from "@refinedev/core";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useMemo } from "react";
import { useGetSetting } from "../../utils/querySettings";
import { getAPIURL } from "../../utils/url";
import { ISpool } from "../spools/model";
export const EMPTYLOC = "";
interface LocationRename {
old: string;
new: string;
}
export function useRenameSpoolLocation() {
const queryClient = useQueryClient();
const queryKey = ["default", "spool"];
const queryKeyList = ["default", "spool", "list"];
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);
},
});
}
export function useLocations(): string[] | null {
const query = useGetSetting("locations");
return useMemo(() => {
if (!query.data) return null;
try {
let data = (JSON.parse(query.data.value) ?? []) as string[];
data = data.filter((location) => location != null && location.length > 0);
return data;
} catch {
console.warn("Failed to parse locations", query.data.value);
return null;
}
}, [query.data]);
}
export function useLocationsSpoolOrders(): Record<string, number[]> {
const query = useGetSetting("locations_spoolorders");
return useMemo(() => {
if (!query.data) return {};
try {
return (JSON.parse(query.data.value) ?? {}) as Record<string, number[]>;
} catch {
console.warn("Failed to parse locations spool orders", query.data.value);
return {};
}
}, [query.data]);
}

View File

@@ -0,0 +1,25 @@
import { IResourceComponentsProps, useTranslate } from "@refinedev/core";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { LocationContainer } from "./components/locationContainer";
import "./locations.css";
dayjs.extend(utc);
export const Locations: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
return (
<div>
<h1>{t("locations.locations")}</h1>
<DndProvider backend={HTML5Backend}>
<LocationContainer />
</DndProvider>
</div>
);
};
export default Locations;

View File

@@ -0,0 +1,75 @@
.loc-metacontainer {
display: flex;
flex-wrap: wrap;
}
.loc-container {
padding: 1em;
width: 24em;
}
.loc-container.grabable,
.loc-container .spool {
cursor: move;
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
}
.loc-container h3 {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 21px;
width: 100%;
}
.loc-container h3 span {
cursor: default;
}
.loc-container h3 span.editable {
cursor: text;
}
.loc-container h3 input {
font-size: 21px;
margin: 0;
padding: 0;
border: 0;
}
.loc-container .loc-spools {
padding: 0.2em;
display: flex;
flex-direction: column;
gap: 0.5em;
min-height: 3em;
max-height: 50em;
overflow-y: scroll;
}
.loc-container .spool {
padding: 0.5em 0.5em 0.5em 0;
display: flex;
align-items: center;
border-radius: 0.5em;
}
.loc-container .spool .info {
display: flex;
flex-direction: column;
width: 100%;
}
.loc-container .spool .info .title {
font-size: 1em;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.loc-container .spool .info .subtitle {
font-size: 0.8em;
}

View File

@@ -100,17 +100,13 @@ export function renderLabelContents(template: string, spool: ISpool): JSX.Elemen
// Find all {tags} in the template string and loop over them // Find all {tags} in the template string and loop over them
// let matches = [...template.matchAll(/(?:{(.*?))?{(.*?)}(.*?)(?:}(.*?))?/gs)]; // let matches = [...template.matchAll(/(?:{(.*?))?{(.*?)}(.*?)(?:}(.*?))?/gs)];
let matches = [...template.matchAll(/{(?:[^}{]|{[^}{]*})*}/gs)]; let matches = [...template.matchAll(/{(?:[^}{]|{[^}{]*})*}/gs)];
// console.log(matches){(?:[^}{]|{[^}{]*})*}
let label_text = template; let label_text = template;
matches.forEach((match) => { matches.forEach((match) => {
// console.log(match) if ((match[0].match(/{/g) || []).length == 1) {
if ((match[0].match(/{/g)||[]).length == 1) {
let tag = match[0].replace(/[{}]/g, ""); let tag = match[0].replace(/[{}]/g, "");
// console.log(tag) let tagValue = getTagValue(tag, spool);
let tagValue = getTagValue(tag, spool)
label_text = label_text.replace(match[0], tagValue); label_text = label_text.replace(match[0], tagValue);
} } else if ((match[0].match(/{/g) || []).length == 2) {
else if ((match[0].match(/{/g)||[]).length == 2) {
let structure = match[0].match(/{(.*?){(.*?)}(.*?)}/); let structure = match[0].match(/{(.*?){(.*?)}(.*?)}/);
if (structure != null) { if (structure != null) {
const tag = structure[2]; const tag = structure[2];

View File

@@ -5,6 +5,7 @@ import TextArea from "antd/es/input/TextArea";
import { message } from "antd/lib"; import { message } from "antd/lib";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields"; import { ExtraFieldFormItem, ParsedExtras, StringifiedExtras } from "../../components/extraFields";
import { useSpoolmanLocations } from "../../components/otherModels"; import { useSpoolmanLocations } from "../../components/otherModels";
import { searchMatches } from "../../utils/filtering"; import { searchMatches } from "../../utils/filtering";
@@ -12,6 +13,7 @@ import { numberFormatter, numberParser } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields"; import { EntityType, useGetFields } from "../../utils/queryFields";
import { getCurrencySymbol, useCurrency } from "../../utils/settings"; import { getCurrencySymbol, useCurrency } from "../../utils/settings";
import { createFilamentFromExternal } from "../filaments/functions"; import { createFilamentFromExternal } from "../filaments/functions";
import { useLocations } from "../locations/functions";
import { useGetFilamentSelectOptions } from "./functions"; import { useGetFilamentSelectOptions } from "./functions";
import { ISpool, ISpoolParsedExtras, WeightToEnter } from "./model"; import { ISpool, ISpoolParsedExtras, WeightToEnter } from "./model";
@@ -32,6 +34,8 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
const [hasChanged, setHasChanged] = useState(false); const [hasChanged, setHasChanged] = useState(false);
const extraFields = useGetFields(EntityType.spool); const extraFields = useGetFields(EntityType.spool);
const currency = useCurrency(); const currency = useCurrency();
const [searchParams, _] = useSearchParams();
const navigate = useNavigate();
const { form, formProps, saveButtonProps } = useForm<ISpool, HttpError, ISpoolRequest, ISpool>({ const { form, formProps, saveButtonProps } = useForm<ISpool, HttpError, ISpoolRequest, ISpool>({
liveMode: "manual", liveMode: "manual",
@@ -40,6 +44,17 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
messageApi.warning(t("spool.form.spool_updated")); messageApi.warning(t("spool.form.spool_updated"));
setHasChanged(true); setHasChanged(true);
}, },
// Custom redirect logic
redirect: false,
onMutationSuccess: () => {
const returnUrl = searchParams.get("return");
if (returnUrl) {
navigate(returnUrl, { relative: "path" });
} else {
navigate("/spool");
}
},
}); });
const initialWeightValue = Form.useWatch("initial_weight", form); const initialWeightValue = Form.useWatch("initial_weight", form);
@@ -118,7 +133,6 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
useEffect(() => { useEffect(() => {
const newFilamentWeight = selectedFilament?.weight || 0; const newFilamentWeight = selectedFilament?.weight || 0;
const newSpoolWeight = selectedFilament?.spool_weight || 0; const newSpoolWeight = selectedFilament?.spool_weight || 0;
console.log("selectedFilament", selectedFilament, newFilamentWeight, newSpoolWeight);
if (newFilamentWeight > 0) { if (newFilamentWeight > 0) {
form.setFieldValue("initial_weight", newFilamentWeight); form.setFieldValue("initial_weight", newFilamentWeight);
} }
@@ -135,9 +149,15 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
}; };
const locations = useSpoolmanLocations(true); const locations = useSpoolmanLocations(true);
const settingsLocation = useLocations();
const [newLocation, setNewLocation] = useState(""); const [newLocation, setNewLocation] = useState("");
const allLocations = [...(locations.data || [])]; const allLocations = [...(settingsLocation || [])];
locations?.data?.forEach((loc) => {
if (!allLocations.includes(loc)) {
allLocations.push(loc);
}
});
if (newLocation.trim() && !allLocations.includes(newLocation)) { if (newLocation.trim() && !allLocations.includes(newLocation)) {
allLocations.push(newLocation.trim()); allLocations.push(newLocation.trim());
} }

View File

@@ -1,7 +1,7 @@
import {InboxOutlined, PrinterOutlined, ToTopOutlined} from "@ant-design/icons"; import { InboxOutlined, PrinterOutlined, ToTopOutlined } from "@ant-design/icons";
import { DateField, NumberField, Show, TextField } from "@refinedev/antd"; import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
import {IResourceComponentsProps, useInvalidate, useShow, useTranslate} from "@refinedev/core"; import { IResourceComponentsProps, useInvalidate, useShow, useTranslate } from "@refinedev/core";
import {Button, Modal, Typography} from "antd"; import { Button, Modal, Typography } from "antd";
import dayjs from "dayjs"; import dayjs from "dayjs";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import React from "react"; import React from "react";
@@ -12,8 +12,8 @@ import { enrichText } from "../../utils/parsing";
import { EntityType, useGetFields } from "../../utils/queryFields"; import { EntityType, useGetFields } from "../../utils/queryFields";
import { useCurrency } from "../../utils/settings"; import { useCurrency } from "../../utils/settings";
import { IFilament } from "../filaments/model"; import { IFilament } from "../filaments/model";
import { setSpoolArchived } from "./functions";
import { ISpool } from "./model"; import { ISpool } from "./model";
import {setSpoolArchived} from "./functions";
dayjs.extend(utc); dayjs.extend(utc);
@@ -52,9 +52,9 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
}); });
}; };
const archiveSpoolPopup = async (spool: ISpool|undefined) => { const archiveSpoolPopup = async (spool: ISpool | undefined) => {
if (spool === undefined) { if (spool === undefined) {
return return;
} }
// If the spool has no remaining weight, archive it immediately since it's likely not a mistake // If the spool has no remaining weight, archive it immediately since it's likely not a mistake
if (spool.remaining_weight != undefined && spool.remaining_weight <= 0) { if (spool.remaining_weight != undefined && spool.remaining_weight <= 0) {
@@ -123,18 +123,11 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
{t("printing.qrcode.button")} {t("printing.qrcode.button")}
</Button> </Button>
{record?.archived ? ( {record?.archived ? (
<Button <Button icon={<ToTopOutlined />} onClick={() => archiveSpool(record, false)}>
icon={<ToTopOutlined />}
onClick={() => archiveSpool(record, false)}
>
{t("buttons.unArchive")} {t("buttons.unArchive")}
</Button> </Button>
) : ( ) : (
<Button <Button danger icon={<InboxOutlined />} onClick={() => archiveSpoolPopup(record)}>
danger
icon={<InboxOutlined />}
onClick={() => archiveSpoolPopup(record)}
>
{t("buttons.archive")} {t("buttons.archive")}
</Button> </Button>
)} )}

View File

@@ -4,6 +4,7 @@ import logging
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field, RootModel
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from spoolman.database import filament, spool from spoolman.database import filament, spool
@@ -123,3 +124,25 @@ async def find_locations(
db: Annotated[AsyncSession, Depends(get_db_session)], db: Annotated[AsyncSession, Depends(get_db_session)],
) -> list[str]: ) -> list[str]:
return await spool.find_locations(db=db) return await spool.find_locations(db=db)
class RenameLocationBody(BaseModel):
name: str = Field(description="The new name of the location.", min_length=1)
@router.patch(
"/location/{location}",
name="Rename location",
description="Rename a spool location. All spools in this location will be moved to the new location.",
response_model_exclude_none=True,
response_model=RootModel[str],
)
async def rename_location(
location: str,
*,
db: Annotated[AsyncSession, Depends(get_db_session)],
body: RenameLocationBody,
) -> str:
logger.info("Renaming location %s to %s", location, body.name)
await spool.rename_location(db=db, current_name=location, new_name=body.name)
return body.name

View File

@@ -462,3 +462,15 @@ async def reset_initial_weight(db: AsyncSession, spool_id: int, weight: float) -
await db.commit() await db.commit()
await spool_changed(spool, EventType.UPDATED) await spool_changed(spool, EventType.UPDATED)
return spool return spool
async def rename_location(
*,
db: AsyncSession,
current_name: str,
new_name: str,
) -> None:
"""Rename all spools with the current location name to the new name."""
await db.execute(
sqlalchemy.update(models.Spool).where(models.Spool.location == current_name).values(location=new_name),
)

View File

@@ -68,3 +68,6 @@ register_setting("extra_fields_vendor", SettingType.ARRAY, json.dumps([]))
register_setting("extra_fields_filament", SettingType.ARRAY, json.dumps([])) register_setting("extra_fields_filament", SettingType.ARRAY, json.dumps([]))
register_setting("extra_fields_spool", SettingType.ARRAY, json.dumps([])) register_setting("extra_fields_spool", SettingType.ARRAY, json.dumps([]))
register_setting("base_url", SettingType.STRING, json.dumps("")) register_setting("base_url", SettingType.STRING, json.dumps(""))
register_setting("locations", SettingType.ARRAY, json.dumps([]))
register_setting("locations_spoolorders", SettingType.OBJECT, json.dumps({}))