@@ -115,7 +121,6 @@ const QRCodePrintingDialog = ({
- {/* Mirror the encoded payload so users can confirm which QR format the preset will emit. */}
{useHTTPUrl ? preview.url : preview.default}
>
@@ -162,6 +167,8 @@ const QRCodePrintingDialog = ({
+ {extraTitleSettings}
+ {extraInfoSettings}
{extraSettings}
>
}
diff --git a/client/src/pages/printing/spoolQrCodeExportDialog.tsx b/client/src/pages/printing/spoolQrCodeExportDialog.tsx
new file mode 100644
index 000000000..fbad9b880
--- /dev/null
+++ b/client/src/pages/printing/spoolQrCodeExportDialog.tsx
@@ -0,0 +1,383 @@
+import { CopyOutlined, DeleteOutlined, PlusOutlined, SaveOutlined } from "@ant-design/icons";
+import { useTranslate } from "@refinedev/core";
+import { Button, Flex, Form, Input, Modal, Popconfirm, Select, Table, Typography, message } from "antd";
+import TextArea from "antd/es/input/TextArea";
+import { useState } from "react";
+import { v4 as uuidv4 } from "uuid";
+import { EntityType, useGetFields } from "../../utils/queryFields";
+import { useGetSetting } from "../../utils/querySettings";
+import { useSavedState } from "../../utils/saveload";
+import { useGetSpoolsByIds } from "../spools/functions";
+import { ISpool } from "../spools/model";
+import {
+ getConfiguredBaseUrl,
+ SpoolQRCodePrintSettings,
+ renderLabelContents,
+ renderTemplateText,
+ useGetPrintSettings as useGetPrintPresets,
+ useSetPrintSettings as useSetPrintPresets,
+} from "./printing";
+import QRCodeExportDialog from "./qrCodeExportDialog";
+
+const { Text } = Typography;
+
+interface SpoolQRCodeExportDialog {
+ spoolIds: number[];
+}
+
+// Adapt spool records into the generic QR export dialog and keep export-only
+// preset fields isolated from the simpler print-only spool presets.
+const SpoolQRCodeExportDialog = ({ spoolIds }: SpoolQRCodeExportDialog) => {
+ const t = useTranslate();
+ const baseUrlSetting = useGetSetting("base_url");
+ // Accept both JSON-backed settings and legacy plain strings so old `base_url` values do not crash the dialog.
+ const baseUrlRoot = getConfiguredBaseUrl(baseUrlSetting.data?.value, window.location.origin);
+ const [messageApi, contextHolder] = message.useMessage();
+ const [useHTTPUrl, setUseHTTPUrl] = useSavedState("export-useHTTPUrl", false);
+
+ const itemQueries = useGetSpoolsByIds(spoolIds);
+ const items = itemQueries
+ .map((itemQuery) => {
+ return itemQuery.data ?? null;
+ })
+ .filter((item) => item !== null) as ISpool[];
+
+ const [selectedPresetState, setSelectedPresetState] = useSavedState
(
+ "selectedImagePresetSpool",
+ undefined,
+ );
+
+ const [localPresets, setLocalPresets] = useState();
+ // Export presets stay in their own bucket so filename/DPI/export-format choices do not
+ // mutate the simpler print-only presets used by the non-export dialog.
+ const remotePresets = useGetPrintPresets("image_presets");
+ const setRemotePresets = useSetPrintPresets("image_presets");
+
+ const localOrRemotePresets = localPresets ?? remotePresets;
+
+ const savePresetsRemote = () => {
+ if (!localPresets) return;
+ setRemotePresets.mutate(localPresets);
+ };
+
+ const addNewPreset = () => {
+ if (!localOrRemotePresets) return;
+ const newId = uuidv4();
+ const newPreset = {
+ labelSettings: {
+ printSettings: {
+ id: newId,
+ name: t("printing.generic.newSetting"),
+ },
+ },
+ };
+ setLocalPresets([...localOrRemotePresets, newPreset]);
+ setSelectedPresetState(newId);
+ return newPreset;
+ };
+ const duplicateCurrentPreset = () => {
+ if (!localOrRemotePresets) return;
+ const newPreset = {
+ ...curPreset,
+ labelSettings: { ...curPreset.labelSettings, printSettings: { ...curPreset.labelSettings.printSettings } },
+ };
+ newPreset.labelSettings.printSettings.id = uuidv4();
+ setLocalPresets([...localOrRemotePresets, newPreset]);
+ setSelectedPresetState(newPreset.labelSettings.printSettings.id);
+ };
+ const updateCurrentPreset = (newSettings: SpoolQRCodePrintSettings) => {
+ if (!localOrRemotePresets) return;
+ setLocalPresets(
+ localOrRemotePresets.map((presets) =>
+ presets.labelSettings.printSettings.id === newSettings.labelSettings.printSettings.id ? newSettings : presets,
+ ),
+ );
+ };
+ const deleteCurrentPreset = () => {
+ if (!localOrRemotePresets) return;
+ setLocalPresets(
+ localOrRemotePresets.filter((qPreset) => qPreset.labelSettings.printSettings.id !== selectedPresetState),
+ );
+ setSelectedPresetState(undefined);
+ };
+
+ let curPreset: SpoolQRCodePrintSettings;
+ if (localOrRemotePresets === undefined) {
+ curPreset = {
+ labelSettings: {
+ printSettings: {
+ id: "TEMP",
+ name: t("printing.generic.newSetting"),
+ },
+ },
+ };
+ } else {
+ if (localOrRemotePresets.length === 0) {
+ // First-time export users should land in a usable preset immediately instead of an
+ // empty export dialog with no selected settings object to edit.
+ const newSetting = addNewPreset();
+ if (!newSetting) {
+ console.error("Error adding new setting, this should never happen");
+ return;
+ }
+ localOrRemotePresets.push(newSetting);
+ curPreset = newSetting;
+ } else {
+ if (!selectedPresetState) {
+ curPreset = localOrRemotePresets[0];
+ setSelectedPresetState(localOrRemotePresets[0].labelSettings.printSettings.id);
+ } else {
+ const foundSetting = localOrRemotePresets.find(
+ (settings) => settings.labelSettings.printSettings.id === selectedPresetState,
+ );
+ if (foundSetting) {
+ curPreset = foundSetting;
+ } else {
+ // Recover to the first saved preset when the remembered selection no longer exists.
+ curPreset = localOrRemotePresets[0];
+ setSelectedPresetState(localOrRemotePresets[0].labelSettings.printSettings.id);
+ }
+ }
+ }
+ }
+
+ const [templateHelpOpen, setTemplateHelpOpen] = useState(false);
+ const template =
+ curPreset.template ??
+ `**{filament.vendor.name} - {filament.name}
+#{id} - {filament.material}**
+Spool Weight: {filament.spool_weight} g
+{ET: {filament.settings_extruder_temp} °C}
+{BT: {filament.settings_bed_temp} °C}
+{Lot Nr: {lot_nr}}
+{{comment}}
+{filament.comment}
+{filament.vendor.comment}`;
+ const filenameTemplate =
+ curPreset.filenameTemplate ?? `{filament.vendor.name}-{filament.material}-{filament.name}-{id}`;
+
+ const spoolTags = [
+ { tag: "id" },
+ { tag: "registered" },
+ { tag: "first_used" },
+ { tag: "last_used" },
+ { tag: "price" },
+ { tag: "initial_weight" },
+ { tag: "spool_weight" },
+ { tag: "remaining_weight" },
+ { tag: "used_weight" },
+ { tag: "remaining_length" },
+ { tag: "used_length" },
+ { tag: "location" },
+ { tag: "lot_nr" },
+ { tag: "comment" },
+ { tag: "archived" },
+ ];
+ const spoolFields = useGetFields(EntityType.spool);
+ if (spoolFields.data !== undefined) {
+ spoolFields.data.forEach((field) => {
+ spoolTags.push({ tag: `extra.${field.key}` });
+ });
+ }
+ const filamentTags = [
+ { tag: "filament.id" },
+ { tag: "filament.registered" },
+ { tag: "filament.name" },
+ { tag: "filament.material" },
+ { tag: "filament.price" },
+ { tag: "filament.density" },
+ { tag: "filament.diameter" },
+ { tag: "filament.weight" },
+ { tag: "filament.spool_weight" },
+ { tag: "filament.article_number" },
+ { tag: "filament.comment" },
+ { tag: "filament.settings_extruder_temp" },
+ { tag: "filament.settings_bed_temp" },
+ { tag: "filament.color_hex" },
+ { tag: "filament.multi_color_hexes" },
+ { tag: "filament.multi_color_direction" },
+ { tag: "filament.external_id" },
+ ];
+ const filamentFields = useGetFields(EntityType.filament);
+ if (filamentFields.data !== undefined) {
+ filamentFields.data.forEach((field) => {
+ filamentTags.push({ tag: `filament.extra.${field.key}` });
+ });
+ }
+ const vendorTags = [
+ { tag: "filament.vendor.id" },
+ { tag: "filament.vendor.registered" },
+ { tag: "filament.vendor.name" },
+ { tag: "filament.vendor.comment" },
+ { tag: "filament.vendor.empty_spool_weight" },
+ { tag: "filament.vendor.external_id" },
+ ];
+ const vendorFields = useGetFields(EntityType.vendor);
+ if (vendorFields.data !== undefined) {
+ vendorFields.data.forEach((field) => {
+ vendorTags.push({ tag: `filament.vendor.extra.${field.key}` });
+ });
+ }
+
+ // Expose spool, filament, and vendor placeholders because the same tag picker drives
+ // label text and export filename templates.
+ const templateTags = [...spoolTags, ...filamentTags, ...vendorTags];
+
+ return (
+ <>
+ {contextHolder}
+ {
+ updateCurrentPreset({ ...curPreset, labelSettings: newSettings });
+ }}
+ baseUrlRoot={baseUrlRoot}
+ useHTTPUrl={useHTTPUrl}
+ setUseHTTPUrl={setUseHTTPUrl}
+ previewValues={{
+ default: "WEB+SPOOLMAN:S-{id}",
+ url: `${baseUrlRoot}/spool/show/{id}`,
+ }}
+ zipFileTypeName="spool"
+ extraSettingsStart={
+ <>
+
+
+
+ }
+ title={t("printing.generic.addSettings")}
+ onClick={addNewPreset}
+ />
+ }
+ title={t("printing.generic.duplicateSettings")}
+ onClick={duplicateCurrentPreset}
+ />
+ {localOrRemotePresets && localOrRemotePresets.length > 1 && (
+
+ }
+ title={t("printing.generic.deleteSettings")}
+ />
+
+ )}
+
+
+
+ {
+ updateCurrentPreset({
+ ...curPreset,
+ labelSettings: {
+ ...curPreset.labelSettings,
+ printSettings: { ...curPreset.labelSettings.printSettings, name: e.target.value },
+ },
+ });
+ }}
+ />
+
+ >
+ }
+ items={items.map((spool) => ({
+ value: useHTTPUrl ? `${baseUrlRoot}/spool/show/${spool.id}` : `WEB+SPOOLMAN:S-${spool.id}`,
+ amlName: renderTemplateText(filenameTemplate, spool),
+ label: (
+
+ {renderLabelContents(template, spool)}
+
+ ),
+ errorLevel: "H",
+ }))}
+ extraFormatSettings={
+
+ {
+ updateCurrentPreset({ ...curPreset, filenameTemplate: newValue.target.value });
+ }}
+ />
+
+ }
+ extraSettings={
+ <>
+
+
+ setTemplateHelpOpen(false)}>
+
+
+
+ {t("printing.qrcode.templateHelp")}{" "}
+
+
+ >
+ }
+ extraButtons={
+ <>
+ }
+ onClick={() => {
+ savePresetsRemote();
+ messageApi.success(t("notifications.saveSuccessful"));
+ }}
+ >
+ {t("printing.generic.saveSetting")}
+
+ >
+ }
+ />
+ >
+ );
+};
+
+export default SpoolQRCodeExportDialog;
diff --git a/client/src/pages/printing/spoolQrCodePrintingDialog.tsx b/client/src/pages/printing/spoolQrCodePrintingDialog.tsx
index e5d08f5b5..64553beec 100644
--- a/client/src/pages/printing/spoolQrCodePrintingDialog.tsx
+++ b/client/src/pages/printing/spoolQrCodePrintingDialog.tsx
@@ -10,6 +10,7 @@ import { useSavedState } from "../../utils/saveload";
import { useGetSpoolsByIds } from "../spools/functions";
import { ISpool } from "../spools/model";
import {
+ getConfiguredBaseUrl,
SpoolQRCodePrintSettings,
renderLabelContents,
useGetPrintSettings as useGetPrintPresets,
@@ -23,13 +24,13 @@ interface SpoolQRCodePrintingDialog {
spoolIds: number[];
}
+// Adapt spool records into the generic QR print dialog while keeping spool print
+// presets isolated from the export-specific preset buckets.
const SpoolQRCodePrintingDialog = ({ spoolIds }: SpoolQRCodePrintingDialog) => {
const t = useTranslate();
const baseUrlSetting = useGetSetting("base_url");
- const baseUrlRoot =
- baseUrlSetting.data?.value !== undefined && JSON.parse(baseUrlSetting.data?.value) !== ""
- ? JSON.parse(baseUrlSetting.data?.value)
- : window.location.origin;
+ // Accept both JSON-backed settings and legacy plain strings so old `base_url` values do not crash the dialog.
+ const baseUrlRoot = getConfiguredBaseUrl(baseUrlSetting.data?.value, window.location.origin);
const [messageApi, contextHolder] = message.useMessage();
const [useHTTPUrl, setUseHTTPUrl] = useSavedState("print-useHTTPUrl", false);
@@ -40,11 +41,10 @@ const SpoolQRCodePrintingDialog = ({ spoolIds }: SpoolQRCodePrintingDialog) => {
})
.filter((item) => item !== null) as ISpool[];
- // Selected preset state
const [selectedPresetState, setSelectedPresetState] = useSavedState("selectedPreset", undefined);
- // Keep a local copy of the settings which is what's actually displayed. Use the remote state only for saving.
- // This decouples the debounce stuff from the UI
+ // Edit a local preset copy first so the form stays responsive and only persists to
+ // saved settings when the user explicitly clicks save.
const [localPresets, setLocalPresets] = useState();
const remotePresets = useGetPrintPresets();
const setRemotePresets = useSetPrintPresets();
@@ -53,10 +53,9 @@ const SpoolQRCodePrintingDialog = ({ spoolIds }: SpoolQRCodePrintingDialog) => {
const savePresetsRemote = () => {
if (!localPresets) return;
- setRemotePresets(localPresets);
+ setRemotePresets.mutate(localPresets);
};
- // Functions to update settings
const addNewPreset = () => {
if (!localOrRemotePresets) return;
const newId = uuidv4();
@@ -98,10 +97,8 @@ const SpoolQRCodePrintingDialog = ({ spoolIds }: SpoolQRCodePrintingDialog) => {
setSelectedPresetState(undefined);
};
- // Initialize presets
let curPreset: SpoolQRCodePrintSettings;
if (localOrRemotePresets === undefined) {
- // DB not loaded yet, use a temporary one
curPreset = {
labelSettings: {
printSettings: {
@@ -111,41 +108,31 @@ const SpoolQRCodePrintingDialog = ({ spoolIds }: SpoolQRCodePrintingDialog) => {
},
};
} else {
- // DB is loaded, find the selected setting
if (localOrRemotePresets.length === 0) {
- // DB loaded, but no settings found, add a new one and select it
+ // First-time print users should land in an editable preset immediately instead of
+ // an empty dialog with no selected settings object.
const newSetting = addNewPreset();
if (!newSetting) {
console.error("Error adding new setting, this should never happen");
return;
}
- // Mutate the allPrintSettings list so that the rest of the UI will work fine
localOrRemotePresets.push(newSetting);
curPreset = newSetting;
} else {
- // DB loaded and at least 1 setting exists
if (!selectedPresetState) {
- // No setting has been selected, select the first one
curPreset = localOrRemotePresets[0];
setSelectedPresetState(localOrRemotePresets[0].labelSettings.printSettings.id);
} else {
- // A setting has been selected, find it
const foundSetting = localOrRemotePresets.find(
(settings) => settings.labelSettings.printSettings.id === selectedPresetState,
);
if (foundSetting) {
curPreset = foundSetting;
} else {
- // Selected setting not found, select a temp one
- curPreset = {
- labelSettings: {
- printSettings: {
- id: "TEMP",
- name: t("printing.generic.newSetting"),
- },
- },
- };
+ // Recover to the first saved preset when the remembered selection no longer exists.
+ curPreset = localOrRemotePresets[0];
+ setSelectedPresetState(localOrRemotePresets[0].labelSettings.printSettings.id);
}
}
}
@@ -227,6 +214,8 @@ Spool Weight: {filament.spool_weight} g
});
}
+ // Expose spool, filament, and vendor placeholders because the same tag picker drives
+ // preview text and printed label templates.
const templateTags = [...spoolTags, ...filamentTags, ...vendorTags];
return (
@@ -247,7 +236,7 @@ Spool Weight: {filament.spool_weight} g
}}
extraSettingsStart={
<>
-
+