Merge branch 'locationspage' into master
This commit is contained in:
2205
client/package-lock.json
generated
2205
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
@@ -70,4 +74,4 @@
|
|||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -325,13 +324,13 @@ export const FilamentEdit: React.FC<IResourceComponentsProps> = () => {
|
|||||||
<Input maxLength={64} />
|
<Input maxLength={64} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("filament.fields.external_id")}
|
label={t("filament.fields.external_id")}
|
||||||
name={["external_id"]}
|
name={["external_id"]}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input maxLength={64} />
|
<Input maxLength={64} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
181
client/src/pages/locations/components/location.tsx
Normal file
181
client/src/pages/locations/components/location.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
200
client/src/pages/locations/components/locationContainer.tsx
Normal file
200
client/src/pages/locations/components/locationContainer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
186
client/src/pages/locations/components/spoolCard.tsx
Normal file
186
client/src/pages/locations/components/spoolCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
client/src/pages/locations/components/spoolList.tsx
Normal file
54
client/src/pages/locations/components/spoolList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
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 }),
|
||||||
|
}));
|
||||||
109
client/src/pages/locations/functions.ts
Normal file
109
client/src/pages/locations/functions.ts
Normal 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]);
|
||||||
|
}
|
||||||
25
client/src/pages/locations/index.tsx
Normal file
25
client/src/pages/locations/index.tsx
Normal 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;
|
||||||
75
client/src/pages/locations/locations.css
Normal file
75
client/src/pages/locations/locations.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -116,27 +116,20 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
|
|||||||
headerButtons={({ defaultButtons }) => (
|
headerButtons={({ defaultButtons }) => (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PrinterOutlined />}
|
icon={<PrinterOutlined />}
|
||||||
href={"/spool/print?spools=" + record?.id + "&return=" + encodeURIComponent(window.location.pathname)}
|
href={"/spool/print?spools=" + record?.id + "&return=" + encodeURIComponent(window.location.pathname)}
|
||||||
>
|
>
|
||||||
{t("printing.qrcode.button")}
|
{t("printing.qrcode.button")}
|
||||||
</Button>
|
</Button>
|
||||||
{record?.archived ? (
|
{record?.archived ? (
|
||||||
<Button
|
<Button icon={<ToTopOutlined />} onClick={() => archiveSpool(record, false)}>
|
||||||
icon={<ToTopOutlined />}
|
{t("buttons.unArchive")}
|
||||||
onClick={() => archiveSpool(record, false)}
|
</Button>
|
||||||
>
|
|
||||||
{t("buttons.unArchive")}
|
|
||||||
</Button>
|
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button danger icon={<InboxOutlined />} onClick={() => archiveSpoolPopup(record)}>
|
||||||
danger
|
{t("buttons.archive")}
|
||||||
icon={<InboxOutlined />}
|
</Button>
|
||||||
onClick={() => archiveSpoolPopup(record)}
|
|
||||||
>
|
|
||||||
{t("buttons.archive")}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{defaultButtons}
|
{defaultButtons}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
)
|
||||||
|
|||||||
@@ -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({}))
|
||||||
|
|||||||
Reference in New Issue
Block a user