Merge branch 'qrcode' into master

This commit is contained in:
Donkie
2023-08-08 21:32:41 +02:00
13 changed files with 1575 additions and 5 deletions

View File

@@ -2,9 +2,9 @@ FROM node:16-alpine as client-builder
COPY ./client /client
WORKDIR /client
RUN npm install
RUN npm ci
RUN echo "VITE_APIURL=/api/v1" > .env.production
RUN rm -f .env && echo "VITE_APIURL=/api/v1" > .env.production
RUN npm run build
FROM python:3.11-alpine as python-builder

337
client/package-lock.json generated
View File

@@ -17,6 +17,7 @@
"@refinedev/simple-rest": "^4.5.0",
"@tanstack/react-query": "^4.29.19",
"@types/loadable__component": "^5.13.4",
"@yudiel/react-qr-scanner": "^1.1.10",
"antd": "^5.6.4",
"flag-icons": "^6.7.0",
"i18next": "^23.2.8",
@@ -26,6 +27,7 @@
"react-dom": "^18.2.0",
"react-i18next": "^13.0.1",
"react-router-dom": "^6.14.1",
"react-to-print": "^2.14.13",
"vite-plugin-svgr": "^3.2.0"
},
"devDependencies": {
@@ -40,7 +42,8 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.1",
"typescript": "^5.1.6",
"vite": "^4.4.1"
"vite": "^4.4.1",
"vite-plugin-mkcert": "^1.16.0"
},
"engines": {
"node": "16.x"
@@ -1553,6 +1556,195 @@
"node": ">= 8"
}
},
"node_modules/@octokit/auth-token": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz",
"integrity": "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==",
"dev": true,
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/core": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.4.tgz",
"integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==",
"dev": true,
"dependencies": {
"@octokit/auth-token": "^3.0.0",
"@octokit/graphql": "^5.0.0",
"@octokit/request": "^6.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/endpoint": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz",
"integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==",
"dev": true,
"dependencies": {
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/endpoint/node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@octokit/graphql": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz",
"integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==",
"dev": true,
"dependencies": {
"@octokit/request": "^6.0.0",
"@octokit/types": "^9.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/openapi-types": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.0.0.tgz",
"integrity": "sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==",
"dev": true
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.1.2.tgz",
"integrity": "sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==",
"dev": true,
"dependencies": {
"@octokit/tsconfig": "^1.0.2",
"@octokit/types": "^9.2.3"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@octokit/core": ">=4"
}
},
"node_modules/@octokit/plugin-request-log": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
"integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
"dev": true,
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.2.3.tgz",
"integrity": "sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA==",
"dev": true,
"dependencies": {
"@octokit/types": "^10.0.0"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz",
"integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==",
"dev": true,
"dependencies": {
"@octokit/openapi-types": "^18.0.0"
}
},
"node_modules/@octokit/request": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz",
"integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==",
"dev": true,
"dependencies": {
"@octokit/endpoint": "^7.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^9.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/request-error": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz",
"integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==",
"dev": true,
"dependencies": {
"@octokit/types": "^9.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/request/node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@octokit/rest": {
"version": "19.0.13",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.13.tgz",
"integrity": "sha512-/EzVox5V9gYGdbAI+ovYj3nXQT1TtTHRT+0eZPcuC05UFSWO3mdO9UY1C0i2eLF9Un1ONJkAk+IEtYGAC+TahA==",
"dev": true,
"dependencies": {
"@octokit/core": "^4.2.1",
"@octokit/plugin-paginate-rest": "^6.1.2",
"@octokit/plugin-request-log": "^1.0.4",
"@octokit/plugin-rest-endpoint-methods": "^7.1.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/tsconfig": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@octokit/tsconfig/-/tsconfig-1.0.2.tgz",
"integrity": "sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==",
"dev": true
},
"node_modules/@octokit/types": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz",
"integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==",
"dev": true,
"dependencies": {
"@octokit/openapi-types": "^18.0.0"
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
@@ -2538,6 +2730,38 @@
"vite": "^4.2.0"
}
},
"node_modules/@yudiel/react-qr-scanner": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@yudiel/react-qr-scanner/-/react-qr-scanner-1.1.10.tgz",
"integrity": "sha512-+YIHbnocynkkGgyMK8MMkn1NBnJRwJntdm4shQ+linm4NdGdSAavLYxJYwnk2PixZyVu/GZ3Uc2aM0XWlBq+Zw==",
"dependencies": {
"@zxing/library": "^0.20.0"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
},
"node_modules/@zxing/library": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.20.0.tgz",
"integrity": "sha512-6Ev6rcqVjMakZFIDvbUf0dtpPGeZMTfyxYg4HkVWioWeN7cRcnUWT3bU6sdohc82O1nPXcjq6WiGfXX2Pnit6A==",
"dependencies": {
"ts-custom-error": "^3.2.1"
},
"engines": {
"node": ">= 10.4.0"
},
"optionalDependencies": {
"@zxing/text-encoding": "~0.9.0"
}
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"optional": true
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -2830,6 +3054,12 @@
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -2945,6 +3175,12 @@
}
]
},
"node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"dev": true
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -3522,6 +3758,18 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/comma-separated-tokens": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
@@ -3790,6 +4038,15 @@
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -3799,6 +4056,12 @@
"node": ">= 0.8"
}
},
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"dev": true
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -4926,6 +5189,20 @@
"node": ">=0.10.0"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -7640,6 +7917,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/punycode": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@@ -8487,6 +8770,15 @@
"react-dom": "^16.3.0 || ^17.0.0"
}
},
"node_modules/react-to-print": {
"version": "2.14.13",
"resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-2.14.13.tgz",
"integrity": "sha512-PqUGgTRZvkyBzWgaZHVBECWPX2nGEc3HOUy6WNXof6HT4yTFI92wtIkqQtr4jfvHbadqjwTgzgh6vgN8KXlWWw==",
"peerDependencies": {
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-virtual": {
"version": "2.10.4",
"resolved": "https://registry.npmjs.org/react-virtual/-/react-virtual-2.10.4.tgz",
@@ -9781,6 +10073,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ts-custom-error": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
"integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tslib": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
@@ -9994,6 +10294,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
"dev": true
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -10245,6 +10551,35 @@
}
}
},
"node_modules/vite-plugin-mkcert": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.16.0.tgz",
"integrity": "sha512-5r+g8SB9wZzLNUFekGwZo3e0P6QlS6rbxK5p9z/itxNAimsYohgjK/YfVPVxM9EuglP9hjridq0lUejo9v1nVg==",
"dev": true,
"dependencies": {
"@octokit/rest": "^19.0.5",
"axios": "^1.2.2",
"debug": "^4.3.4",
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=v16.7.0"
},
"peerDependencies": {
"vite": ">=3"
}
},
"node_modules/vite-plugin-mkcert/node_modules/axios": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/vite-plugin-svgr": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-3.2.0.tgz",

