Browse Source

[Fix]: Update location when linking/unlinking to/from Spoolman (#669)

* Enhance link spool functionality with additional printer and AMS details

* Refactor linkSpool function to accept detailed context object for improved spooling integration

* Enhance LinkSpoolModal to include amsName in props and linkSpool mutation for improved functionality

* Add amsName prop to PrinterCard for enhanced spool linking functionality

* Updates LinkSpoolModal test to work with recent spool location update changes

* Adds clear_location to unlink testing

* Refactor LinkSpoolRequest to remove ams_name and update location generation logic for improved clarity

* Remove amsName from LinkSpoolModal and PrinterCard for cleaner API integration

* Remove amsName from LinkSpoolModal test props and update linkSpool mock response for improved clarity
Dakota G 2 months ago
parent
commit
be2ddbee5d

+ 18 - 3
backend/app/api/routes/spoolman.py

@@ -1,7 +1,7 @@
 """Spoolman integration API routes."""
 
-import logging
 import json
+import logging
 
 from fastapi import APIRouter, Depends, HTTPException
 from pydantic import BaseModel
@@ -669,6 +669,9 @@ class LinkSpoolRequest(BaseModel):
     spool_tag: str | None = None
     tray_uuid: str | None = None
     tag_uid: str | None = None
+    printer_id: int | None = None
+    ams_id: int | None = None
+    tray_id: int | None = None
 
 
 @router.post("/spools/{spool_id}/link")
@@ -708,12 +711,23 @@ async def link_spool(
     if set(spool_tag) == {"0"}:
         raise HTTPException(status_code=400, detail="Invalid spool tag format (all-zero tag is not linkable)")
 
+    spool_tag = spool_tag.upper()
+
+    # Build location like: "{Printer Name} - {AMS Name} {Slot Number}"
+    location: str | None = None
+    if request.printer_id is not None and request.ams_id is not None and request.tray_id is not None:
+        printer_result = await db.execute(select(Printer).where(Printer.id == request.printer_id))
+        printer = printer_result.scalar_one_or_none()
+        if not printer:
+            raise HTTPException(status_code=404, detail="Printer not found")
+
+        location = f"{printer.name} - {client.convert_ams_slot_to_location(request.ams_id, request.tray_id)}"
+
     # Update spool with tag
     # Note: Spoolman extra field values must be valid JSON, so we encode the string
-    import json
-
     result = await client.update_spool(
         spool_id=spool_id,
+        location=location,
         extra={"tag": json.dumps(spool_tag)},
     )
 
@@ -748,6 +762,7 @@ async def unlink_spool(
 
     result = await client.update_spool(
         spool_id=spool_id,
+        clear_location=True,
         extra={"tag": json.dumps("")},
     )
 

+ 1 - 0
backend/tests/integration/test_spoolman_api.py

@@ -479,6 +479,7 @@ class TestSpoolmanAPI:
 
         mock_spoolman_client.update_spool.assert_called_once_with(
             spool_id=1,
+            clear_location=True,
             extra={"tag": '""'},
         )
 

+ 9 - 2
frontend/src/__tests__/components/LinkSpoolModal.test.tsx

@@ -54,6 +54,7 @@ describe('LinkSpoolModal', () => {
       filament_material: 'PLA',
       filament_color_hex: 'FF0000',
       remaining_weight: 800,
+      location: null,
     },
     {
       id: 2,
@@ -61,13 +62,14 @@ describe('LinkSpoolModal', () => {
       filament_material: 'PETG',
       filament_color_hex: '0000FF',
       remaining_weight: 500,
+      location: null,
     },
   ];
 
   beforeEach(() => {
     vi.clearAllMocks();
     vi.mocked(api.getUnlinkedSpools).mockResolvedValue(mockSpools);
-    vi.mocked(api.linkSpool).mockResolvedValue({});
+    vi.mocked(api.linkSpool).mockResolvedValue({ success: true, message: 'ok' });
   });
 
   describe('rendering', () => {
@@ -125,7 +127,12 @@ describe('LinkSpoolModal', () => {
       fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
 
       await waitFor(() => {
-        expect(api.linkSpool).toHaveBeenCalledWith(1, 'A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4');
+        expect(api.linkSpool).toHaveBeenCalledWith(1, {
+          spoolTag: 'ABCD1234',
+          printerId: 1,
+          amsId: 0,
+          trayId: 0,
+        });
       });
     });
 

+ 15 - 2
frontend/src/api/client.ts

@@ -3527,10 +3527,23 @@ export const api = {
     request<UnlinkedSpool[]>('/spoolman/spools/unlinked'),
   getLinkedSpools: () =>
     request<LinkedSpoolsMap>('/spoolman/spools/linked'),
-  linkSpool: (spoolId: number, spoolTag: string) =>
+  linkSpool: (
+    spoolId: number,
+    context: {
+      spoolTag: string;
+      printerId: number;
+      amsId: number;
+      trayId: number;
+    }
+  ) =>
     request<{ success: boolean; message: string }>(`/spoolman/spools/${spoolId}/link`, {
       method: 'POST',
-      body: JSON.stringify({ spool_tag: spoolTag }),
+      body: JSON.stringify({
+        spool_tag: context.spoolTag,
+        printer_id: context.printerId,
+        ams_id: context.amsId,
+        tray_id: context.trayId,
+      }),
     }),
   unlinkSpool: (spoolId: number) =>
     request<{ success: boolean; message: string }>(`/spoolman/spools/${spoolId}/unlink`, {

+ 6 - 1
frontend/src/components/LinkSpoolModal.tsx

@@ -46,7 +46,12 @@ export function LinkSpoolModal({ isOpen, onClose, tagUid, trayUuid, printerId, a
 
   const linkMutation = useMutation({
     mutationFn: (spoolId: number) =>
-      api.linkSpool(spoolId, spoolTag!),
+      api.linkSpool(spoolId, {
+        spoolTag: spoolTag!,
+        printerId,
+        amsId,
+        trayId,
+      }),
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ['unlinked-spools'] });
       queryClient.invalidateQueries({ queryKey: ['linked-spools'] });