Skip to content

Commit 3f677cc

Browse files
sherrmannclaudeakira69
authored
Add filament label printing with QR codes and scanning (#4)
* Add filament label printing foundation Ports upstream Donkie#846: extends the QR/label printing system to filaments (filament select modal, filament QR/label print dialogs, and a filament printing page) alongside the existing spool printing, with supporting settings and API/query changes. Ruff-formatted to the fork's style. Verified: eslint, prettier, client build (tsc + vite), ruff, and a backend import smoke test. Ported from upstream Donkie#846 by @akira69. Co-authored-by: akira69 <akira69@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt * Address label-printing review nits (memo, fetch error handling, print guard) - filamentSelectModal: memoize dataSource so the selectUnselectFiltered useCallback (deps [dataSource]) is actually stable instead of rebuilt every render. - useGetFilamentsByIds: check res.ok before parsing so a 404/500 for a stale filament id surfaces as a real error instead of being parsed as an IFilament. - filaments/show print button: disable it (and omit the href) until the record has loaded, so it can't build "...?filaments=undefined" during the initial load. Deferred (with reason): consolidating database/filament._build_search_filters into the shared utils helpers — the new multi-field/empty-term behavior intentionally differs, so consolidation is a judgment call best left to the owner. The qrCodeScanner URL regexes are unchanged — they only ever navigate to local routes (no open redirect) and the nested-path match supports base-path deployments. Verified: tsc, eslint, prettier, build all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: akira69 <akira69@users.noreply.github.com>
1 parent b8ac022 commit 3f677cc

23 files changed

Lines changed: 1030 additions & 60 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Changelog
2+
3+
## Unreleased
4+
- Add filament label printing with separate presets, QR codes, and filament QR scanning support.

client/public/locales/en/common.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,11 @@
123123
"no": "No",
124124
"simple": "Simple",
125125
"withIcon": "With Icon"
126-
}
126+
},
127+
"exportButton": "Export Labels",
128+
"printFilamentTitle": "Print Filament Labels",
129+
"printSpoolTitle": "Print Spool Labels",
130+
"templateHelpFilament": "Use {} to insert values of the filament object as text. For example, {id} will be replaced with the filament id, or {vendor.name} will be replaced with the vendor name. If a value is missing it will be replaced with \"?\". A second set of {} can be used to remove this. In addition, any text between the sets of {} will be removed if the value is missing. For example, {Article: {article_number}} will only show the label if a filament has an article number. Enclose text with double asterix ** to make it bold. Click the button to view a list of all available tags."
127131
},
128132
"spoolSelect": {
129133
"title": "Select Spools",
@@ -133,6 +137,14 @@
133137
"selectAll": "Select/Unselect All",
134138
"selectedTotal_one": "{{count}} spool selected",
135139
"selectedTotal_other": "{{count}} spools selected"
140+
},
141+
"filamentSelect": {
142+
"title": "Select Filaments",
143+
"description": "Select filaments to print labels for.",
144+
"noFilamentsSelected": "You have not selected any filaments.",
145+
"selectAll": "Select/Unselect All",
146+
"selectedTotal_one": "{{count}} filament selected",
147+
"selectedTotal_other": "{{count}} filaments selected"
136148
}
137149
},
138150
"scanner": {

client/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ function App() {
208208
/>
209209
<Route path="edit/:id" element={<LoadableResourcePage resource="filaments" page="edit" />} />
210210
<Route path="show/:id" element={<LoadableResourcePage resource="filaments" page="show" />} />
211+
<Route path="print" element={<LoadablePage name="filamentPrinting" />} />
211212
</Route>
212213
<Route path="/vendor">
213214
<Route index element={<LoadableResourcePage resource="vendors" page="list" />} />

client/src/components/qrCodeScanner.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,28 @@ const QRCodeScannerModal = () => {
1818
const result = detectedCodes[0].rawValue;
1919

2020
// Check for the spoolman ID format
21-
const match = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/i);
22-
if (match && match.groups) {
21+
const spoolMatch = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/i);
22+
if (spoolMatch && spoolMatch.groups) {
2323
setVisible(false);
24-
navigate(`/spool/show/${match.groups.id}`);
24+
navigate(`/spool/show/${spoolMatch.groups.id}`);
25+
return;
26+
}
27+
const filamentMatch = result.match(/^web\+spoolman:f-(?<id>[0-9]+)$/i);
28+
if (filamentMatch && filamentMatch.groups) {
29+
setVisible(false);
30+
navigate(`/filament/show/${filamentMatch.groups.id}`);
31+
return;
32+
}
33+
const spoolURLmatch = result.match(/^https?:\/\/[^/]+(?:\/[^/]+)*\/spool\/show\/(?<id>[0-9]+)$/i);
34+
if (spoolURLmatch && spoolURLmatch.groups) {
35+
setVisible(false);
36+
navigate(`/spool/show/${spoolURLmatch.groups.id}`);
37+
return;
2538
}
26-
const fullURLmatch = result.match(/^https?:\/\/[^/]+\/spool\/show\/(?<id>[0-9]+)$/i);
27-
if (fullURLmatch && fullURLmatch.groups) {
39+
const filamentURLmatch = result.match(/^https?:\/\/[^/]+(?:\/[^/]+)*\/filament\/show\/(?<id>[0-9]+)$/i);
40+
if (filamentURLmatch && filamentURLmatch.groups) {
2841
setVisible(false);
29-
navigate(`/spool/show/${fullURLmatch.groups.id}`);
42+
navigate(`/filament/show/${filamentURLmatch.groups.id}`);
3043
}
3144
};
3245

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { PageHeader } from "@refinedev/antd";
2+
import { useTranslate } from "@refinedev/core";
3+
import { theme } from "antd";
4+
import { Content } from "antd/es/layout/layout";
5+
import dayjs from "dayjs";
6+
import utc from "dayjs/plugin/utc";
7+
import { useNavigate, useSearchParams } from "react-router";
8+
import FilamentQRCodePrintingDialog from "../printing/filamentQrCodePrintingDialog";
9+
import FilamentSelectModal from "../printing/filamentSelectModal";
10+
11+
dayjs.extend(utc);
12+
13+
const { useToken } = theme;
14+
15+
export const FilamentPrinting = () => {
16+
const { token } = useToken();
17+
const t = useTranslate();
18+
const [searchParams, setSearchParams] = useSearchParams();
19+
const navigate = useNavigate();
20+
21+
const filamentIds = searchParams.getAll("filaments").map(Number);
22+
const step = filamentIds.length > 0 ? 1 : 0;
23+
24+
return (
25+
<>
26+
<PageHeader
27+
title={t("printing.qrcode.printFilamentTitle")}
28+
onBack={() => {
29+
const returnUrl = searchParams.get("return");
30+
if (returnUrl) {
31+
navigate(returnUrl, { relative: "path" });
32+
} else {
33+
navigate("/filament");
34+
}
35+
}}
36+
>
37+
<Content
38+
style={{
39+
padding: 20,
40+
minHeight: 280,
41+
margin: "0 auto",
42+
backgroundColor: token.colorBgContainer,
43+
borderRadius: token.borderRadiusLG,
44+
color: token.colorText,
45+
fontFamily: token.fontFamily,
46+
fontSize: token.fontSizeLG,
47+
lineHeight: 1.5,
48+
}}
49+
>
50+
{step === 0 && (
51+
<FilamentSelectModal
52+
description={t("printing.filamentSelect.description")}
53+
onPrint={(selectedFilamentIds: number[]) => {
54+
setSearchParams((prev) => {
55+
const newParams = new URLSearchParams(prev);
56+
newParams.delete("filaments");
57+
selectedFilamentIds.forEach((id) => newParams.append("filaments", id.toString()));
58+
newParams.set("return", "/filament/print");
59+
return newParams;
60+
});
61+
}}
62+
/>
63+
)}
64+
{step === 1 && <FilamentQRCodePrintingDialog filamentIds={filamentIds} />}
65+
</Content>
66+
</PageHeader>
67+
</>
68+
);
69+
};
70+
71+
export default FilamentPrinting;

client/src/pages/filaments/functions.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useQueries } from "@tanstack/react-query";
12
import { ExternalFilament } from "../../utils/queryExternalDB";
23
import { getAPIURL } from "../../utils/url";
34
import { getOrCreateVendorFromExternal } from "../vendors/functions";
@@ -48,3 +49,27 @@ export async function createFilamentFromExternal(externalFilament: ExternalFilam
4849
}
4950
return response.json();
5051
}
52+
53+
/**
54+
* Returns an array of queries using the useQueries hook from @tanstack/react-query.
55+
* Each query fetches a filament by its ID from the server.
56+
*
57+
* @param {number[]} ids - An array of filament IDs to fetch.
58+
* @return An array of query results, each containing the fetched filament data.
59+
*/
60+
export function useGetFilamentsByIds(ids: number[]) {
61+
return useQueries({
62+
queries: ids.map((id) => {
63+
return {
64+
queryKey: ["filament", id],
65+
queryFn: async () => {
66+
const res = await fetch(getAPIURL() + "/filament/" + id);
67+
if (!res.ok) {
68+
throw new Error(`Failed to fetch filament ${id} (status ${res.status})`);
69+
}
70+
return (await res.json()) as IFilament;
71+
},
72+
};
73+
}),
74+
});
75+
}