View File

@@ -16,6 +16,7 @@
"@refinedev/simple-rest": "^4.5.0",
"@tanstack/react-query": "^4.29.19",
"@types/loadable__component": "^5.13.4",
"@yudiel/react-qr-scanner": "^1.1.10",
"antd": "^5.6.4",
"flag-icons": "^6.7.0",
"i18next": "^23.2.8",
@@ -25,6 +26,7 @@
"react-dom": "^18.2.0",
"react-i18next": "^13.0.1",
"react-router-dom": "^6.14.1",
"react-to-print": "^2.14.13",
"vite-plugin-svgr": "^3.2.0"
},
"devDependencies": {
@@ -39,7 +41,8 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.1",
"typescript": "^5.1.6",
"vite": "^4.4.1"
"vite": "^4.4.1",
"vite-plugin-mkcert": "^1.16.0"
},
"scripts": {
"dev": "refine dev",

View File

@@ -53,6 +53,58 @@
"dashboard": {
"title": "Dashboard"
},
"printing": {
"generic": {
"title": "Printing",
"description": "Tune in the settings below to get the desired print layout. Keep in mind that printers and your OS may apply it's own margins and scaling, so you may need to adjust the settings accordingly. Test it on a piece of regular paper before printing on the actual labels.",
"print": "Print",
"columns": "Columns",
"rows": "Rows",
"paperSize": "Paper Size",
"showBorder": "Show Border",
"previewScale": "Preview Scale",
"marginLeft": "Left Margin",
"marginRight": "Right Margin",
"marginTop": "Top Margin",
"marginBottom": "Bottom Margin",
"blockRowsTop": "Block Rows Top",
"blockRowsBottom": "Block Rows Bottom",
"blockColumnsLeft": "Block Columns Left",
"blockColumnsRight": "Block Columns Right",
"borders": {
"none": "None",
"border": "Border",
"grid": "Grid"
}
},
"qrcode": {
"button": "Print QR Codes",
"title": "QR Code Printing",
"textSize": "Content Text Size",
"showSpoolmanIcon": "Show Spoolman Icon",
"showContent": "Show QR Code Content"
},
"spoolSelect": {
"title": "Select Spools",
"description": "Select spools to print QR codes for.",
"showArchived": "Show Archived",
"noSpoolsSelected": "You have not selected any spools.",
"selectAll": "Select/Unselect All",
"selectedTotal_one": "{{count}} spool selected",
"selectedTotal_other": "{{count}} spools selected"
}
},
"scanner": {
"title": "QR Code Scanner",
"description": "Scan a Spoolman QR code to view details about the spool.",
"error": {
"notAllowed": "You have not allowed access to the camera.",
"insecureContext": "The page is not served over HTTPS.",
"streamApiNotSupported": "The browser does not support the MediaStream API.",
"notReadable": "The camera is not readable.",
"unknown": "An unknown error occurred. ({{error}})"
}
},
"spool": {
"spool": "Spools",
"fields": {

View File

@@ -53,6 +53,58 @@
"dashboard": {
"title": "Hem"
},
"printing": {
"generic": {
"title": "Utskrift",
"description": "Justera inställningarna nedan för att få önskat utskriftslayout. Kom ihåg att skrivare och ditt operativsystem kan tillämpa sina egna marginaler och skalning, så du kan behöva justera inställningarna därefter. Testa på ett vanligt papper innan du skriver ut på de faktiska etiketterna.",
"print": "Skriv ut",
"columns": "Kolumner",
"rows": "Rader",
"paperSize": "Papperstyp",
"showBorder": "Visa ram",
"previewScale": "Skala förhandsgranskning",
"marginLeft": "Marginal - vänster",
"marginRight": "Marginal - höger",
"marginTop": "Marginal - topp",
"marginBottom": "Marginal - botten",
"blockRowsTop": "Blockera rader - topp",
"blockRowsBottom": "Blockera rader - botten",
"blockColumnsLeft": "Blockera kolumner - vänster",
"blockColumnsRight": "Blockera kolumner - höger",
"borders": {
"none": "Ingen",
"border": "Ram",
"grid": "Rutnät"
}
},
"qrcode": {
"button": "Skriv ut QR-koder",
"title": "QR-kod utskrift",
"textSize": "Textstorlek",
"showSpoolmanIcon": "Visa Spoolman-ikon",
"showContent": "Visa QR-kodsinnehåll"
},
"spoolSelect": {
"title": "Välj spolar",
"description": "Välj spolar att skriva ut QR-koder för.",
"showArchived": "Tillåt arkiverade",
"noSpoolsSelected": "Du har inte valt några spolar.",
"selectAll": "Markera/avmarkera alla",
"selectedTotal_one": "{{count}} spole vald",
"selectedTotal_other": "{{count}} spolar valda"
}
},
"scanner": {
"title": "Skanna QR-kod",
"description": "Skanna en QR-kod för att visa information om en spole.",
"error": {
"notAllowed": "Du har inte tillåtit åtkomst till kameran.",
"insecureContext": "Sidan måste använda sig av HTTPS.",
"streamApiNotSupported": "Webbläsaren stödjer inte MediaStream API.",
"notReadable": "Kameran är inte läsbar.",
"unknown": "Ett okänt fel inträffade. ({{error}})"
}
},
"spool": {
"spool": "Spolar",
"fields": {

View File

@@ -15,6 +15,7 @@ import { ColorModeContext } from "../../contexts/color-mode";
import "/node_modules/flag-icons/css/flag-icons.min.css";
import { languages } from "../../i18n";
import QRCodeScannerModal from "../qrCodeScanner";
const { useToken } = theme;
@@ -84,6 +85,7 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = ({
onChange={() => setMode(mode === "light" ? "dark" : "light")}
defaultChecked={mode === "dark"}
/>
<QRCodeScannerModal />
</Space>
</AntdLayout.Header>
);

View File

@@ -0,0 +1,640 @@
import React, { useRef } from "react";
import {
Modal,
Slider,
Button,
Select,
Row,
Col,
Form,
Divider,
RadioChangeEvent,
Radio,
InputNumber,
} from "antd";
import ReactToPrint from "react-to-print";
import { useSavedState } from "../../utils/saveload";
import { useTranslate } from "@refinedev/core";
interface PrintingDialogProps {
items: JSX.Element[];
style?: string;
extraSettings?: JSX.Element;
visible: boolean;
onCancel: () => void;
title?: string;
}
interface PaperDimensions {
width: number;
height: number;
}
const paperDimensions: { [key: string]: PaperDimensions } = {
A3: {
width: 297,
height: 420,
},
A4: {
width: 210,
height: 297,
},
A5: {
width: 148,
height: 210,
},
Letter: {
width: 216,
height: 279,
},
Legal: {
width: 216,
height: 356,
},
Tabloid: {
width: 279,
height: 432,
},
};
const PrintingDialog: React.FC<PrintingDialogProps> = ({
items,
style,
extraSettings,
visible,
onCancel,
title,
}) => {
const t = useTranslate();
const [marginLeft, setMarginLeft] = useSavedState("print-marginLeft", 10);
const [marginTop, setMarginTop] = useSavedState("print-marginTop", 10);
const [marginRight, setMarginRight] = useSavedState("print-marginRight", 10);
const [marginBottom, setMarginBottom] = useSavedState(
"print-marginBottom",
10
);
const [blockRowsTop, setBlockRowsTop] = useSavedState(
"print-blockRowsTop",
0
);
const [blockRowsBottom, setBlockRowsBottom] = useSavedState(
"print-blockRowsBottom",
0
);
const [blockColumnsLeft, setBlockColumnsLeft] = useSavedState(
"print-blockColumnsLeft",
0
);
const [blockColumnsRight, setBlockColumnsRight] = useSavedState(
"print-blockColumnsRight",
0
);
const [paperColumns, setPaperColumns] = useSavedState("print-itemsPerRow", 3);
const [paperRows, setPaperRows] = useSavedState("print-rowsPerPage", 8);
const [paperSize, setPaperSize] = useSavedState("print-paperSize", "A4");
const [previewScale, setPreviewScale] = useSavedState(
"print-previewScale",
0.6
);
const [borderShowMode, setBorderShowMode] = useSavedState<
"none" | "border" | "grid"
>("print-borderShowMode", "grid");
const paperWidth = paperDimensions[paperSize].width;
const paperHeight = paperDimensions[paperSize].height;
const printRef = useRef<HTMLDivElement>(null);
const calculatedRowHeight =
(paperHeight - marginTop - marginBottom) / paperRows;
const itemsPerRow = paperColumns - blockColumnsLeft - blockColumnsRight;
const rowsPerPage = paperRows - blockRowsTop - blockRowsBottom;
const rowsOfItems = [];
for (let row_idx = 0; row_idx <= items.length / itemsPerRow; row_idx += 1) {
rowsOfItems.push(
items.slice(row_idx * itemsPerRow, (row_idx + 1) * itemsPerRow)
);
}
const pageBlocks = [];
for (
let page_idx = 0;
page_idx <= rowsOfItems.length / rowsPerPage;
page_idx += 1
) {
pageBlocks.push(
rowsOfItems.slice(page_idx * rowsPerPage, (page_idx + 1) * rowsPerPage)
);
}
const pages = pageBlocks.map(function (rows, idx) {
const itemRowsBlockTop = [...Array(blockRowsTop).keys()].map((row) => (
<tr key={row}>
<td>
<div style={{ height: `${calculatedRowHeight}mm` }}></div>
</td>
</tr>
));
const itemRows = rows.map((row, idx) => {
const itemColumnsBlockLeft = [...Array(blockColumnsLeft).keys()].map(
(col) => (
<td key={col}>
<div
style={{
width: `${
(paperWidth - marginLeft - marginRight) / paperColumns
}mm`,
}}
></div>
</td>
)
);
return (
<tr key={idx}>
{itemColumnsBlockLeft}
{row.map((item, index) => (
<td>
<div
key={index}
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: `${
(paperWidth - marginLeft - marginRight) / paperColumns
}mm`,
height: `${calculatedRowHeight}mm`,
flexDirection: "column",
border: borderShowMode === "grid" ? "1px solid #000" : "none",
}}
>
{item}
</div>
</td>
))}
</tr>
);
});
return (
<div
className="print-page"
key={idx}
style={{
width: `${paperWidth}mm`,
height: `${paperHeight}mm`,
backgroundColor: "#FFF",
overflow: "hidden",
}}
>
<div
className="print-page-area"
style={{
border: borderShowMode !== "none" ? "1px solid #000" : "none",
height: `${paperHeight - marginTop - marginBottom}mm`,
width: `${paperWidth - marginLeft - marginRight}mm`,
marginTop: `calc(${marginTop}mm - ${
borderShowMode !== "none" ? "1px" : "0px"
})`,
marginLeft: `calc(${marginLeft}mm - ${
borderShowMode !== "none" ? "1px" : "0px"
})`,
marginRight: `calc(${marginRight}mm - ${
borderShowMode !== "none" ? "1px" : "0px"
})`,
marginBottom: `calc(${marginBottom}mm - ${
borderShowMode !== "none" ? "1px" : "0px"
})`,
}}
>
<table
style={{
alignContent: "flex-start",
}}
>
{itemRowsBlockTop}
{itemRows}
</table>
</div>
</div>
);
});
return (
<Modal
open={visible}
title={title ?? t("printing.generic.title")}
onCancel={onCancel}
footer={[
<ReactToPrint
key="print-button"
trigger={() => (
<Button type="primary">{t("printing.generic.print")}</Button>
)}
content={() => printRef.current}
/>,
]}
width={1200} // Set the modal width to accommodate the preview
>
<Row gutter={16}>
<Col span={24}>{t("printing.generic.description")}</Col>
<Col span={14}>
<div
style={{
transform: "translateZ(0)",
overflow: "hidden scroll",
height: "70vh",
}}
>
<div
className="print-container"
ref={printRef}
style={{
transform: `scale(${previewScale})`,
transformOrigin: "top left",
display: "block",
}}
>
<style>
{`
@media print {
@page {
size: auto;
margin: 0;
}
.print-container {
transform: scale(1) !important;
}
.print-page {
page-break-before: auto;
}
}
@media screen {
.print-page {
margin-top: 10mm;
}
}
.print-page td {
padding: 0;
}
${style ?? ""}
`}
</style>
{pages}
</div>
</div>
</Col>
<Col span={10}>
<Form
labelAlign="left"
colon={false}
labelWrap={true}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
>
<Form.Item label={t("printing.generic.marginLeft")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginLeft}
onChange={(value) => {
setMarginLeft(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginLeft}
addonAfter="mm"
onChange={(value) => {
setMarginLeft(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.marginTop")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginTop}
onChange={(value) => {
setMarginTop(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginTop}
addonAfter="mm"
onChange={(value) => {
setMarginTop(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.marginRight")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginRight}
onChange={(value) => {
setMarginRight(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginRight}
addonAfter="mm"
onChange={(value) => {
setMarginRight(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.marginBottom")}>
<Row>
<Col span={12}>
<Slider
min={-20}
max={50}
step={0.1}
tooltip={{ formatter: (value) => `${value} mm` }}
value={marginBottom}
onChange={(value) => {
setMarginBottom(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
step={0.1}
style={{ margin: "0 16px" }}
value={marginBottom}
addonAfter="mm"
onChange={(value) => {
setMarginBottom(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.columns")}>
<Row>
<Col span={12}>
<Slider
min={1}
max={5}
value={paperColumns}
onChange={(value) => {
if (value - blockColumnsLeft - blockColumnsRight < 1)
return;
setPaperColumns(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={1}
style={{ margin: "0 16px" }}
value={paperColumns}
onChange={(value) => {
if (
(value ?? 1) - blockColumnsLeft - blockColumnsRight <
1
)
return;
setPaperColumns(value ?? 1);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.rows")}>
<Row>
<Col span={12}>
<Slider
min={1}
max={15}
value={paperRows}
onChange={(value) => {
if (value - blockRowsTop - blockRowsBottom < 1) return;
setPaperRows(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={1}
style={{ margin: "0 16px" }}
value={paperRows}
onChange={(value) => {
if ((value ?? 1) - blockRowsTop - blockRowsBottom < 1)
return;
setPaperRows(value ?? 1);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.paperSize")}>
<Select
value={paperSize}
onChange={(value) => setPaperSize(value)}
>
{Object.keys(paperDimensions).map((key) => (
<Select.Option key={key} value={key}>
{key}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label={t("printing.generic.showBorder")}>
<Radio.Group
options={[
{ label: t("printing.generic.borders.none"), value: "none" },
{
label: t("printing.generic.borders.border"),
value: "border",
},
{ label: t("printing.generic.borders.grid"), value: "grid" },
]}
onChange={(e: RadioChangeEvent) => {
setBorderShowMode(e.target.value);
}}
value={borderShowMode}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item label={t("printing.generic.blockRowsTop")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={3}
value={blockRowsTop}
onChange={(value) => {
if (paperRows - value - blockRowsBottom < 1) return;
setBlockRowsTop(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0}
style={{ margin: "0 16px" }}
value={blockRowsTop}
onChange={(value) => {
if (paperRows - (value ?? 0) - blockRowsBottom < 1)
return;
setBlockRowsTop(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.blockRowsBottom")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={3}
value={blockRowsBottom}
onChange={(value) => {
if (paperRows - value - blockRowsTop < 1) return;
setBlockRowsBottom(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0}
style={{ margin: "0 16px" }}
value={blockRowsBottom}
onChange={(value) => {
if (paperRows - (value ?? 0) - blockRowsTop < 1) return;
setBlockRowsBottom(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.blockColumnsLeft")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={3}
value={blockColumnsLeft}
onChange={(value) => {
if (paperColumns - value - blockColumnsRight < 1) return;
setBlockColumnsLeft(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0}
style={{ margin: "0 16px" }}
value={blockColumnsLeft}
onChange={(value) => {
if (paperColumns - (value ?? 0) - blockColumnsRight < 1)
return;
setBlockColumnsLeft(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.generic.blockColumnsRight")}>
<Row>
<Col span={12}>
<Slider
min={0}
max={3}
value={blockColumnsRight}
onChange={(value) => {
if (paperColumns - value - blockColumnsLeft < 1) return;
setBlockColumnsRight(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0}
style={{ margin: "0 16px" }}
value={blockColumnsRight}
onChange={(value) => {
if (paperColumns - (value ?? 0) - blockColumnsLeft < 1)
return;
setBlockColumnsRight(value ?? 0);
}}
/>
</Col>
</Row>
</Form.Item>
{extraSettings && <Divider />}
{extraSettings}
<Divider />
<Form.Item label={t("printing.generic.previewScale")}>
<Row>
<Col span={12}>
<Slider
min={0.1}
max={1}
step={0.01}
value={previewScale}
onChange={(value) => {
setPreviewScale(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
min={0.1}
max={1}
step={0.01}
style={{ margin: "0 16px" }}
value={previewScale}
onChange={(value) => {
setPreviewScale(value ?? 0.1);
}}
/>
</Col>
</Row>
</Form.Item>
</Form>
</Col>
</Row>
</Modal>
);
};
export default PrintingDialog;

View File

@@ -0,0 +1,130 @@
import { Col, Form, InputNumber, QRCode, Row, Slider, Switch } from "antd";
import { useSavedState } from "../../utils/saveload";
import PrintingDialog from "./printingDialog";
import { useTranslate } from "@refinedev/core";
interface QRCodeData {
value: string;
label?: string;
errorLevel?: "L" | "M" | "Q" | "H";
}
interface QRCodePrintingDialogProps {
visible: boolean;
items: QRCodeData[];
onCancel: () => void;
}
const QRCodePrintingDialog: React.FC<QRCodePrintingDialogProps> = ({
visible,
items,
onCancel,
}) => {
const t = useTranslate();
const [showContent, setShowContent] = useSavedState(
"print-showContent",
true
);
const [textSize, setTextSize] = useSavedState("print-textSize", 5);
const [showSpoolmanIcon, setShowSpoolmanIcon] = useSavedState(
"print-showSpoolmanIcon",
true
);
const elements = items.map((item) => {
return (
<>
<QRCode
className="print-qrcode"
icon={showSpoolmanIcon ? "/favicon.ico" : undefined}
value={item.value}
errorLevel={item.errorLevel}
type="svg"
color="#000"
/>
{showContent && (
<div
className="print-qrcode-title"
style={{ textAlign: "center", color: "#000" }}
>
{item.label ?? item.value}
</div>
)}
</>
);
});
return (
<PrintingDialog
visible={visible}
title={t("printing.qrcode.title")}
items={elements}
extraSettings={
<>
<Form.Item label={t("printing.qrcode.showContent")}>
<Switch
checked={showContent}
onChange={(checked) => setShowContent(checked)}
/>
</Form.Item>
<Form.Item label={t("printing.qrcode.textSize")}>
<Row>
<Col span={12}>
<Slider
disabled={!showContent}
tooltip={{ formatter: (value) => `${value} mm` }}
min={3}
max={15}
value={textSize}
step={0.1}
onChange={(value) => {
setTextSize(value);
}}
/>
</Col>
<Col span={12}>
<InputNumber
disabled={!showContent}
min={0.01}
step={0.1}
style={{ margin: "0 16px" }}
value={textSize}
addonAfter="mm"
onChange={(value) => {
setTextSize(value ?? 5);
}}
/>
</Col>
</Row>
</Form.Item>
<Form.Item label={t("printing.qrcode.showSpoolmanIcon")}>
<Switch
checked={showSpoolmanIcon}
onChange={(checked) => setShowSpoolmanIcon(checked)}
/>
</Form.Item>
</>
}
style={`
.print-page .print-qrcode {
height: 100% !important;
width: 100% !important;
}
.print-page .print-qrcode-title {
font-size: ${textSize}mm;
}
.print-page svg {
display: block;
height: 100%;
width: auto;
}
`}
onCancel={onCancel}
/>
);
};
export default QRCodePrintingDialog;

View File

@@ -0,0 +1,82 @@
import React, { useState } from "react";
import { FloatButton, Modal, Space } from "antd";
import { QrScanner } from "@yudiel/react-qr-scanner";
import { useNavigate } from "react-router-dom";
import { CameraOutlined } from "@ant-design/icons";
import { useTranslate } from "@refinedev/core";
const QRCodeScannerModal: React.FC = () => {
const [visible, setVisible] = useState(false);
const [lastError, setLastError] = useState<string | null>(null);
const t = useTranslate();
const navigate = useNavigate();
const onScan = (result: string) => {
// Check for the spoolman ID format
const match = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/);
if (match && match.groups) {
setVisible(false);
navigate(`/spool/show/${match.groups.id}`);
}
};
return (
<>
<FloatButton
type="primary"
onClick={() => setVisible(true)}
icon={<CameraOutlined />}
shape="circle"
/>
<Modal
open={visible}
onCancel={() => setVisible(false)}
footer={null}
title={t("scanner.title")}
>
<Space direction="vertical" style={{ width: "100%" }}>
<p>{t("scanner.description")}</p>
<QrScanner
viewFinder={
lastError
? () => (
<div
style={{
position: "absolute",
textAlign: "center",
width: "100%",
top: "50%",
}}
>
<p>{lastError}</p>
</div>
)
: undefined
}
onDecode={onScan}
onError={(error: Error) => {
console.error(error);
if (error.name === "NotAllowedError") {
setLastError(t("scanner.error.notAllowed"));
} else if (
error.name === "InsecureContextError" ||
(location.protocol !== "https:" &&
navigator.mediaDevices === undefined)
) {
setLastError(t("scanner.error.insecureContext"));
} else if (error.name === "StreamApiNotSupportedError") {
setLastError(t("scanner.error.streamApiNotSupported"));
} else if (error.name === "NotReadableError") {
setLastError(t("scanner.error.notReadable"));
} else {
setLastError(t("scanner.error.unknown", { error: error.name }));
}
}}
/>
</Space>
</Modal>
</>
);
};
export default QRCodeScannerModal;

View File

@@ -0,0 +1,53 @@
// React FC that combines the functionality of first selecting what spools to print QR code for, and then opens up the QR code printing dialog.
import React from "react";
import SpoolSelectModal from "./spoolSelectModal";
import QRCodePrintingDialog from "./printing/qrCodePrintingDialog";
import { Button } from "antd";
import { ISpool } from "../pages/spools/model";
import { PrinterOutlined } from "@ant-design/icons";
import { useTranslate } from "@refinedev/core";
const SelectAndPrint: React.FC = () => {
const t = useTranslate();
const [step, setStep] = React.useState(0);
const [selectedSpools, setSelectedSpools] = React.useState<ISpool[]>([]);
return (
<>
<Button
type="primary"
icon={<PrinterOutlined />}
onClick={() => {
setStep(1);
}}
>
{t("printing.qrcode.button")}
</Button>
<SpoolSelectModal
visible={step === 1}
description={t("printing.spoolSelect.description")}
onCancel={() => {
setStep(0);
}}
onContinue={(spools) => {
setSelectedSpools(spools);
setStep(2);
}}
/>
<QRCodePrintingDialog
visible={step === 2}
onCancel={() => {
setStep(1);
}}
items={selectedSpools.map((spool) => ({
value: `web+spoolman:s-${spool.id}`,
label: `s-${spool.id}`,
errorLevel: "H",
}))}
/>
</>
);
};
export default SelectAndPrint;

View File

@@ -0,0 +1,217 @@
import React, { useState } from "react";
import { Modal, Table, Checkbox, Space, Row, Col, message } from "antd";
import { ISpool } from "../pages/spools/model";
import { FilteredColumn, SortedColumn, SpoolIconColumn } from "./column";
import { TableState } from "../utils/saveload";
import { useTable } from "@refinedev/antd";
import { genericSorter, typeSorters } from "../utils/sorting";
import { genericFilterer, typeFilters } from "../utils/filtering";
import { t } from "i18next";
interface Props {
visible: boolean;
description?: string;
onCancel: () => void;
onContinue: (selectedSpools: ISpool[]) => void;
}
interface ISpoolCollapsed extends ISpool {
filament_name: string;
material?: string;
}
const SpoolSelectModal: React.FC<Props> = ({
visible,
description,
onCancel,
onContinue,
}) => {
const [selectedItems, setSelectedItems] = useState<number[]>([]);
const [showArchived, setShowArchived] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const { tableProps, sorters, filters, current, pageSize } = useTable<ISpool>({
meta: {
queryParams: {
["allow_archived"]: showArchived,
},
},
syncWithLocation: false,
pagination: {
mode: "off",
current: 1,
pageSize: 10,
},
sorters: {
mode: "off",
},
filters: {
mode: "off",
},
});
// Store state in local storage
const tableState: TableState = {
sorters,
filters,
pagination: { current, pageSize },
};
// Collapse the dataSource to a mutable list and add a filament_name field
const dataSource: ISpoolCollapsed[] = React.useMemo(
() =>
(tableProps.dataSource ?? []).map((element) => {
let filament_name: string;
if (element.filament.vendor && "name" in element.filament.vendor) {
filament_name = `${element.filament.vendor.name} - ${element.filament.name}`;
} else {
filament_name =
element.filament.name ?? element.filament.id.toString();
}
return {
...element,
filament_name,
material: element.filament.material,
};
}),
[tableProps.dataSource]
);
// Type the sorters and filters
const typedSorters = typeSorters<ISpoolCollapsed>(sorters);
const typedFilters = typeFilters<ISpoolCollapsed>(filters);
// Filter and sort the dataSource
const filteredDataSource = React.useMemo(() => {
const filtered = dataSource.filter(genericFilterer(typedFilters));
filtered.sort(genericSorter(typedSorters));
return filtered;
}, [dataSource, typedFilters, typedSorters]);
// Function to add/remove all filtered items from selected items
const selectUnselectFiltered = (select: boolean) => {
setSelectedItems((prevSelected) => {
const filtered = filteredDataSource
.map((spool) => spool.id)
.filter((spool) => !prevSelected.includes(spool));
return select ? [...prevSelected, ...filtered] : filtered;
});
};
// Handler for selecting/unselecting individual items
const handleSelectItem = (item: number) => {
setSelectedItems((prevSelected) =>
prevSelected.includes(item)
? prevSelected.filter((selected) => selected !== item)
: [...prevSelected, item]
);
};
// State for the select/unselect all checkbox
const isAllFilteredSelected = filteredDataSource.every((spool) =>
selectedItems.includes(spool.id)
);
return (
<Modal
title={t("printing.spoolSelect.title")}
open={visible}
onCancel={onCancel}
onOk={() => {
if (selectedItems.length === 0) {
messageApi.open({
type: "error",
content: t("printing.spoolSelect.noSpoolsSelected"),
});
return;
}
onContinue(
dataSource.filter((spool) => selectedItems.includes(spool.id))
);
}}
width={600}
>
{contextHolder}
<Space direction="vertical" style={{ width: "100%" }}>
{description && <div>{description}</div>}
<Table
{...tableProps}
rowKey="id"
dataSource={filteredDataSource}
pagination={false}
scroll={{ x: "max-content", y: 200 }}
>
<Table.Column
width={50}
render={(_, item: ISpool) => (
<Checkbox
checked={selectedItems.includes(item.id)}
onChange={() => handleSelectItem(item.id)}
/>
)}
/>
{SortedColumn({
id: "id",
i18ncat: "spool",
dataSource,
tableState,
})}
{SpoolIconColumn({
id: "filament_name",
i18ncat: "spool",
color: (record: ISpoolCollapsed) => record.filament.color_hex,
dataSource,
tableState,
})}
{FilteredColumn({
id: "material",
i18ncat: "spool",
dataSource,
tableState,
})}
</Table>
<Row>
<Col span={12}>
<Checkbox
checked={isAllFilteredSelected}
onChange={(e) => {
selectUnselectFiltered(e.target.checked);
}}
>
{t("printing.spoolSelect.selectAll")}
</Checkbox>
</Col>
<Col span={12}>
<div style={{ float: "right" }}>
{t("printing.spoolSelect.selectedTotal", {
count: selectedItems.length,
})}
</div>
</Col>
<Col span={12}>
<Checkbox
checked={showArchived}
onChange={(e) => {
setShowArchived(e.target.checked);
if (!e.target.checked) {
// Remove archived spools from selected items
setSelectedItems((prevSelected) =>
prevSelected.filter(
(selected) =>
dataSource.find((spool) => spool.id === selected)
?.archived !== true
)
);
}
}}
>
{t("printing.spoolSelect.showArchived")}
</Checkbox>
</Col>
</Row>
</Space>
</Modal>
);
};
export default SpoolSelectModal;

View File

@@ -37,6 +37,7 @@ import {
SpoolIconColumn,
} from "../../components/column";
import { setSpoolArchived } from "./functions";
import SelectAndPrint from "../../components/selectAndPrintDialog";
dayjs.extend(utc);
@@ -191,6 +192,7 @@ export const SpoolList: React.FC<IResourceComponentsProps> = () => {
<List
headerButtons={({ defaultButtons }) => (
<>
<SelectAndPrint />
<Button
type="primary"
icon={<InboxOutlined />}

View File

@@ -1,8 +1,10 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import svgr from "vite-plugin-svgr";
import mkcert from 'vite-plugin-mkcert'
export default defineConfig({
plugins: [react(), svgr()],
server: { https: true },
plugins: [react(), svgr(), mkcert()],
});