Sfoglia il codice sorgente

fix(inventory): assign-spool toast no longer claims AMS configured when slot was empty (#1680)

  Backend correctly defers MQTT configuration when the AMS slot is empty
  at assign time (state ∈ {9, 10}) — firmware drops the push silently;
  on_ams_change replays the config once a spool is detected. The
  response carries pending_config=true to communicate that. The
  printer-card AssignSpoolModal was ignoring the flag and always
  showing "Spool assigned and AMS slot configured" — the SpoolBuddy
  modal has handled this since it shipped. Now mirrors the same
  branch and shows "Assigned. Slot will configure when you insert
  the spool." when pending_config is true.
maziggy 3 mesi fa
parent
commit
8957cfaeef

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


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

@@ -18,6 +18,15 @@ vi.mock('../../api/client', () => ({
   },
 }));
 
+const mockShowToast = vi.fn();
+vi.mock('../../contexts/ToastContext', async (importOriginal) => {
+  const actual = await importOriginal<typeof import('../../contexts/ToastContext')>();
+  return {
+    ...actual,
+    useToast: () => ({ showToast: mockShowToast }),
+  };
+});
+
 const defaultProps = {
   isOpen: true,
   onClose: vi.fn(),
@@ -323,6 +332,88 @@ describe('AssignSpoolModal', () => {
       expect(api.refreshPrinterStatus).toHaveBeenCalledWith(7);
     });
   });
