From be2ddbee5db935a9811b1418c23acddae98feeb5 Mon Sep 17 00:00:00 2001 From: Dakota G <13515931+shrunbr@users.noreply.github.com> Date: Wed, 11 Mar 2026 03:38:14 -0500 Subject: [PATCH] [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 --- backend/app/api/routes/spoolman.py | 21 ++++++++++++++++--- .../tests/integration/test_spoolman_api.py | 1 + .../components/LinkSpoolModal.test.tsx | 11 ++++++++-- frontend/src/api/client.ts | 17 +++++++++++++-- frontend/src/components/LinkSpoolModal.tsx | 7 ++++++- 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/backend/app/api/routes/spoolman.py b/backend/app/api/routes/spoolman.py index 125b3861d..b8de1927d 100644 --- a/backend/app/api/routes/spoolman.py +++ b/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("")}, ) diff --git a/backend/tests/integration/test_spoolman_api.py b/backend/tests/integration/test_spoolman_api.py index 0c2c3d1bb..6ad9e12de 100644 --- a/backend/tests/integration/test_spoolman_api.py +++ b/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": '""'}, ) diff --git a/frontend/src/__tests__/components/LinkSpoolModal.test.tsx b/frontend/src/__tests__/components/LinkSpoolModal.test.tsx index 7792ae772..8aa3129fe 100644 --- a/frontend/src/__tests__/components/LinkSpoolModal.test.tsx +++ b/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, + }); }); }); diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 79829f9ed..2fc744c72 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -3527,10 +3527,23 @@ export const api = { request('/spoolman/spools/unlinked'), getLinkedSpools: () => request('/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`, { diff --git a/frontend/src/components/LinkSpoolModal.tsx b/frontend/src/components/LinkSpoolModal.tsx index e15356e7b..25ec4129a 100644 --- a/frontend/src/components/LinkSpoolModal.tsx +++ b/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'] });