Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

## Unreleased
- Add filament label printing with separate presets, QR codes, and AML export (labels and pages), plus AML size control and filament QR scanning support.
34 changes: 32 additions & 2 deletions client/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,21 @@
"helpMargin": "Margins should be configured to match your label paper and printer, changing these will affect the size of the entire grid.",
"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.",
"print": "Print",
"exportLabels": "Export Labels",
"columns": "Columns",
"rows": "Rows",
"paperSize": "Paper Size",
"customSize": "Custom",
"dimensions": "Dimensions",
"amlLabelSize": "AML Label Size",
"exportFormat": "Export Format",
"exportFormatOptions": {
"png": "PNG",
"aml": "AML"
},
"exportAmlPages": "Export as multiple page AML",
"exportDpi": "Export DPI",
"exportDpiHelp": "Higher DPI makes sharper PNG/AML exports but increases file size and export time.",
"showBorder": "Show Border",
"previewScale": "Preview Scale",
"skipItems": "Skip Items",
Expand All @@ -92,6 +102,10 @@
"grid": "Grid"
},
"settings": "Presets",
"spoolPrintPresets": "Spool Print Presets",
"filamentPrintPresets": "Filament Print Presets",
"spoolImagePresets": "Spool Image Presets",
"filamentImagePresets": "Filament Image Presets",
"defaultSettings": "Default",
"addSettings": "Add New Preset",
"newSetting": "New",
Expand All @@ -100,13 +114,19 @@
"deleteSettingsConfirm": "Are you sure you want to delete this preset?",
"settingsName": "Preset Name",
"saveSetting": "Save Presets",
"saveAsImage": "Save as Image"
"saveAsImage": "Save as Image",
"saveAsAmlLabels": "Save as AML (Labels)",
"saveAsAmlPages": "Save as AML (Pages)"
},
"qrcode": {
"button": "Print Labels",
"exportButton": "Export Labels",
"selectButton": "Export/Print",
"selectTitle": "Export / Print Labels",
"title": "Label Printing",
"template": "Label Template",
"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.",
"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.",
"textSize": "Label Text Size",
"showContent": "Print Label",
"useHTTPUrl": {
Expand All @@ -127,12 +147,22 @@
},
"spoolSelect": {
"title": "Select Spools",
"description": "Select spools to print labels for.",
"description": "Select spools to export or print labels for.",
"searchPlaceholder": "Search vendor, filament, material, location, lot #",
"showArchived": "Show Archived",
"noSpoolsSelected": "You have not selected any spools.",
"selectAll": "Select/Unselect All",
"selectedTotal_one": "{{count}} spool selected",
"selectedTotal_other": "{{count}} spools selected"
},
"filamentSelect": {
"title": "Select Filaments",
"description": "Select filaments to export or print labels for.",
"searchPlaceholder": "Search vendor, name, material, article #",
"noFilamentsSelected": "You have not selected any filaments.",
"selectAll": "Select/Unselect All",
"selectedTotal_one": "{{count}} filament selected",
"selectedTotal_other": "{{count}} filaments selected"
}
},
"scanner": {
Expand Down
5 changes: 5 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ function App() {
/>
<Route path="edit/:id" element={<LoadableResourcePage resource="spools" page="edit" />} />
<Route path="show/:id" element={<LoadableResourcePage resource="spools" page="show" />} />
<Route path="labels" element={<LoadablePage name="spoolLabels" />} />
<Route path="print" element={<LoadablePage name="printing" />} />
<Route path="export" element={<LoadablePage name="printingExport" />} />
</Route>
<Route path="/filament">
<Route index element={<LoadableResourcePage resource="filaments" page="list" />} />
Expand All @@ -208,6 +210,9 @@ function App() {
/>
<Route path="edit/:id" element={<LoadableResourcePage resource="filaments" page="edit" />} />
<Route path="show/:id" element={<LoadableResourcePage resource="filaments" page="show" />} />
<Route path="labels" element={<LoadablePage name="filamentLabels" />} />
<Route path="print" element={<LoadablePage name="filamentPrinting" />} />
<Route path="export" element={<LoadablePage name="filamentExport" />} />
</Route>
<Route path="/vendor">
<Route index element={<LoadableResourcePage resource="vendors" page="list" />} />
Expand Down
8 changes: 7 additions & 1 deletion client/src/components/column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ interface BaseColumnProps<Obj extends Entity> {
title?: string;
align?: AlignType;
sorter?: boolean;
ellipsis?: boolean;
t: (key: string) => string;
navigate: (link: string) => void;
dataSource: Obj[];
Expand All @@ -62,6 +63,7 @@ interface FilteredColumnProps {
allowMultipleFilters?: boolean;
onFilterDropdownOpen?: () => void;
loadingFilters?: boolean;
filterSearch?: boolean | ((input: string, record: ColumnFilterItem) => boolean);
}

interface CustomColumnProps<Obj> {
Expand Down Expand Up @@ -90,6 +92,7 @@ function Column<Obj extends Entity>(
dataIndex: props.id,
align: props.align,
title: props.title ?? t(props.i18nkey ?? `${props.i18ncat}.fields.${props.id}`),
ellipsis: props.ellipsis,
filterMultiple: props.allowMultipleFilters ?? true,
width: props.width ?? undefined,
onCell: props.onCell ?? undefined,
Expand All @@ -108,6 +111,7 @@ function Column<Obj extends Entity>(
if (props.filters && props.filteredValue) {
columnProps.filters = props.filters;
columnProps.filteredValue = props.filteredValue;
columnProps.filterSearch = props.filterSearch ?? true;
if (props.loadingFilters) {
columnProps.filterDropdown = <FilterDropdownLoading />;
}
Expand Down Expand Up @@ -356,7 +360,9 @@ export function SpoolIconColumn<Obj extends Entity>(props: SpoolIconColumnProps<
<SpoolIcon color={colorObj} />
</Col>
)}
<Col flex="auto">{value}</Col>
<Col flex="auto" style={{ minWidth: 0 }}>
{value}
</Col>
</Row>
);
},
Expand Down
19 changes: 4 additions & 15 deletions client/src/components/otherModels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,29 +86,18 @@ export function useSpoolmanFilamentFilter(enabled: boolean = false) {
}

export function useSpoolmanFilamentNames(enabled: boolean = false) {
return useQuery<IFilament[], unknown, string[]>({
return useQuery<string[]>({
enabled: enabled,
queryKey: ["filaments"],
queryKey: ["filamentNames"],
queryFn: async () => {
const response = await fetch(getAPIURL() + "/filament");
const response = await fetch(getAPIURL() + "/filament-name");
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
},
select: (data) => {
// Concatenate vendor name and filament name
let names = data
.filter((filament) => {
return filament.name !== null && filament.name !== undefined && filament.name !== "";
})
.map((filament) => {
return filament.name ?? "<unknown>";
})
.sort();
// Remove duplicates
names = [...new Set(names)];
return names;
return data.sort();
},
});
}
Expand Down
25 changes: 19 additions & 6 deletions client/src/components/qrCodeScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,28 @@ const QRCodeScannerModal = () => {
const result = detectedCodes[0].rawValue;

// Check for the spoolman ID format
const match = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/i);
if (match && match.groups) {
const spoolMatch = result.match(/^web\+spoolman:s-(?<id>[0-9]+)$/i);
if (spoolMatch && spoolMatch.groups) {
setVisible(false);
navigate(`/spool/show/${match.groups.id}`);
navigate(`/spool/show/${spoolMatch.groups.id}`);
return;
}
const filamentMatch = result.match(/^web\+spoolman:f-(?<id>[0-9]+)$/i);
if (filamentMatch && filamentMatch.groups) {
setVisible(false);
navigate(`/filament/show/${filamentMatch.groups.id}`);
return;
}
const spoolURLmatch = result.match(/^https?:\/\/[^/]+(?:\/[^/]+)*\/spool\/show\/(?<id>[0-9]+)$/i);
if (spoolURLmatch && spoolURLmatch.groups) {
setVisible(false);
navigate(`/spool/show/${spoolURLmatch.groups.id}`);
return;
}
const fullURLmatch = result.match(/^https?:\/\/[^/]+\/spool\/show\/(?<id>[0-9]+)$/i);
if (fullURLmatch && fullURLmatch.groups) {
const filamentURLmatch = result.match(/^https?:\/\/[^/]+(?:\/[^/]+)*\/filament\/show\/(?<id>[0-9]+)$/i);
if (filamentURLmatch && filamentURLmatch.groups) {
setVisible(false);
navigate(`/spool/show/${fullURLmatch.groups.id}`);
navigate(`/filament/show/${filamentURLmatch.groups.id}`);
}
};

Expand Down
69 changes: 69 additions & 0 deletions client/src/pages/filamentExport/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { PageHeader } from "@refinedev/antd";
import { useTranslate } from "@refinedev/core";
import { theme } from "antd";
import { Content } from "antd/es/layout/layout";
import { useEffect, useMemo } from "react";
import { useNavigate, useSearchParams } from "react-router";
import FilamentQRCodeExportDialog from "../printing/filamentQrCodeExportDialog";

const { useToken } = theme;

export const FilamentExport = () => {
const { token } = useToken();
const t = useTranslate();
const [searchParams] = useSearchParams();
const navigate = useNavigate();

const filamentIds = searchParams.getAll("filaments").map(Number);
const returnUrl = searchParams.get("return");
const selectionPath = useMemo(() => {
const params = new URLSearchParams();
if (returnUrl) {
params.set("return", returnUrl);
}
const query = params.toString();
return `/filament/labels${query ? `?${query}` : ""}`;
}, [returnUrl]);

useEffect(() => {
if (filamentIds.length === 0) {
navigate(selectionPath, { replace: true });
}
}, [filamentIds.length, navigate, selectionPath]);

return (
<>
<PageHeader
title={t("printing.qrcode.exportButton")}
onBack={() => {
const returnUrl = searchParams.get("return");
if (returnUrl) {
navigate(returnUrl, { relative: "path" });
} else {
navigate("/filament");
}
}}
>
<Content
style={{
padding: 20,
minHeight: "70vh",
height: "calc(100vh - 200px)",
margin: "0 auto",
backgroundColor: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
color: token.colorText,
fontFamily: token.fontFamily,
fontSize: token.fontSizeLG,
lineHeight: 1.5,
overflow: "auto",
}}
>
{filamentIds.length > 0 && <FilamentQRCodeExportDialog filamentIds={filamentIds} />}
</Content>
</PageHeader>
</>
);
};

export default FilamentExport;
77 changes: 77 additions & 0 deletions client/src/pages/filamentLabels/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { PageHeader } from "@refinedev/antd";
import { useTranslate } from "@refinedev/core";
import { theme } from "antd";
import { Content } from "antd/es/layout/layout";
import { useMemo } from "react";
import { useNavigate, useSearchParams } from "react-router";
import FilamentSelectModal from "../printing/filamentSelectModal";

const { useToken } = theme;

export const FilamentLabels = () => {
const { token } = useToken();
const t = useTranslate();
const [searchParams] = useSearchParams();
const navigate = useNavigate();

const returnUrl = searchParams.get("return");
const initialSelectedIds = searchParams.getAll("filaments").map(Number).filter((id) => !Number.isNaN(id));

const selectionPath = useMemo(() => {
const params = new URLSearchParams();
if (returnUrl) {
params.set("return", returnUrl);
}
const query = params.toString();
return `/filament/labels${query ? `?${query}` : ""}`;
}, [returnUrl]);

const handleNavigate = (mode: "print" | "export", ids: number[]) => {
const params = new URLSearchParams();
ids.forEach((id) => params.append("filaments", id.toString()));
params.set("return", selectionPath);
navigate(`/filament/${mode}?${params.toString()}`);
};

return (
<>
<PageHeader
title={t("printing.qrcode.selectTitle")}
onBack={() => {
if (returnUrl) {
navigate(returnUrl, { relative: "path" });
} else {
navigate("/filament");
}
}}
>
<Content
style={{
padding: 20,
minHeight: "70vh",
height: "calc(100vh - 200px)",
margin: "0 auto",
backgroundColor: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
color: token.colorText,
fontFamily: token.fontFamily,
fontSize: token.fontSizeLG,
lineHeight: 1.5,
display: "flex",
flexDirection: "column",
overflow: "hidden",
}}
>
<FilamentSelectModal
description={t("printing.filamentSelect.description")}
initialSelectedIds={initialSelectedIds}
onPrint={(ids) => handleNavigate("print", ids)}
onExport={(ids) => handleNavigate("export", ids)}
/>
</Content>
</PageHeader>
</>
);
};

export default FilamentLabels;
Loading