+
+  // #1680: backend returns `pending_config=true` when the AMS slot was empty
+  // at assign time (firmware drops the MQTT push silently; on_ams_change
+  // re-fires the config when filament is detected). The toast MUST reflect
+  // that the slot hasn't been configured yet — saying "AMS slot configured"
+  // reads as a lie and is what the reporter hit.
+  it('shows the pending-insert toast when backend returns pending_config=true (#1680)', async () => {
+    const { default: userEvent } = await import('@testing-library/user-event');
+    const user = userEvent.setup();
+
+    (api.getSpools as ReturnType<typeof vi.fn>).mockResolvedValue([manualSpool]);
+    (api.assignSpool as ReturnType<typeof vi.fn>).mockResolvedValue({
+      id: 1, spool_id: 1, printer_id: 7, ams_id: 0, tray_id: 0,
+      pending_config: true,
+    });
+
+    render(
+      <AssignSpoolModal
+        {...defaultProps}
+        printerId={7}
+        trayInfo={{ type: 'PLA', material: 'PLA', profile: 'PLA', color: 'FF0000', location: 'AMS 1 - Slot 1' }}
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByText(/Polymaker/)).toBeInTheDocument();
+    });
+    await user.click(screen.getByText(/Polymaker/));
+    await user.click(screen.getByRole('button', { name: /assign spool/i }));
+
+    await waitFor(() => {
+      expect(mockShowToast).toHaveBeenCalledWith(
+        expect.stringContaining('Slot will configure when you insert the spool'),
+        'success',
+      );
+    });
+    // And the "AMS slot configured" toast must NOT also have fired.
+    expect(mockShowToast).not.toHaveBeenCalledWith(
+      expect.stringContaining('AMS slot configured'),
+      expect.anything(),
+    );
+  });
+
+  // Counterpart guard: when the backend did configure the slot
+  // (pending_config=false or absent), the existing success toast still
+  // fires. Without this regression test, the #1680 fix could silently
+  // overshoot and label every assign as pending.
+  it('shows the configured toast when backend returns pending_config=false (#1680)', async () => {
+    const { default: userEvent } = await import('@testing-library/user-event');
+    const user = userEvent.setup();
+
+    (api.getSpools as ReturnType<typeof vi.fn>).mockResolvedValue([manualSpool]);
+    (api.assignSpool as ReturnType<typeof vi.fn>).mockResolvedValue({
+      id: 1, spool_id: 1, printer_id: 7, ams_id: 0, tray_id: 0,
+      pending_config: false,
+    });
+
+    render(
+      <AssignSpoolModal
+        {...defaultProps}
+        printerId={7}
+        trayInfo={{ type: 'PLA', material: 'PLA', profile: 'PLA', color: 'FF0000', location: 'AMS 1 - Slot 1' }}
+      />
+    );
+
+    await waitFor(() => {
+      expect(screen.getByText(/Polymaker/)).toBeInTheDocument();
+    });
+    await user.click(screen.getByText(/Polymaker/));
+    await user.click(screen.getByRole('button', { name: /assign spool/i }));
+
+    await waitFor(() => {
+      expect(mockShowToast).toHaveBeenCalledWith(
+        expect.stringContaining('AMS slot configured'),
+        'success',
+      );
+    });
+    expect(mockShowToast).not.toHaveBeenCalledWith(
+      expect.stringContaining('Slot will configure when you insert'),
+      expect.anything(),
+    );
+  });
 });
 
 describe('AssignSpoolModal — Spoolman enabled (T-Gap 7)', () => {

+ 13 - 1
frontend/src/components/AssignSpoolModal.tsx

@@ -150,7 +150,19 @@ export function AssignSpoolModal({ isOpen, onClose, printerId, amsId, trayId, tr
       });
       queryClient.invalidateQueries({ queryKey: ['spool-assignments'] });
       nudgePrinterRepublish();
-      showToast(t('inventory.assignSuccess'), 'success');
+      // When the AMS slot was empty at assign time (tray_state ∈ {9, 10}), the
+      // backend persists the assignment but deliberately skips the MQTT
+      // `ams_filament_setting` push because Bambu firmware drops it silently
+      // for empty slots. `on_ams_change` re-fires the configuration once a
+      // spool is detected in the slot (#1680). The success-but-pending case
+      // gets a distinct toast so the user understands the slot hasn't been
+      // configured on the printer yet — saying "AMS slot configured" reads
+      // as a lie in that state. Mirror of `spoolbuddy/AssignToAmsModal.tsx`,
+      // which has handled this since the SpoolBuddy assign flow shipped.
+      const toastKey = newAssignment.pending_config
+        ? 'inventory.assignPendingInsert'
+        : 'inventory.assignSuccess';
+      showToast(t(toastKey), 'success');
       setShowMismatchConfirm(false);
       setPendingAssignId(null);
       setMismatchDetails(null);

+ 1 - 0
frontend/src/i18n/locales/de.ts

@@ -3731,6 +3731,7 @@ export default {
     assignSpool: 'Spule zuweisen',
     unassignSpool: 'Zuweisung aufheben',
     assignSuccess: 'Spule zugewiesen und AMS-Slot konfiguriert',
+    assignPendingInsert: 'Zugewiesen. Slot wird beim Einsetzen der Spule konfiguriert.',
     assignFailed: 'Spulenzuweisung fehlgeschlagen',
     selectSpool: 'Wählen Sie eine Spule für diesen Slot',
     assigned: 'Zugewiesen',

+ 1 - 0
frontend/src/i18n/locales/en.ts

@@ -3739,6 +3739,7 @@ export default {
     assignSpool: 'Assign Spool',
     unassignSpool: 'Unassign',
     assignSuccess: 'Spool assigned and AMS slot configured',
+    assignPendingInsert: 'Assigned. Slot will configure when you insert the spool.',
     assignFailed: 'Failed to assign spool',
     selectSpool: 'Select a spool to assign to this slot',
     assigned: 'Assigned',

+ 1 - 0
frontend/src/i18n/locales/es.ts

@@ -3735,6 +3735,7 @@ export default {
     assignSpool: 'Asignar bobina',
     unassignSpool: 'Desasignar',
     assignSuccess: 'Bobina asignada y ranura del AMS configurada',
+    assignPendingInsert: 'Asignada. La ranura se configurará cuando inserte la bobina.',
     assignFailed: 'Error al asignar la bobina',
     selectSpool: 'Seleccione una bobina para asignar a esta ranura',
     assigned: 'Asignada',

+ 1 - 0
frontend/src/i18n/locales/fr.ts

@@ -3719,6 +3719,7 @@ export default {
     assignSpool: 'Assigner Bobine',
     unassignSpool: 'Désassigner',
     assignSuccess: 'Bobine assignée et slot AMS configuré',
+    assignPendingInsert: 'Assigné. Le slot sera configuré lors de l\'insertion de la bobine.',
     assignFailed: 'Échec assignation',
     selectSpool: 'Choisir une bobine pour ce slot',
     assigned: 'Assigné',

+ 1 - 0
frontend/src/i18n/locales/it.ts

@@ -3718,6 +3718,7 @@ export default {
     assignSpool: 'Assegna Bobina',
     unassignSpool: 'Scollega',
     assignSuccess: 'Bobina assegnata e slot AMS configurato',
+    assignPendingInsert: 'Assegnato. Lo slot verrà configurato all\'inserimento della bobina.',
     assignFailed: 'Assegnazione bobina fallita',
     selectSpool: 'Seleziona una bobina da assegnare a questo slot',
     assigned: 'Assegnato',

+ 1 - 0
frontend/src/i18n/locales/ja.ts

@@ -3730,6 +3730,7 @@ export default {
     assignSpool: 'スプールを割り当て',
     unassignSpool: '割り当て解除',
     assignSuccess: 'スプールを割り当て、AMSスロットを設定しました',
+    assignPendingInsert: '割り当てました。スプールを挿入したときにスロットが設定されます。',
     assignFailed: 'スプールの割り当てに失敗しました',
     selectSpool: 'このスロットに割り当てるスプールを選択',
     assigned: '割り当て済み',

+ 1 - 0
frontend/src/i18n/locales/ko.ts

@@ -3519,6 +3519,7 @@ export default {
     assignSpool: '스풀 할당',
     unassignSpool: '할당 해제',
     assignSuccess: '스풀 할당 및 AMS 슬롯 구성됨',
+    assignPendingInsert: '할당됨. 스풀을 삽입하면 슬롯이 구성됩니다.',
     assignFailed: '스풀 할당 실패',
     selectSpool: '이 슬롯에 할당할 스풀 선택',
     assigned: '할당됨',

+ 1 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -3718,6 +3718,7 @@ export default {
     assignSpool: 'Atribuir Carretel',
     unassignSpool: 'Desatribuir',
     assignSuccess: 'Carretel atribuído e slot AMS configurado',
+    assignPendingInsert: 'Atribuído. O slot será configurado quando você inserir o carretel.',
     assignFailed: 'Falha ao atribuir carretel',
     selectSpool: 'Selecione um carretel para atribuir a este slot',
     assigned: 'Atribuído',

+ 1 - 0
frontend/src/i18n/locales/tr.ts

@@ -3718,6 +3718,7 @@ export default {
     assignSpool: 'Makara Ata',
     unassignSpool: 'Atamayı Kaldır',
     assignSuccess: 'Makara atandı ve AMS yuvası yapılandırıldı',
+    assignPendingInsert: 'Atandı. Makarayı yerleştirdiğinizde yuva yapılandırılacak.',
     assignFailed: 'Makara atanamadı',
     selectSpool: 'Bu yuvaya atamak için bir makara seçin',
     assigned: 'Atandı',

+ 1 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -3718,6 +3718,7 @@ export default {
     assignSpool: '分配耗材',
     unassignSpool: '取消分配',
     assignSuccess: '耗材已分配,AMS 槽位已配置',
+    assignPendingInsert: '已分配。插入耗材后将配置槽位。',
     assignFailed: '分配耗材失败',
     assignMismatchTitle: '材料不匹配',
     assignMismatchMessage: '所选线轴材料 "{{spoolMaterial}}" 与 {{location}} 的料槽材料 "{{trayMaterial}}" 不匹配。仍要分配吗?',

+ 1 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -3718,6 +3718,7 @@ export default {
     assignSpool: '分配耗材',
     unassignSpool: '取消分配',
     assignSuccess: '耗材已分配,AMS 槽位已設定',
+    assignPendingInsert: '已分配。插入耗材後將設定槽位。',
     assignFailed: '分配耗材失敗',
     assignMismatchTitle: '材料不符',
     assignMismatchMessage: '所選料盤材料 "{{spoolMaterial}}" 與 {{location}} 的料槽材料 "{{trayMaterial}}" 不符。仍要分配嗎?',

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CqscW2CN.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-B-fcW9HO.js"></script>
+    <script type="module" crossorigin src="/assets/index-CqscW2CN.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DgecYhis.css">
   </head>
   <body>

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