Compare commits
13 Commits
e1349cc760
...
a55194ba16
| Author | SHA1 | Date | |
|---|---|---|---|
| a55194ba16 | |||
|
|
cda756557d | ||
|
|
b627e151a8 | ||
|
|
848f5dcc94 | ||
|
|
6adb6bedc2 | ||
|
|
edda1e965a | ||
|
|
7bb0c6b1ec | ||
|
|
9124ce0b86 | ||
|
|
753140ecc3 | ||
|
|
9baf91380a | ||
|
|
9df3f0c91b | ||
|
|
e49976bf8a | ||
|
|
25f78e90e6 |
122
CLAUDE.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Spoolman is a self-hosted web service for managing 3D printer filament spools. It tracks inventory, monitors usage, and integrates with OctoPrint and Klipper/Moonraker.
|
||||
|
||||
**Tech Stack:**
|
||||
- Backend: Python 3.9-3.12, FastAPI, SQLAlchemy 2.0 (async), Alembic
|
||||
- Frontend: React 19, TypeScript, Vite, Ant Design, Refine framework
|
||||
- Databases: SQLite (default), PostgreSQL, MySQL, CockroachDB
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Python (PDM)
|
||||
|
||||
```bash
|
||||
pdm install # Install all dependencies including dev
|
||||
pdm sync --prod --no-editable # Install production dependencies only
|
||||
pdm run app # Run backend server (uvicorn)
|
||||
pdm run itest # Run integration tests (all databases)
|
||||
pdm run docs # Generate API documentation
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd client
|
||||
npm ci # Install dependencies
|
||||
npm run dev # Development server with hot reload
|
||||
npm run build # Production build
|
||||
npm run tsc # Type check only
|
||||
```
|
||||
|
||||
### Linting/Formatting
|
||||
|
||||
```bash
|
||||
black . # Format Python code
|
||||
ruff check . --fix # Lint and auto-fix Python
|
||||
pre-commit run --all-files # Run all pre-commit hooks
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Tests run in Docker containers against multiple database types:
|
||||
|
||||
```bash
|
||||
python tests_integration/run.py sqlite # Single database
|
||||
python tests_integration/run.py sqlite postgres # Multiple databases
|
||||
pdm run itest # All databases
|
||||
```
|
||||
|
||||
Run a single test:
|
||||
```bash
|
||||
pytest tests_integration/tests/spool/test_add.py::test_add_spool_remaining_weight -v
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
|
||||
1. Edit models in `spoolman/database/models.py`
|
||||
2. Run the server to update local SQLite
|
||||
3. `pdm run alembic revision -m "description" --autogenerate`
|
||||
4. Review generated migration, format with Black/Ruff
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend (`/spoolman/`)
|
||||
|
||||
- `main.py` - FastAPI app entry, startup/shutdown, SPA mounting
|
||||
- `env.py` - Environment variable parsing and configuration
|
||||
- `ws.py` - WebSocket manager for real-time updates
|
||||
- `api/v1/` - REST endpoints and WebSocket handlers
|
||||
- `router.py` - Main router with WebSocket root endpoint
|
||||
- `spool.py`, `filament.py`, `vendor.py` - CRUD endpoints per resource
|
||||
- `models.py` - Pydantic response models
|
||||
- `database/` - SQLAlchemy models and async database operations
|
||||
- `models.py` - ORM definitions (Vendor, Filament, Spool, Setting, *Field)
|
||||
- `database.py` - Connection setup, backup logic, migrations
|
||||
|
||||
### Frontend (`/client/src/`)
|
||||
|
||||
- `App.tsx` - Routing and Refine framework setup
|
||||
- `pages/` - Page components (spools/, filaments/, vendors/, settings/)
|
||||
- `components/dataProvider.ts` - REST API integration
|
||||
- `components/liveProvider.ts` - WebSocket live updates
|
||||
|
||||
### Data Model
|
||||
|
||||
```
|
||||
Vendor (1) ──→ (many) Filament (1) ──→ (many) Spool
|
||||
```
|
||||
|
||||
All entities support custom fields via *Field tables (VendorField, FilamentField, SpoolField).
|
||||
|
||||
### API Patterns
|
||||
|
||||
- REST: Standard CRUD at `/api/v1/{resource}`
|
||||
- WebSocket: Real-time events at `WS /api/v1/` or `WS /api/v1/{resource}`
|
||||
- Query params: `?skip=0&limit=50&sort_by=name&sort_order=asc`
|
||||
|
||||
### Key Configuration
|
||||
|
||||
Environment variables (see `.env.example`):
|
||||
- `SPOOLMAN_DB_TYPE` - Database type (sqlite/mysql/postgres/cockroachdb)
|
||||
- `SPOOLMAN_PORT` - Server port (default: 7912)
|
||||
- `SPOOLMAN_DEBUG_MODE` - Enables permissive CORS
|
||||
- `VITE_APIURL` - Frontend API base URL (e.g., `/api/v1`)
|
||||
|
||||
## Code Quality
|
||||
|
||||
- Line length: 120 characters
|
||||
- Python target: 3.9
|
||||
- Pre-commit hooks enforce Black formatting and Ruff linting
|
||||
- Backend is fully async using SQLAlchemy AsyncSession
|
||||
|
||||
## Gitea Repository
|
||||
|
||||
- **URL:** http://192.168.0.5:3022/tonym/spoolman2
|
||||
- **API Token:** 8a04b3cb5dbb54e2d895b707305523c3ad83a945
|
||||
|
||||
This is a fork with UX improvements. Issues are tracked on the Gitea instance above.
|
||||
25
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12-bookworm AS python-builder
|
||||
FROM python:3.12-slim-bookworm AS python-builder
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
@@ -32,28 +32,17 @@ COPY --chown=app:app spoolman /home/app/spoolman/spoolman
|
||||
COPY --chown=app:app alembic.ini /home/app/spoolman/
|
||||
COPY --chown=app:app README.md /home/app/spoolman/
|
||||
|
||||
FROM python:3.12-bookworm AS python-runner
|
||||
FROM python:3.12-slim-bookworm AS python-runner
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/Donkie/Spoolman
|
||||
LABEL org.opencontainers.image.description="Keep track of your inventory of 3D-printer filament spools."
|
||||
LABEL org.opencontainers.image.licenses=MIT
|
||||
|
||||
# Install latest su-exec
|
||||
RUN set -ex; \
|
||||
\
|
||||
curl -o /usr/local/bin/su-exec.c https://raw.githubusercontent.com/ncopa/su-exec/master/su-exec.c; \
|
||||
\
|
||||
fetch_deps='gcc libc-dev'; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends $fetch_deps; \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
gcc -Wall \
|
||||
/usr/local/bin/su-exec.c -o/usr/local/bin/su-exec; \
|
||||
chown root:root /usr/local/bin/su-exec; \
|
||||
chmod 0755 /usr/local/bin/su-exec; \
|
||||
rm /usr/local/bin/su-exec.c; \
|
||||
\
|
||||
apt-get purge -y --auto-remove $fetch_deps
|
||||
# Install gosu for privilege dropping
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gosu \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Add local user so we don't run as root
|
||||
RUN groupmod -g 1000 users \
|
||||
|
||||
1
client/.gitignore
vendored
@@ -7,6 +7,7 @@
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/dev-dist
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
8057
client/package-lock.json
generated
@@ -7,53 +7,52 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"@loadable/component": "^5.16.7",
|
||||
"@refinedev/antd": "^5.46.1",
|
||||
"@refinedev/core": "^4.57.9",
|
||||
"@refinedev/kbar": "^1.3.16",
|
||||
"@refinedev/react-router": "^1.0.1",
|
||||
"@refinedev/simple-rest": "^5.0.10",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-query-devtools": "^4.36.1",
|
||||
"@types/loadable__component": "^5.13.9",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"@refinedev/antd": "^6.0.3",
|
||||
"@refinedev/core": "^5.0.7",
|
||||
"@refinedev/kbar": "^2.0.1",
|
||||
"@refinedev/react-router": "^2.0.3",
|
||||
"@refinedev/simple-rest": "^6.0.1",
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
"@tanstack/react-query-devtools": "^5.91.2",
|
||||
"@types/loadable__component": "^5.13.10",
|
||||
"@types/lodash": "^4.17.21",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@yudiel/react-qr-scanner": "^1.2.10",
|
||||
"antd": "^5.25.4",
|
||||
"axios": "^1.12.2",
|
||||
"@yudiel/react-qr-scanner": "^2.5.0",
|
||||
"axios": "^1.13.2",
|
||||
"html-to-image": "^1.11.13",
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"i18next": "^25.7.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^19.1.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-i18next": "^15.5.2",
|
||||
"react-router": "^7.6.2",
|
||||
"react-to-print": "^3.1.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"zustand": "^5.0.5"
|
||||
"react-dom": "^19.2.3",
|
||||
"react-i18next": "^16.5.1",
|
||||
"react-router": "^7.11.0",
|
||||
"react-to-print": "^3.2.0",
|
||||
"uuid": "^13.0.0",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@refinedev/cli": "^2.16.46",
|
||||
"@refinedev/cli": "^2.16.50",
|
||||
"@simbathesailor/use-what-changed": "^2.0.0",
|
||||
"@types/node": "^22.15.30",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
||||
"@typescript-eslint/parser": "^8.33.1",
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.4.1",
|
||||
"vite-plugin-mkcert": "^1.17.8"
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
||||
"@typescript-eslint/parser": "^8.52.0",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.0",
|
||||
"vite-plugin-mkcert": "^1.17.9",
|
||||
"vite-plugin-pwa": "^1.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "refine dev",
|
||||
|
||||
BIN
client/public/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 768 B |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "Spoolman",
|
||||
"short_name": "Spoolman",
|
||||
"description": "Keep track of your inventory of 3D-printer filament spools.",
|
||||
"icons": [
|
||||
{
|
||||
"purpose": "maskable",
|
||||
"sizes": "512x512",
|
||||
"src": "icon512_maskable.png",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"purpose": "any",
|
||||
"sizes": "512x512",
|
||||
"src": "icon512_rounded.png",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"background_color": "#1F1F1F",
|
||||
"theme_color": "#DC7734",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
BIN
client/public/pwa-192x192.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
client/public/pwa-512x512.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
client/public/pwa-64x64.png
Normal file
|
After Width: | Height: | Size: 710 B |
@@ -90,14 +90,7 @@ function App() {
|
||||
<BrowserRouter basename={getBasePath() + "/"}>
|
||||
<RefineKbarProvider>
|
||||
<ColorModeContextProvider>
|
||||
<ConfigProvider
|
||||
locale={antdLocale}
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "#dc7734",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ConfigProvider locale={antdLocale}>
|
||||
<Refine
|
||||
dataProvider={dataProvider(getAPIURL())}
|
||||
notificationProvider={SpoolmanNotificationProvider}
|
||||
|
||||
@@ -116,8 +116,8 @@ function Column<Obj extends Entity>(
|
||||
if (open && props.onFilterDropdownOpen) {
|
||||
props.onFilterDropdownOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
if (props.dataId) {
|
||||
columnProps.key = props.dataId;
|
||||
}
|
||||
@@ -186,7 +186,7 @@ export function RichColumn<Obj extends Entity>(
|
||||
}
|
||||
|
||||
interface FilteredQueryColumnProps<Obj extends Entity> extends BaseColumnProps<Obj> {
|
||||
filterValueQuery: UseQueryResult<string[] | ColumnFilterItem[]>;
|
||||
filterValueQuery: UseQueryResult<string[] | ColumnFilterItem[], unknown>;
|
||||
allowMultipleFilters?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const dataProvider = (
|
||||
|
||||
if (pagination && pagination.mode == "server") {
|
||||
const pageSize = pagination.pageSize ?? 10;
|
||||
const offset = ((pagination.current ?? 1) - 1) * pageSize;
|
||||
const offset = ((pagination.currentPage ?? 1) - 1) * pageSize;
|
||||
queryParams["limit"] = pageSize;
|
||||
queryParams["offset"] = offset;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownOutlined } from "@ant-design/icons";
|
||||
import type { RefineThemedLayoutV2HeaderProps } from "@refinedev/antd";
|
||||
import type { RefineThemedLayoutHeaderProps } from "@refinedev/antd";
|
||||
import { useGetLocale, useSetLocale } from "@refinedev/core";
|
||||
import { Layout as AntdLayout, Button, Dropdown, MenuProps, Space, Switch, theme } from "antd";
|
||||
import React, { useContext } from "react";
|
||||
@@ -10,7 +10,7 @@ import QRCodeScannerModal from "../qrCodeScanner";
|
||||
|
||||
const { useToken } = theme;
|
||||
|
||||
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({ sticky }) => {
|
||||
export const Header: React.FC<RefineThemedLayoutHeaderProps> = ({ sticky }) => {
|
||||
const { token } = useToken();
|
||||
const locale = useGetLocale();
|
||||
const changeLanguage = useSetLocale();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ThemedLayoutV2, ThemedSiderV2, ThemedTitleV2 } from "@refinedev/antd";
|
||||
import { ThemedLayout, ThemedSider, ThemedTitle } from "@refinedev/antd";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { Button } from "antd";
|
||||
import { Footer } from "antd/es/layout/layout";
|
||||
@@ -51,16 +51,16 @@ interface SpoolmanLayoutProps {
|
||||
}
|
||||
|
||||
export const SpoolmanLayout: React.FC<SpoolmanLayoutProps> = ({ children }) => (
|
||||
<ThemedLayoutV2
|
||||
<ThemedLayout
|
||||
Header={() => <Header sticky />}
|
||||
Sider={() => (
|
||||
<ThemedSiderV2
|
||||
<ThemedSider
|
||||
fixed
|
||||
Title={({ collapsed }) => <ThemedTitleV2 collapsed={collapsed} text="Spoolman" icon={<Logo />} />}
|
||||
Title={({ collapsed }) => <ThemedTitle collapsed={collapsed} text="Spoolman" icon={<Logo />} />}
|
||||
/>
|
||||
)}
|
||||
Footer={() => <SpoolmanFooter />}
|
||||
>
|
||||
{children}
|
||||
</ThemedLayoutV2>
|
||||
</ThemedLayout>
|
||||
);
|
||||
|
||||
@@ -17,12 +17,12 @@ type Props = NumberFieldProps & {
|
||||
*
|
||||
* @see {@link https://refine.dev/docs/ui-frameworks/antd/components/fields/number} for more details.
|
||||
*/
|
||||
export const NumberFieldUnit: React.FC<Props> = ({ value, locale, options, ...rest }) => {
|
||||
export const NumberFieldUnit: React.FC<Props> = ({ value, locale, options, unit }) => {
|
||||
const number = Number(value);
|
||||
|
||||
return (
|
||||
<Text {...rest}>
|
||||
{toLocaleStringSupportsOptions() ? number.toLocaleString(locale, options) : number} {rest.unit}
|
||||
<Text>
|
||||
{toLocaleStringSupportsOptions() ? number.toLocaleString(locale, options) : number} {unit}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CameraOutlined } from "@ant-design/icons";
|
||||
import { useTranslate } from "@refinedev/core";
|
||||
import { QrScanner } from "@yudiel/react-qr-scanner";
|
||||
import { IDetectedBarcode, Scanner } from "@yudiel/react-qr-scanner";
|
||||
import { FloatButton, Modal, Space } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
@@ -11,7 +11,12 @@ const QRCodeScannerModal: React.FC = () => {
|
||||
const t = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onScan = (result: string) => {
|
||||
const onScan = (detectedCodes: IDetectedBarcode[]) => {
|
||||
if (detectedCodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const result = detectedCodes[0].rawValue;
|
||||
|
||||
// Check for the spoolman ID format
|
||||
const match = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/);
|
||||
if (match && match.groups) {
|
||||
@@ -28,31 +33,17 @@ const QRCodeScannerModal: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<FloatButton type="primary" onClick={() => setVisible(true)} icon={<CameraOutlined />} shape="circle" />
|
||||
<Modal open={visible} destroyOnClose onCancel={() => setVisible(false)} footer={null} title={t("scanner.title")}>
|
||||
<Modal open={visible} destroyOnHidden onCancel={() => setVisible(false)} footer={null} title={t("scanner.title")}>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<p>{t("scanner.description")}</p>
|
||||
<QrScanner
|
||||
<Scanner
|
||||
constraints={{
|
||||
facingMode: "environment",
|
||||
}}
|
||||
viewFinder={
|
||||
lastError
|
||||
? () => (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
top: "50%",
|
||||
}}
|
||||
>
|
||||
<p>{lastError}</p>
|
||||
</div>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onDecode={onScan}
|
||||
onError={(error: Error) => {
|
||||
onScan={onScan}
|
||||
formats={["qr_code"]}
|
||||
onError={(err: unknown) => {
|
||||
const error = err as Error;
|
||||
console.error(error);
|
||||
if (error.name === "NotAllowedError") {
|
||||
setLastError(t("scanner.error.notAllowed"));
|
||||
@@ -71,7 +62,20 @@ const QRCodeScannerModal: React.FC = () => {
|
||||
setLastError(t("scanner.error.unknown", { error: error.name }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{lastError && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
top: "50%",
|
||||
}}
|
||||
>
|
||||
<p>{lastError}</p>
|
||||
</div>
|
||||
)}
|
||||
</Scanner>
|
||||
</Space>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const Version: React.FC = () => {
|
||||
return <Spin />;
|
||||
}
|
||||
|
||||
if (infoResult.isError) {
|
||||
if (infoResult.isError || !infoResult.data) {
|
||||
return <span>Unknown</span>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { RefineThemes } from "@refinedev/antd";
|
||||
import { ConfigProvider, theme } from "antd";
|
||||
import { createContext, PropsWithChildren, useEffect, useState } from "react";
|
||||
|
||||
@@ -40,8 +39,10 @@ export const ColorModeContextProvider: React.FC<PropsWithChildren> = ({ children
|
||||
<ConfigProvider
|
||||
// you can change the theme colors here. example: ...RefineThemes.Magenta,
|
||||
theme={{
|
||||
...RefineThemes.Yellow,
|
||||
algorithm: mode === "light" ? defaultAlgorithm : darkAlgorithm,
|
||||
token: {
|
||||
colorPrimary: "#dc7734",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import '@ant-design/v5-patch-for-react-19';
|
||||
import "@ant-design/v5-patch-for-react-19";
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
|
||||
@@ -7,21 +7,21 @@ import utc from "dayjs/plugin/utc";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
ActionsColumn,
|
||||
CustomFieldColumn,
|
||||
DateColumn,
|
||||
FilteredQueryColumn,
|
||||
NumberColumn,
|
||||
RichColumn,
|
||||
SortedColumn,
|
||||
SpoolIconColumn,
|
||||
ActionsColumn,
|
||||
CustomFieldColumn,
|
||||
DateColumn,
|
||||
FilteredQueryColumn,
|
||||
NumberColumn,
|
||||
RichColumn,
|
||||
SortedColumn,
|
||||
SpoolIconColumn,
|
||||
} from "../../components/column";
|
||||
import { useLiveify } from "../../components/liveify";
|
||||
import {
|
||||
useSpoolmanArticleNumbers,
|
||||
useSpoolmanFilamentNames,
|
||||
useSpoolmanMaterials,
|
||||
useSpoolmanVendors,
|
||||
useSpoolmanArticleNumbers,
|
||||
useSpoolmanFilamentNames,
|
||||
useSpoolmanMaterials,
|
||||
useSpoolmanVendors,
|
||||
} from "../../components/otherModels";
|
||||
import { removeUndefined } from "../../utils/filtering";
|
||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||
@@ -88,12 +88,12 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
// To provide the live updates, we use a custom solution (useLiveify) instead of the built-in refine "liveMode" feature.
|
||||
// This is because the built-in feature does not call the liveProvider subscriber with a list of IDs, but instead
|
||||
// calls it with a list of filters, sorters, etc. This means the server-side has to support this, which is quite hard.
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } =
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, currentPage, pageSize, setCurrentPage } =
|
||||
useTable<IFilamentCollapsed>({
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "server",
|
||||
current: initialState.pagination.current,
|
||||
currentPage: initialState.pagination.currentPage,
|
||||
pageSize: initialState.pagination.pageSize,
|
||||
},
|
||||
sorters: {
|
||||
@@ -131,7 +131,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
const tableState: TableState = {
|
||||
sorters,
|
||||
filters,
|
||||
pagination: { current, pageSize },
|
||||
pagination: { currentPage: currentPage, pageSize },
|
||||
showColumns,
|
||||
};
|
||||
useStoreInitialState(namespace, tableState);
|
||||
@@ -175,7 +175,7 @@ export const FilamentList: React.FC<IResourceComponentsProps> = () => {
|
||||
onClick={() => {
|
||||
setFilters([], "replace");
|
||||
setSorters([{ field: "id", order: "asc" }]);
|
||||
setCurrent(1);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
>
|
||||
{t("buttons.clearFilters")}
|
||||
|
||||
@@ -21,10 +21,10 @@ export const FilamentShow: React.FC<IResourceComponentsProps> = () => {
|
||||
const navigate = useNavigate();
|
||||
const extraFields = useGetFields(EntityType.filament);
|
||||
const currencyFormatter = useCurrencyFormatter();
|
||||
const { queryResult } = useShow<IFilament>({
|
||||
const { query } = useShow<IFilament>({
|
||||
liveMode: "auto",
|
||||
});
|
||||
const { data, isLoading } = queryResult;
|
||||
const { data, isLoading } = query;
|
||||
|
||||
const record = data?.data;
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import Title from "antd/es/typography/Title";
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import React, { ReactNode } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import Logo from "../../icon.svg?react";
|
||||
import { ISpool } from "../spools/model";
|
||||
|
||||
@@ -32,7 +32,7 @@ export const Home: React.FC<IResourceComponentsProps> = () => {
|
||||
pagination: { pageSize: 1 },
|
||||
});
|
||||
|
||||
const hasSpools = !spools.data || spools.data.data.length > 0;
|
||||
const hasSpools = !spools.result || spools.result.data.length > 0;
|
||||
|
||||
const ResourceStatsCard = (props: { loading: boolean; value: number; resource: string; icon: ReactNode }) => (
|
||||
<Col xs={12} md={6}>
|
||||
@@ -89,20 +89,20 @@ export const Home: React.FC<IResourceComponentsProps> = () => {
|
||||
<Row justify="center" gutter={[16, 16]} style={{ marginTop: "3em" }}>
|
||||
<ResourceStatsCard
|
||||
resource="spool"
|
||||
value={spools.data?.total || 0}
|
||||
loading={spools.isLoading}
|
||||
value={spools.result?.total || 0}
|
||||
loading={spools.query.isLoading}
|
||||
icon={<FileOutlined />}
|
||||
/>
|
||||
<ResourceStatsCard
|
||||
resource="filament"
|
||||
value={filaments.data?.total || 0}
|
||||
loading={filaments.isLoading}
|
||||
value={filaments.result?.total || 0}
|
||||
loading={filaments.query.isLoading}
|
||||
icon={<HighlightOutlined />}
|
||||
/>
|
||||
<ResourceStatsCard
|
||||
resource="vendor"
|
||||
value={vendors.data?.total || 0}
|
||||
loading={vendors.isLoading}
|
||||
value={vendors.result?.total || 0}
|
||||
loading={vendors.query.isLoading}
|
||||
icon={<UserOutlined />}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
@@ -17,11 +17,7 @@ export function LocationContainer() {
|
||||
const locationsSpoolOrders = useLocationsSpoolOrders();
|
||||
const setLocationsSpoolOrders = useSetSetting<Record<string, number[]>>("locations_spoolorders");
|
||||
|
||||
const {
|
||||
data: spoolData,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useList<ISpool>({
|
||||
const { result: spoolData, query } = useList<ISpool>({
|
||||
resource: "spool",
|
||||
meta: {
|
||||
queryParams: {
|
||||
@@ -32,6 +28,8 @@ export function LocationContainer() {
|
||||
mode: "off",
|
||||
},
|
||||
});
|
||||
const isLoading = query.isLoading;
|
||||
const isError = query.isError;
|
||||
|
||||
// Group spools by location
|
||||
const spoolLocations = (() => {
|
||||
|
||||
@@ -34,7 +34,9 @@ export function useRenameSpoolLocation() {
|
||||
return await response.text();
|
||||
},
|
||||
onMutate: async (value) => {
|
||||
await queryClient.cancelQueries(queryKeyList);
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: queryKeyList
|
||||
});
|
||||
|
||||
// Optimistically update all spools with matching location to the new one
|
||||
queryClient.setQueriesData<GetListResponse<ISpool>>({ queryKey: queryKeyList }, (old) => {
|
||||
@@ -71,7 +73,9 @@ export function useRenameSpoolLocation() {
|
||||
},
|
||||
onSuccess: (_data, _value) => {
|
||||
// Mutation succeeded, refetch
|
||||
queryClient.invalidateQueries(queryKey);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKey
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ const SpoolSelectModal: React.FC<Props> = ({ description, onContinue }) => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { tableProps, sorters, filters, current, pageSize } = useTable<ISpoolCollapsed>({
|
||||
const { tableProps, sorters, filters, currentPage, pageSize } = useTable<ISpoolCollapsed>({
|
||||
resource: "spool",
|
||||
meta: {
|
||||
queryParams: {
|
||||
@@ -52,7 +52,7 @@ const SpoolSelectModal: React.FC<Props> = ({ description, onContinue }) => {
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "off",
|
||||
current: 1,
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
sorters: {
|
||||
@@ -75,7 +75,7 @@ const SpoolSelectModal: React.FC<Props> = ({ description, onContinue }) => {
|
||||
const tableState: TableState = {
|
||||
sorters,
|
||||
filters,
|
||||
pagination: { current, pageSize },
|
||||
pagination: { currentPage: currentPage, pageSize },
|
||||
};
|
||||
|
||||
// Collapse the dataSource to a mutable list and add a filament_name field
|
||||
|
||||
@@ -105,7 +105,7 @@ export function GeneralSettings() {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
|
||||
<Button type="primary" htmlType="submit" loading={settings.isFetching || setCurrency.isLoading}>
|
||||
<Button type="primary" htmlType="submit" loading={settings.isFetching || setCurrency.isPending}>
|
||||
{t("buttons.save")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
@@ -131,8 +131,8 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
|
||||
const [usedWeight, setUsedWeight] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const newFilamentWeight = selectedFilament?.weight || 0;
|
||||
const newSpoolWeight = selectedFilament?.spool_weight || 0;
|
||||
const newFilamentWeight = getFilamentWeight();
|
||||
const newSpoolWeight = getSpoolWeight();
|
||||
if (newFilamentWeight > 0) {
|
||||
form.setFieldValue("initial_weight", newFilamentWeight);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useSelect, useTranslate } from "@refinedev/core";
|
||||
import { useQueries } from "@tanstack/react-query";
|
||||
import { Form, InputNumber, Modal, Radio } from "antd";
|
||||
import { useForm } from "antd/es/form/Form";
|
||||
import type { InputNumberRef } from "rc-input-number";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { formatLength, formatWeight } from "../../utils/parsing";
|
||||
import { SpoolType, useGetExternalDBFilaments } from "../../utils/queryExternalDB";
|
||||
@@ -63,7 +64,6 @@ export async function useSpoolFilamentMeasure(spool: ISpool, weight: number) {
|
||||
await fetch(request, init);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an array of queries using the useQueries hook from @tanstack/react-query.
|
||||
* Each query fetches a spool by its ID from the server.
|
||||
@@ -126,7 +126,7 @@ interface SelectOption {
|
||||
export function useGetFilamentSelectOptions() {
|
||||
// Setup hooks
|
||||
const t = useTranslate();
|
||||
const { queryResult: internalFilaments } = useSelect<IFilament>({
|
||||
const { query: internalFilaments } = useSelect<IFilament>({
|
||||
resource: "filament",
|
||||
});
|
||||
const externalFilaments = useGetExternalDBFilaments();
|
||||
@@ -201,7 +201,7 @@ export function useSpoolAdjustModal() {
|
||||
|
||||
const [curSpool, setCurSpool] = useState<ISpool | null>(null);
|
||||
const [measurementType, setMeasurementType] = useState<MeasurementType>("length");
|
||||
const inputNumberRef = useRef<HTMLInputElement | null>(null);
|
||||
const inputNumberRef = useRef<InputNumberRef | null>(null);
|
||||
|
||||
const openSpoolAdjustModal = useCallback((spool: ISpool) => {
|
||||
setCurSpool(spool);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
FilterOutlined,
|
||||
InboxOutlined,
|
||||
PlusSquareOutlined,
|
||||
PrinterOutlined,
|
||||
ToolOutlined,
|
||||
ToTopOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
FilterOutlined,
|
||||
InboxOutlined,
|
||||
PlusSquareOutlined,
|
||||
PrinterOutlined,
|
||||
ToolOutlined,
|
||||
ToTopOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { List, useTable } from "@refinedev/antd";
|
||||
import { IResourceComponentsProps, useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
|
||||
@@ -16,22 +16,22 @@ import utc from "dayjs/plugin/utc";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
Action,
|
||||
ActionsColumn,
|
||||
CustomFieldColumn,
|
||||
DateColumn,
|
||||
FilteredQueryColumn,
|
||||
NumberColumn,
|
||||
RichColumn,
|
||||
SortedColumn,
|
||||
SpoolIconColumn,
|
||||
Action,
|
||||
ActionsColumn,
|
||||
CustomFieldColumn,
|
||||
DateColumn,
|
||||
FilteredQueryColumn,
|
||||
NumberColumn,
|
||||
RichColumn,
|
||||
SortedColumn,
|
||||
SpoolIconColumn,
|
||||
} from "../../components/column";
|
||||
import { useLiveify } from "../../components/liveify";
|
||||
import {
|
||||
useSpoolmanFilamentFilter,
|
||||
useSpoolmanLocations,
|
||||
useSpoolmanLotNumbers,
|
||||
useSpoolmanMaterials,
|
||||
useSpoolmanFilamentFilter,
|
||||
useSpoolmanLocations,
|
||||
useSpoolmanLotNumbers,
|
||||
useSpoolmanMaterials,
|
||||
} from "../../components/otherModels";
|
||||
import { removeUndefined } from "../../utils/filtering";
|
||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||
@@ -117,7 +117,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
// To provide the live updates, we use a custom solution (useLiveify) instead of the built-in refine "liveMode" feature.
|
||||
// This is because the built-in feature does not call the liveProvider subscriber with a list of IDs, but instead
|
||||
// calls it with a list of filters, sorters, etc. This means the server-side has to support this, which is quite hard.
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } =
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, currentPage, pageSize, setCurrentPage } =
|
||||
useTable<ISpoolCollapsed>({
|
||||
meta: {
|
||||
queryParams: {
|
||||
@@ -127,7 +127,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "server",
|
||||
current: initialState.pagination.current,
|
||||
currentPage: initialState.pagination.currentPage,
|
||||
pageSize: initialState.pagination.pageSize,
|
||||
},
|
||||
sorters: {
|
||||
@@ -165,7 +165,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
const tableState: TableState = {
|
||||
sorters,
|
||||
filters,
|
||||
pagination: { current, pageSize },
|
||||
pagination: { currentPage: currentPage, pageSize },
|
||||
showColumns,
|
||||
};
|
||||
useStoreInitialState(namespace, tableState);
|
||||
@@ -284,7 +284,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
|
||||
onClick={() => {
|
||||
setFilters([], "replace");
|
||||
setSorters([{ field: "id", order: "asc" }]);
|
||||
setCurrent(1);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
>
|
||||
{t("buttons.clearFilters")}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getBasePath } from "../../utils/url";
|
||||
import { InboxOutlined, PrinterOutlined, ToTopOutlined, ToolOutlined } from "@ant-design/icons";
|
||||
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
|
||||
import { IResourceComponentsProps, useInvalidate, useShow, useTranslate } from "@refinedev/core";
|
||||
@@ -12,6 +11,7 @@ import SpoolIcon from "../../components/spoolIcon";
|
||||
import { enrichText } from "../../utils/parsing";
|
||||
import { EntityType, useGetFields } from "../../utils/queryFields";
|
||||
import { useCurrencyFormatter } from "../../utils/settings";
|
||||
import { getBasePath } from "../../utils/url";
|
||||
import { IFilament } from "../filaments/model";
|
||||
import { setSpoolArchived, useSpoolAdjustModal } from "./functions";
|
||||
import { ISpool } from "./model";
|
||||
@@ -27,10 +27,10 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
|
||||
const currencyFormatter = useCurrencyFormatter();
|
||||
const invalidate = useInvalidate();
|
||||
|
||||
const { queryResult } = useShow<ISpool>({
|
||||
const { query } = useShow<ISpool>({
|
||||
liveMode: "auto",
|
||||
});
|
||||
const { data, isLoading } = queryResult;
|
||||
const { data, isLoading } = query;
|
||||
|
||||
const record = data?.data;
|
||||
|
||||
@@ -107,9 +107,9 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
|
||||
|
||||
const colorObj = record?.filament.multi_color_hexes
|
||||
? {
|
||||
colors: record.filament.multi_color_hexes.split(","),
|
||||
vertical: record.filament.multi_color_direction === "longitudinal",
|
||||
}
|
||||
colors: record.filament.multi_color_hexes.split(","),
|
||||
vertical: record.filament.multi_color_direction === "longitudinal",
|
||||
}
|
||||
: record?.filament.color_hex;
|
||||
|
||||
return (
|
||||
@@ -118,17 +118,19 @@ export const SpoolShow: React.FC<IResourceComponentsProps> = () => {
|
||||
title={record ? formatTitle(record) : ""}
|
||||
headerButtons={({ defaultButtons }) => (
|
||||
<>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ToolOutlined />}
|
||||
onClick={() => record && openSpoolAdjustModal(record)}
|
||||
>
|
||||
<Button type="primary" icon={<ToolOutlined />} onClick={() => record && openSpoolAdjustModal(record)}>
|
||||
{t("spool.titles.adjust")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PrinterOutlined />}
|
||||
href={getBasePath() + "/spool/print?spools=" + record?.id + "&return=" + encodeURIComponent(window.location.pathname)}
|
||||
href={
|
||||
getBasePath() +
|
||||
"/spool/print?spools=" +
|
||||
record?.id +
|
||||
"&return=" +
|
||||
encodeURIComponent(window.location.pathname)
|
||||
}
|
||||
>
|
||||
{t("printing.qrcode.button")}
|
||||
</Button>
|
||||
|
||||
69
client/src/pages/vendors/list.tsx
vendored
@@ -7,12 +7,12 @@ import utc from "dayjs/plugin/utc";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import {
|
||||
ActionsColumn,
|
||||
CustomFieldColumn,
|
||||
DateColumn,
|
||||
NumberColumn,
|
||||
RichColumn,
|
||||
SortedColumn,
|
||||
ActionsColumn,
|
||||
CustomFieldColumn,
|
||||
DateColumn,
|
||||
NumberColumn,
|
||||
RichColumn,
|
||||
SortedColumn,
|
||||
} from "../../components/column";
|
||||
import { useLiveify } from "../../components/liveify";
|
||||
import { removeUndefined } from "../../utils/filtering";
|
||||
@@ -38,32 +38,33 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
||||
const initialState = useInitialTableState(namespace);
|
||||
|
||||
// Fetch data from the API
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, current, pageSize, setCurrent } = useTable<IVendor>({
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "server",
|
||||
current: initialState.pagination.current,
|
||||
pageSize: initialState.pagination.pageSize,
|
||||
},
|
||||
sorters: {
|
||||
mode: "server",
|
||||
initial: initialState.sorters,
|
||||
},
|
||||
filters: {
|
||||
mode: "server",
|
||||
initial: initialState.filters,
|
||||
},
|
||||
liveMode: "manual",
|
||||
onLiveEvent(event) {
|
||||
if (event.type === "created" || event.type === "deleted") {
|
||||
// updated is handled by the liveify
|
||||
invalidate({
|
||||
resource: "vendor",
|
||||
invalidates: ["list"],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
const { tableProps, sorters, setSorters, filters, setFilters, currentPage, pageSize, setCurrentPage } =
|
||||
useTable<IVendor>({
|
||||
syncWithLocation: false,
|
||||
pagination: {
|
||||
mode: "server",
|
||||
currentPage: initialState.pagination.currentPage,
|
||||
pageSize: initialState.pagination.pageSize,
|
||||
},
|
||||
sorters: {
|
||||
mode: "server",
|
||||
initial: initialState.sorters,
|
||||
},
|
||||
filters: {
|
||||
mode: "server",
|
||||
initial: initialState.filters,
|
||||
},
|
||||
liveMode: "manual",
|
||||
onLiveEvent(event) {
|
||||
if (event.type === "created" || event.type === "deleted") {
|
||||
// updated is handled by the liveify
|
||||
invalidate({
|
||||
resource: "vendor",
|
||||
invalidates: ["list"],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Create state for the columns to show
|
||||
const [showColumns, setShowColumns] = useState<string[]>(initialState.showColumns ?? allColumns);
|
||||
@@ -72,7 +73,7 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
||||
const tableState: TableState = {
|
||||
sorters,
|
||||
filters,
|
||||
pagination: { current, pageSize },
|
||||
pagination: { currentPage, pageSize },
|
||||
showColumns,
|
||||
};
|
||||
useStoreInitialState(namespace, tableState);
|
||||
@@ -117,7 +118,7 @@ export const VendorList: React.FC<IResourceComponentsProps> = () => {
|
||||
onClick={() => {
|
||||
setFilters([], "replace");
|
||||
setSorters([{ field: "id", order: "asc" }]);
|
||||
setCurrent(1);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
>
|
||||
{t("buttons.clearFilters")}
|
||||
|
||||
4
client/src/pages/vendors/show.tsx
vendored
@@ -17,10 +17,10 @@ export const VendorShow: React.FC<IResourceComponentsProps> = () => {
|
||||
const t = useTranslate();
|
||||
const extraFields = useGetFields(EntityType.vendor);
|
||||
|
||||
const { queryResult } = useShow<IVendor>({
|
||||
const { query } = useShow<IVendor>({
|
||||
liveMode: "auto",
|
||||
});
|
||||
const { data, isLoading } = queryResult;
|
||||
const { data, isLoading } = query;
|
||||
|
||||
const record = data?.data;
|
||||
|
||||
|
||||
@@ -66,7 +66,9 @@ export function useSetField(entity_type: EntityType) {
|
||||
},
|
||||
onMutate: async ({ key, params }) => {
|
||||
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
|
||||
await queryClient.cancelQueries(["fields", entity_type]);
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["fields", entity_type]
|
||||
});
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousFields = queryClient.getQueryData<Field[]>(["fields", entity_type]);
|
||||
@@ -101,7 +103,9 @@ export function useSetField(entity_type: EntityType) {
|
||||
},
|
||||
onSettled: () => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries(["fields", entity_type]);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["fields", entity_type]
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -124,7 +128,9 @@ export function useDeleteField(entity_type: EntityType) {
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries(["fields", entity_type]);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["fields", entity_type]
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ export function useSetSetting<T>(key: string) {
|
||||
return response.json();
|
||||
},
|
||||
onMutate: async (value) => {
|
||||
await queryClient.cancelQueries(["settings", key]);
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: ["settings", key]
|
||||
});
|
||||
const previousValue = queryClient.getQueryData<SettingResponseValue>(["settings", key]);
|
||||
queryClient.setQueryData<SettingResponseValue>(["settings", key], (old) =>
|
||||
old ? { ...old, value: JSON.stringify(value) } : undefined
|
||||
@@ -64,7 +66,9 @@ export function useSetSetting<T>(key: string) {
|
||||
},
|
||||
onSuccess: (_data, _value) => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries(["settings", key]);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["settings", key]
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CrudFilter, CrudSort } from "@refinedev/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { isLocalStorageAvailable } from "./support";
|
||||
interface Pagination {
|
||||
current: number;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,55 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
|
||||
export default defineConfig({
|
||||
base: "",
|
||||
plugins: [react(), svgr()],
|
||||
plugins: [
|
||||
react(),
|
||||
svgr(),
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
},
|
||||
includeAssets: ["favicon.ico", "favicon.svg", "apple-touch-icon-180x180.png"],
|
||||
manifest: {
|
||||
name: "Spoolman",
|
||||
short_name: "Spoolman",
|
||||
description: "Keep track of your inventory of 3D-printer filament spools.",
|
||||
icons: [
|
||||
{
|
||||
src: "pwa-64x64.png",
|
||||
sizes: "64x64",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "pwa-192x192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "pwa-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "maskable-icon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
background_color: "#1F1F1F",
|
||||
theme_color: "#DC7734",
|
||||
display: "standalone",
|
||||
start_url: "/",
|
||||
scope: "/",
|
||||
},
|
||||
workbox: {
|
||||
maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -14,4 +14,4 @@ echo User GID: $(id -g app)
|
||||
echo "Starting uvicorn..."
|
||||
|
||||
# Execute the uvicorn command with any additional arguments
|
||||
exec su-exec "app" uvicorn spoolman.main:app --host $SPOOLMAN_HOST --port $SPOOLMAN_PORT "$@"
|
||||
exec gosu "app" uvicorn spoolman.main:app --host $SPOOLMAN_HOST --port $SPOOLMAN_PORT "$@"
|
||||
|
||||
4
pdm.lock
generated
@@ -5,7 +5,7 @@
|
||||
groups = ["default", "dev"]
|
||||
strategy = ["inherit_metadata"]
|
||||
lock_version = "4.5.0"
|
||||
content_hash = "sha256:4bd60f905c3a76c3818c8d812784be141f10cedb41534379bb2ea40723ee9469"
|
||||
content_hash = "sha256:6f14f971b1a5dd3a5a1c02b64c467d1ab5060c7614aef64807f5ce5ee8415fd5"
|
||||
|
||||
[[metadata.targets]]
|
||||
requires_python = ">=3.9,<=3.12"
|
||||
@@ -1177,7 +1177,7 @@ version = "0.21.0"
|
||||
requires_python = ">=3.8.0"
|
||||
summary = "Fast implementation of asyncio event loop on top of libuv"
|
||||
groups = ["default"]
|
||||
marker = "platform_machine != \"armv7l\" and (sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\""
|
||||
marker = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_machine != \"armv7l\" and platform_python_implementation != \"PyPy\""
|
||||
files = [
|
||||
{file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"},
|
||||
{file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"},
|
||||
|
||||
@@ -24,7 +24,7 @@ dependencies = [
|
||||
"httpx~=0.28",
|
||||
"hishel~=0.1",
|
||||
]
|
||||
requires-python = ">=3.9,<=3.12"
|
||||
requires-python = ">=3.9,<3.13"
|
||||
|
||||
[project.license]
|
||||
text = "MIT"
|
||||
|
||||