client/src/pages/filaments/list.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { EditOutlined, EyeOutlined, FileOutlined, FilterOutlined, PlusSquareOutlined } from "@ant-design/icons";
1+
import {
2+
EditOutlined,
3+
EyeOutlined,
4+
FileOutlined,
5+
FilterOutlined,
6+
PlusSquareOutlined,
7+
PrinterOutlined,
8+
} from "@ant-design/icons";
29
import { List, useTable } from "@refinedev/antd";
310
import { useInvalidate, useNavigation, useTranslate } from "@refinedev/core";
411
import { Button, Dropdown, Table } from "antd";
@@ -169,6 +176,15 @@ export const FilamentList = () => {
169176
<List
170177
headerButtons={({ defaultButtons }) => (
171178
<>
179+
<Button
180+
type="primary"
181+
icon={<PrinterOutlined />}
182+
onClick={() => {
183+
navigate("print");
184+
}}
185+
>
186+
{t("printing.qrcode.button")}
187+
</Button>
172188
<Button
173189
type="primary"
174190
icon={<FilterOutlined />}

client/src/pages/filaments/show.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { DateField, NumberField, Show, TextField } from "@refinedev/antd";
22
import { useShow, useTranslate } from "@refinedev/core";
3+
import { PrinterOutlined } from "@ant-design/icons";
34
import { CalibrationSection } from "../calibration/CalibrationSection";
45
import { Button, Typography } from "antd";
56
import dayjs from "dayjs";
@@ -11,6 +12,7 @@ import SpoolIcon from "../../components/spoolIcon";
1112
import { enrichText } from "../../utils/parsing";
1213
import { EntityType, useGetFields } from "../../utils/queryFields";
1314
import { useCurrencyFormatter } from "../../utils/settings";
15+
import { getBasePath } from "../../utils/url";
1416
import { IFilament } from "./model";
1517
dayjs.extend(utc);
1618

@@ -66,6 +68,22 @@ export const FilamentShow = () => {
6668
<Button type="primary" onClick={gotoSpools}>
6769
{t("filament.fields.spools")}
6870
</Button>
71+
<Button
72+
type="primary"
73+
icon={<PrinterOutlined />}
74+
disabled={!record?.id}
75+
href={
76+
record?.id
77+
? getBasePath() +
78+
"/filament/print?filaments=" +
79+
record.id +
80+
"&return=" +
81+
encodeURIComponent(window.location.pathname)
82+
: undefined
83+
}
84+
>
85+
{t("printing.qrcode.button")}
86+
</Button>
6987
{defaultButtons}
7088
</>
7189
)}

0 commit comments

Comments
 (0)