Browse Source

fix(modal): gcode_injection checkbox toggles cleanly on single prints (#1852)

    PrintModal carried a useEffect that reset scheduleOptions.gcodeInjection
    to false whenever mode === 'create' AND effectiveQuantity <= 1. The
    comment claimed the checkbox only renders for quantity > 1, but the
    actual render gate in ScheduleOptions is just hasGcodeSnippets — no
    quantity check. So with snippets configured + quantity = 1 (the OP
    scenario): user clicks the checkbox, React updates state to true,
    the parent's useEffect immediately sees effectiveQuantity <= 1 and
    resets to false, and the checkbox appears un-clickable. Edit-queue-
    item mode worked because mode !== 'create' short-circuited the reset.

    Drop the effectiveQuantity <= 1 clause from the reset. Keep the
    !settings?.gcode_snippets half as the legitimate cleanup for the
    "admin removes all snippets while modal is open" case. The scheduler
    reads item.gcode_injection per queue item regardless of batch size,
    so single prints can inject too.
maziggy 2 months ago
parent
commit
82af3cc0fe

File diff suppressed because it is too large
+ 0 - 0
CHANGELOG.md


+ 46 - 0
frontend/src/__tests__/components/PrintModal.test.tsx

@@ -1197,6 +1197,52 @@ describe('PrintModal', () => {
       expect(queueCalls[0].quantity).toBe(3);
     });
 
+    it('quantity 1 + snippets configured: checkbox toggles cleanly (#1852)', async () => {
+      // The previous reset effect in PrintModal/index.tsx silently flipped
+      // gcodeInjection back to false whenever effectiveQuantity <= 1 in create
+      // mode, so a single print with snippets configured couldn't tick the
+      // checkbox — every click visibly un-ticked itself within the next render.
+      // The scheduler reads item.gcode_injection per queue item regardless of
+      // batch size, so quantity 1 must be allowed too.
+      const queueCalls: Record<string, unknown>[] = [];
+      server.use(
+        withSnippets(),
+        http.post('/api/v1/queue/', async ({ request }) => {
+          queueCalls.push((await request.json()) as Record<string, unknown>);
+          return HttpResponse.json({ id: queueCalls.length, status: 'pending' });
+        }),
+      );
+
+      const user = userEvent.setup();
+      render(
+        <PrintModal
+          mode="create"
+          archiveId={1}
+          archiveName="Benchy"
+          initialSelectedPrinterIds={[1]}
+          onClose={mockOnClose}
+          onSuccess={mockOnSuccess}
+        />
+      );
+
+      // Quantity stays at default (1) — this is the OP's exact scenario.
+      const checkbox = (await screen.findByLabelText(/inject auto-print/i)) as HTMLInputElement;
+      expect(checkbox.checked).toBe(false);
+
+      await user.click(checkbox);
+      // CRITICAL: after the click the checkbox must stay checked. Under the
+      // pre-fix reset effect, the parent state would flip back to false within
+      // a render and the displayed `checked` attribute would track it.
+      await waitFor(() => expect(checkbox.checked).toBe(true));
+
+      await user.click(document.querySelector('button[type="submit"]') as HTMLElement);
+
+      await waitFor(() => expect(queueCalls.length).toBe(1));
+      // The injection flag actually reaches the API — pre-fix the parent reset
+      // would have stripped it before submit.
+      expect(queueCalls[0].gcode_injection).toBe(true);
+    });
+
     it('injection OFF queues all copies through the scheduler path', async () => {
       const queueCalls: Record<string, unknown>[] = [];
       server.use(

+ 10 - 10
frontend/src/components/PrintModal/index.tsx

@@ -940,19 +940,19 @@ export function PrintModal({
   // Quantity only applies for single-printer or model-based assignment (not multi-printer)
   const effectiveQuantity = (assignmentMode === 'printer' && selectedPrinters.length > 1) ? 1 : quantity;
 
-  // Keep scheduleOptions.gcodeInjection in sync with the checkbox's render
-  // condition. The checkbox only renders for create + snippets configured +
-  // quantity > 1, so if the user ticks it at quantity 2 then drops back to 1
-  // the box hides but the state stays true.
+  // Clear gcode_injection if the admin removes all snippets while the modal
+  // is open — the checkbox itself hides via hasGcodeSnippets in
+  // ScheduleOptions, but the boolean would otherwise stay true and ship to
+  // the API. The previous gate also reset the flag whenever effectiveQuantity
+  // dropped to <= 1, which silently un-ticked the checkbox on every single-
+  // print create flow (#1852). The scheduler reads item.gcode_injection per
+  // queue item regardless of batch size, so there's no underlying reason for
+  // the quantity-1 case to be blocked.
   useEffect(() => {
-    if (
-      mode === 'create' &&
-      scheduleOptions.gcodeInjection &&
-      (effectiveQuantity <= 1 || !settings?.gcode_snippets)
-    ) {
+    if (mode === 'create' && scheduleOptions.gcodeInjection && !settings?.gcode_snippets) {
       setScheduleOptions((opts) => ({ ...opts, gcodeInjection: false }));
     }
-  }, [mode, effectiveQuantity, settings?.gcode_snippets, scheduleOptions.gcodeInjection]);
+  }, [mode, settings?.gcode_snippets, scheduleOptions.gcodeInjection]);
 
   // Modal title and action button text based on mode
   const getModalConfig = () => {

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CWQmJXDv.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-bqgSupVY.js"></script>
+    <script type="module" crossorigin src="/assets/index-CWQmJXDv.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-BxVhuRti.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff