Przeglądaj źródła

● fix(ams): restore Configure/Assign actions on reset slots + relax Assign Spool filtering (#1047)

  Three related fixes reported together:

  (1) After resetting an AMS slot, the printer card showed "Empty Slot"
  with no Configure or Assign Spool actions while SpoolBuddy's AMS page
  still let the user re-configure the same slot. Commit c9efa4b8 (#784)
  added a `tray?.state === 10` gate to the EmptySlotHoverCard actions,
  intended to hide them on physically-empty slots (state=9). In practice
  firmware often reports state=9 (or omits state entirely) after a
  user-initiated reset even when a spool is still present, so the gate
  hit the wrong case. The gate was redundant anyway — EmptySlotHoverCard
  only renders when tray_type is empty — so it's removed at both the
  standard-AMS and AMS-HT render paths.

  (2) After configuring a slot with a Generic profile, the Assign Spool
  modal hid manually-added inventory spools even when material matched,
  unless the user flipped "Show all spools". The filter required exact
  slicer_filament_name equality, which manually-added spools don't
  populate. Filter now prefers exact slicer-profile match when both
  sides have one, and falls back to partial material match in either
  direction (so a "PLA" spool shows up for a "PLA Basic" slot).

  (3) On assign, the mismatch dialog fired on every Generic spool
  because Bambu Studio / OrcaSlicer profile names carry an @printer
  nozzle (variant) qualifier while the tray stores the bare base name.
  Both the filter and checkProfileMatch now strip everything from @
  onward before comparing.

  Adds 3 regression tests covering each path.
maziggy 1 miesiąc temu
rodzic
commit
bc895dff6a

Plik diff jest za duży
+ 0 - 0
CHANGELOG.md


+ 105 - 0
frontend/src/__tests__/components/AssignSpoolModal.test.tsx

@@ -134,4 +134,109 @@ describe('AssignSpoolModal', () => {
       expect(screen.getByText(/No manually added spools/i)).toBeInTheDocument();
     });
   });
+
+  it('lists spool with no slicer profile when material matches the tray (#1047)', async () => {
+    const spoolWithoutSlicerProfile = {
+      id: 10,
+      material: 'PLA',
+      subtype: 'Basic',
+      brand: 'Devil Design',
+      color_name: 'Red',
+      rgba: 'FF0000FF',
+      label_weight: 1000,
+      weight_used: 0,
+      tag_uid: null,
+      tray_uuid: null,
+      slicer_filament_name: null,
+      slicer_filament: null,
+    };
+    (api.getSpools as ReturnType<typeof vi.fn>).mockResolvedValue([spoolWithoutSlicerProfile]);
+
+    render(
+      <AssignSpoolModal
+        {...defaultProps}
+        trayInfo={{
+          type: 'PLA',
+          material: 'PLA',
+          profile: 'Devil Design PLA Basic',
+          color: 'FF0000',
+          location: 'AMS 1 - Slot 1',
+        }}
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByText(/Devil Design/)).toBeInTheDocument();
+    });
+  });
+
+  it('lists spool with shorter material when tray advertises a qualified variant (#1047)', async () => {
+    // Spool.material = "PLA", tray material = "PLA Basic" — partial match in either direction.
+    const shortMaterialSpool = {
+      id: 11,
+      material: 'PLA',
+      subtype: 'Basic',
+      brand: 'Devil Design',
+      color_name: 'Red',
+      rgba: 'FF0000FF',
+      label_weight: 1000,
+      weight_used: 0,
+      tag_uid: null,
+      tray_uuid: null,
+      slicer_filament_name: null,
+      slicer_filament: null,
+    };
+    (api.getSpools as ReturnType<typeof vi.fn>).mockResolvedValue([shortMaterialSpool]);
+
+    render(
+      <AssignSpoolModal
+        {...defaultProps}
+        trayInfo={{
+          type: 'PLA Basic',
+          material: 'PLA Basic',
+          color: 'FF0000',
+          location: 'AMS 1 - Slot 1',
+        }}
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByText(/Devil Design/)).toBeInTheDocument();
+    });
+  });
+
+  it('lists spool whose slicer profile has an @printer qualifier that strips to the tray profile (#1047)', async () => {
+    const qualifiedProfileSpool = {
+      id: 12,
+      material: 'PLA',
+      subtype: 'Basic',
+      brand: 'Devil Design',
+      color_name: 'Red',
+      rgba: 'FF0000FF',
+      label_weight: 1000,
+      weight_used: 0,
+      tag_uid: null,
+      tray_uuid: null,
+      slicer_filament_name: 'Devil Design PLA Basic @Bambu Lab H2D 0.4 nozzle (Custom)',
+    };
+    // Use a non-matching material to force the filter to rely on the profile path only.
+    (api.getSpools as ReturnType<typeof vi.fn>).mockResolvedValue([qualifiedProfileSpool]);
+
+    render(
+      <AssignSpoolModal
+        {...defaultProps}
+        trayInfo={{
+          type: 'ABS',
+          material: 'ABS',
+          profile: 'Devil Design PLA Basic',
+          color: 'FF0000',
+          location: 'AMS 1 - Slot 1',
+        }}
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByText(/Devil Design/)).toBeInTheDocument();
+    });
+  });
 });

+ 28 - 7
frontend/src/components/AssignSpoolModal.tsx

@@ -111,12 +111,18 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
     return 'none';
   };
 
