Skip to content

Commit 25652a3

Browse files
committed
Merge PR Donkie#846: Add Filament Label Printing and AML Export
From upstream Donkie/Spoolman PR Donkie#846
2 parents e528d43 + 3b896d2 commit 25652a3

40 files changed

Lines changed: 3413 additions & 193 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 AML export (labels and pages), plus AML size control and filament QR scanning support.

client/public/locales/en/common.json

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,21 @@
6565
"helpMargin": "Margins should be configured to match your label paper and printer, changing these will affect the size of the entire grid.",
6666
"helpPrinterMargin": "Safe-Zone should be set to how close to the paper edge your printer can print, changing these will not affect the entire grid.",
6767
"print": "Print",
68+
"exportLabels": "Export Labels",
6869
"columns": "Columns",
6970
"rows": "Rows",
7071
"paperSize": "Paper Size",
7172
"customSize": "Custom",
7273
"dimensions": "Dimensions",
74+
"amlLabelSize": "AML Label Size",
75+
"exportFormat": "Export Format",
76+
"exportFormatOptions": {
77+
"png": "PNG",
78+
"aml": "AML"
79+
},
80+
"exportAmlPages": "Export as multiple page AML",
81+
"exportDpi": "Export DPI",
82+
"exportDpiHelp": "Higher DPI makes sharper PNG/AML exports but increases file size and export time.",
7383
"showBorder": "Show Border",
7484
"previewScale": "Preview Scale",
7585
"skipItems": "Skip Items",
@@ -92,6 +102,10 @@
92102
"grid": "Grid"
93103
},
94104
"settings": "Presets",
105+
"spoolPrintPresets": "Spool Print Presets",
106+
"filamentPrintPresets": "Filament Print Presets",
107+
"spoolImagePresets": "Spool Image Presets",
108+
"filamentImagePresets": "Filament Image Presets",
95109
"defaultSettings": "Default",
96110
"addSettings": "Add New Preset",
97111
"newSetting": "New",
@@ -100,13 +114,19 @@
100114
"deleteSettingsConfirm": "Are you sure you want to delete this preset?",
101115
"settingsName": "Preset Name",
102116
"saveSetting": "Save Presets",
103-
"saveAsImage": "Save as Image"
117+
"saveAsImage": "Save as Image",
118+
"saveAsAmlLabels": "Save as AML (Labels)",
119+
"saveAsAmlPages": "Save as AML (Pages)"
104120
},
105121
"qrcode": {
106122
"button": "Print Labels",
123+
"exportButton": "Export Labels",
124+
"selectButton": "Export/Print",
125+
"selectTitle": "Export / Print Labels",
107126
"title": "Label Printing",
108127
"template": "Label Template",
109128
"templateHelp": "Use {} to insert values of the spool object as text. For example, {id} will be replaced with the spool id, or {filament.material} will be replaced with the material of the spool. 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, {Lot Nr: {lot_nr}} will only show the label if the spool has a lot number. Enclose text with double asterix ** to make it bold. Click the button to view a list of all available tags.",
129+
"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.",
110130
"textSize": "Label Text Size",
111131
"showContent": "Print Label",
112132
"useHTTPUrl": {
@@ -127,12 +147,22 @@
127147
},
128148
"spoolSelect": {
129149
"title": "Select Spools",
130-
"description": "Select spools to print labels for.",
150+
"description": "Select spools to export or print labels for.",
151+
"searchPlaceholder": "Search vendor, filament, material, location, lot #",
131152
"showArchived": "Show Archived",
132153
"noSpoolsSelected": "You have not selected any spools.",
133154
"selectAll": "Select/Unselect All",
134155
"selectedTotal_one": "{{count}} spool selected",
135156
"selectedTotal_other": "{{count}} spools selected"
157+
},
158+
"filamentSelect": {
159+
"title": "Select Filaments",
160+
"description": "Select filaments to export or print labels for.",
161+
"searchPlaceholder": "Search vendor, name, material, article #",
162+
"noFilamentsSelected": "You have not selected any filaments.",
163+
"selectAll": "Select/Unselect All",
164+
"selectedTotal_one": "{{count}} filament selected",
165+
"selectedTotal_other": "{{count}} filaments selected"
136166
}
137167
},
138168
"scanner": {

client/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,9 @@ function App() {
194194
/>
195195
<Route path="edit/:id" element={<LoadableResourcePage resource="spools" page="edit" />} />
196196
<Route path="show/:id" element={<LoadableResourcePage resource="spools" page="show" />} />
197+
<Route path="labels" element={<LoadablePage name="spoolLabels" />} />
197198
<Route path="print" element={<LoadablePage name="printing" />} />
199+
<Route path="export" element={<LoadablePage name="printingExport" />} />
198200
</Route>
199201
<Route path="/filament">
200202
<Route index element={<LoadableResourcePage resource="filaments" page="list" />} />
@@ -208,6 +210,9 @@ function App() {
208210
/>
209211
<Route path="edit/:id" element={<LoadableResourcePage resource="filaments" page="edit" />} />
210212
<Route path="show/:id" element={<LoadableResourcePage resource="filaments" page="show" />} />
213+
<Route path="labels" element={<LoadablePage name="filamentLabels" />} />
214+
<Route path="print" element={<LoadablePage name="filamentPrinting" />} />
215+
<Route path="export" element={<LoadablePage name="filamentExport" />} />
211216
</Route>
212217
<Route path="/vendor">
213218
<Route index element={<LoadableResourcePage resource="vendors" page="list" />} />

client/src/components/column.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ interface BaseColumnProps<Obj extends Entity> {
4646
title?: string;
4747
align?: AlignType;
4848
sorter?: boolean;
49+
ellipsis?: boolean;
4950
t: (key: string) => string;
5051
navigate: (link: string) => void;
5152
dataSource: Obj[];
@@ -62,6 +63,7 @@ interface FilteredColumnProps {
6263
allowMultipleFilters?: boolean;
6364
onFilterDropdownOpen?: () => void;
6465
loadingFilters?: boolean;
66+
filterSearch?: boolean | ((input: string, record: ColumnFilterItem) => boolean);
6567
}
6668

6769
interface CustomColumnProps<Obj> {
@@ -90,6 +92,7 @@ function Column<Obj extends Entity>(
9092
dataIndex: props.id,
9193
align: props.align,
9294
title: props.title ?? t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
95+
ellipsis: props.ellipsis,
9396
filterMultiple: props.allowMultipleFilters ?? true,
9497
width: props.width ?? undefined,
9598
onCell: props.onCell ?? undefined,
@@ -108,6 +111,7 @@ function Column<Obj extends Entity>(
108111
if (props.filters && props.filteredValue) {
109112
columnProps.filters = props.filters;
110113
columnProps.filteredValue = props.filteredValue;
114+
columnProps.filterSearch = props.filterSearch ?? true;
111115
if (props.loadingFilters) {
112116
columnProps.filterDropdown = <FilterDropdownLoading />;
113117
}
@@ -356,7 +360,9 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
356360
<SpoolIcon color={colorObj} />
357361
</Col>
358362
)}
359-
<Col flex="auto">{value}</Col>
363+
<Col flex="auto" style={{ minWidth: 0 }}>
364+
{value}
365+
</Col>
360366
</Row>
361367
);
362368
},

client/src/components/otherModels.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -86,29 +86,18 @@ export function useSpoolmanFilamentFilter(enabled: boolean = false) {
8686
}
8787

8888
export function useSpoolmanFilamentNames(enabled: boolean = false) {
89-
return useQuery<IFilament[], unknown, string[]>({
89+
return useQuery<string[]>({
9090
enabled: enabled,
91-
queryKey: ["filaments"],
91+
queryKey: ["filamentNames"],
9292
queryFn: async () => {
93-
const response = await fetch(getAPIURL() + "/filament");
93+
const response = await fetch(getAPIURL() + "/filament-name");
9494
if (!response.ok) {
9595
throw new Error("Network response was not ok");
9696
}
9797
return response.json();
9898
},
9999
select: (data) => {
100-
// Concatenate vendor name and filament name
101-
let names = data
102-
.filter((filament) => {
103-
return filament.name !== null && filament.name !== undefined && filament.name !== "";
104-
})
105-
.map((filament) => {
106-
return filament.name ?? "<unknown>";
107-
})
108-
.sort();
109-
// Remove duplicates
110-
names = [...new Set(names)];
111-
return names;
100+
return data.sort();
112101
},
113102
});
114103
}

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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 { useEffect, useMemo } from "react";
6+
import { useNavigate, useSearchParams } from "react-router";
7+
import FilamentQRCodeExportDialog from "../printing/filamentQrCodeExportDialog";
8+
9+
const { useToken } = theme;
10+
11+
export const FilamentExport = () => {
12+
const { token } = useToken();
13+
const t = useTranslate();
14+
const [searchParams] = useSearchParams();
15+
const navigate = useNavigate();
16+
17+
const filamentIds = searchParams.getAll("filaments").map(Number);
18+
const returnUrl = searchParams.get("return");
19+
const selectionPath = useMemo(() => {
20+
const params = new URLSearchParams();
21+
if (returnUrl) {
22+
params.set("return", returnUrl);
23+
}
24+
const query = params.toString();
25+
return `/filament/labels${query ? `?${query}` : ""}`;
26+
}, [returnUrl]);
27+
28+
useEffect(() => {
29+
if (filamentIds.length === 0) {
30+
navigate(selectionPath, { replace: true });
31+
}
32+
}, [filamentIds.length, navigate, selectionPath]);
33+
34+
return (
35+
<>
36+
<PageHeader
37+
title={t("printing.qrcode.exportButton")}
38+
onBack={() => {
39+
const returnUrl = searchParams.get("return");
40+
if (returnUrl) {
41+
navigate(returnUrl, { relative: "path" });
42+
} else {
43+
navigate("/filament");
44+
}
45+
}}
46+
>
47+
<Content
48+
style={{
49+
padding: 20,
50+
minHeight: "70vh",
51+
height: "calc(100vh - 200px)",
52+
margin: "0 auto",
53+
backgroundColor: token.colorBgContainer,
54+
borderRadius: token.borderRadiusLG,
55+
color: token.colorText,
56+
fontFamily: token.fontFamily,
57+
fontSize: token.fontSizeLG,
58+
lineHeight: 1.5,
59+
overflow: "auto",
60+
}}
61+
>
62+
{filamentIds.length > 0 && <FilamentQRCodeExportDialog filamentIds={filamentIds} />}
63+
</Content>
64+
</PageHeader>
65+
</>
66+
);
67+
};
68+
69+
export default FilamentExport;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 { useMemo } from "react";
6+
import { useNavigate, useSearchParams } from "react-router";
7+
import FilamentSelectModal from "../printing/filamentSelectModal";
8+
9+
const { useToken } = theme;
10+
11+
export const FilamentLabels = () => {
12+
const { token } = useToken();
13+
const t = useTranslate();
14+
const [searchParams] = useSearchParams();
15+
const navigate = useNavigate();
16+
17+
const returnUrl = searchParams.get("return");
18+
const initialSelectedIds = searchParams.getAll("filaments").map(Number).filter((id) => !Number.isNaN(id));
19+
20+
const selectionPath = useMemo(() => {
21+
const params = new URLSearchParams();
22+
if (returnUrl) {
23+
params.set("return", returnUrl);
24+
}
25+
const query = params.toString();
26+
return `/filament/labels${query ? `?${query}` : ""}`;
27+
}, [returnUrl]);
28+
29+
const handleNavigate = (mode: "print" | "export", ids: number[]) => {
30+
const params = new URLSearchParams();
31+
ids.forEach((id) => params.append("filaments", id.toString()));
32+
params.set("return", selectionPath);
33+
navigate(`/filament/${mode}?${params.toString()}`);
34+
};
35+
36+
return (
37+
<>
38+
<PageHeader
39+
title={t("printing.qrcode.selectTitle")}
40+
onBack={() => {
41+
if (returnUrl) {
42+
navigate(returnUrl, { relative: "path" });
43+
} else {
44+
navigate("/filament");
45+
}
46+
}}
47+
>
48+
<Content
49+
style={{
50+
padding: 20,
51+
minHeight: "70vh",
52+
height: "calc(100vh - 200px)",
53+
margin: "0 auto",
54+
backgroundColor: token.colorBgContainer,
55+
borderRadius: token.borderRadiusLG,
56+
color: token.colorText,
57+
fontFamily: token.fontFamily,
58+
fontSize: token.fontSizeLG,
59+
lineHeight: 1.5,
60+
display: "flex",
61+
flexDirection: "column",
62+
overflow: "hidden",
63+
}}
64+
>
65+
<FilamentSelectModal
66+
description={t("printing.filamentSelect.description")}
67+
initialSelectedIds={initialSelectedIds}
68+
onPrint={(ids) => handleNavigate("print", ids)}
69+
onExport={(ids) => handleNavigate("export", ids)}
70+
/>
71+
</Content>
72+
</PageHeader>
73+
</>
74+
);
75+
};
76+
77+
export default FilamentLabels;

0 commit comments

Comments
 (0)