+  // Bambu Studio / OrcaSlicer profile names carry a printer/nozzle/variant qualifier after
+  // `@` (e.g. "Devil Design PLA Basic @Bambu Lab H2D 0.4 nozzle (Custom)"), while the tray's
+  // profile is typically the bare base name. Strip the qualifier before comparing so identical
+  // base profiles don't trigger a mismatch warning (#1047).
+  const stripProfileQualifier = (value: string) => value.split('@')[0].trim();
+
   const checkProfileMatch = (
     spoolProfile: string | undefined | null,
     trayProfile: string | undefined | null
   ): boolean => {
-    const normalizedSpoolProfile = normalizeValue(spoolProfile);
-    const normalizedTrayProfile = normalizeValue(trayProfile);
+    const normalizedSpoolProfile = stripProfileQualifier(normalizeValue(spoolProfile));
+    const normalizedTrayProfile = stripProfileQualifier(normalizeValue(trayProfile));
 
     if (!normalizedSpoolProfile || !normalizedTrayProfile) return false;
 
@@ -138,14 +144,29 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
     !assignedSpoolIds.has(spool.id) && (isExternalSlot || (!spool.tag_uid && !spool.tray_uuid))
   );
 
-  // Filtering logic with toggle: search filter always applies, AMS tray profile filter is optional
+  // Filtering logic with toggle: search filter always applies, AMS tray profile filter is optional.
+  // Show a spool if EITHER the slicer profile matches exactly OR the material overlaps with the
+  // tray's material (partial-match both directions — "PLA" spool accepts a "PLA Basic" slot and
+  // vice versa). Manually-added inventory spools typically have no slicer_filament_name; gating
+  // on strict profile equality alone hid them even when the material matched (#1047).
   let filteredSpools = manualSpools;
   if (!disableFiltering) {
-    if (trayInfo?.profile || trayInfo?.type) {
-      const trayProfile = normalizeValue(trayInfo.profile || trayInfo.type);
+    const trayProfile = stripProfileQualifier(normalizeValue(trayInfo?.profile));
+    const trayMaterial = normalizeValue(trayInfo?.material || trayInfo?.type);
+    if (trayProfile || trayMaterial) {
       filteredSpools = filteredSpools?.filter((spool: InventorySpool) => {
-        const spoolProfile = normalizeValue(spool.slicer_filament_name || spool.slicer_filament);
-        return trayProfile && spoolProfile && spoolProfile === trayProfile;
+        const spoolProfile = stripProfileQualifier(normalizeValue(spool.slicer_filament_name || spool.slicer_filament));
+        const spoolMaterial = normalizeValue(spool.material);
+        if (trayProfile && spoolProfile && spoolProfile === trayProfile) return true;
+        if (trayMaterial && spoolMaterial) {
+          return (
+            spoolMaterial === trayMaterial ||
+            trayMaterial.includes(spoolMaterial) ||
+            spoolMaterial.includes(trayMaterial)
+          );
+        }
+        // Neither side has filterable info on whatever dimension remains — show it.
+        return !spoolProfile && !spoolMaterial;
       });
     }
   }

+ 8 - 8
frontend/src/pages/PrintersPage.tsx

@@ -3552,7 +3552,7 @@ function PrinterCard({
                                       </FilamentHoverCard>
                                     ) : (
                                       <EmptySlotHoverCard
-                                        configureSlot={tray?.state === 10 ? {
+                                        configureSlot={{
                                           enabled: hasPermission('printers:control'),
                                           onConfigure: () => setConfigureSlotModal({
                                             amsId: ams.id,
@@ -3560,8 +3560,8 @@ function PrinterCard({
                                             trayCount: ams.tray.length,
                                             extruderId: mappedExtruderId,
                                           }),
-                                        } : undefined}
-                                        inventory={tray?.state === 10 && !spoolmanEnabled ? {
+                                        }}
+                                        inventory={spoolmanEnabled ? undefined : {
                                           onAssignSpool: () => setAssignSpoolModal({
                                             printerId: printer.id,
                                             amsId: ams.id,
@@ -3572,7 +3572,7 @@ function PrinterCard({
                                               location: `${getAmsLabel(ams.id, ams.tray.length)} Slot ${slotIdx + 1}`,
                                             },
                                           }),
-                                        } : undefined}
+                                        }}
                                       >
                                         {slotVisual}
                                       </EmptySlotHoverCard>
@@ -3870,7 +3870,7 @@ function PrinterCard({
                                   </FilamentHoverCard>
                                 ) : (
                                   <EmptySlotHoverCard
-                                    configureSlot={tray?.state === 10 ? {
+                                    configureSlot={{
                                       enabled: hasPermission('printers:control'),
                                       onConfigure: () => setConfigureSlotModal({
                                         amsId: ams.id,
@@ -3878,8 +3878,8 @@ function PrinterCard({
                                         trayCount: ams.tray.length,
                                         extruderId: mappedExtruderId,
                                       }),
-                                    } : undefined}
-                                    inventory={tray?.state === 10 && !spoolmanEnabled ? {
+                                    }}
+                                    inventory={spoolmanEnabled ? undefined : {
                                       onAssignSpool: () => setAssignSpoolModal({
                                         printerId: printer.id,
                                         amsId: ams.id,
@@ -3890,7 +3890,7 @@ function PrinterCard({
                                           location: getAmsLabel(ams.id, ams.tray.length),
                                         },
                                       }),
-                                    } : undefined}
+                                    }}
                                   >
                                     {slotVisual}
                                   </EmptySlotHoverCard>

Plik diff jest za duży
+ 0 - 0
static/assets/index-C60hlRK5.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-BVXFAk6e.js"></script>
+    <script type="module" crossorigin src="/assets/index-C60hlRK5.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-CkAOuJaW.css">
   </head>
   <body>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików