mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
fix(ams): confirm drying start instead of trusting the firmware ack (#2533)
The Start Drying button gave no feedback at all and reported success on the strength of the MQTT ack alone. On a P1S the firmware answers ams_filament_drying with result=success and then silently declines, and P1-family firmware never publishes dry_sf_reason — so the #971 reason guard is inert there and the card just sat unchanged. Both drying mutations now toast on success. After a start, the card watches the unit's dry_status/dry_time (straight from the info bitmask, updated on every push); firmware reaches DryStatus 1 within seconds of a real start, so still sitting at zero 30s later means the cycle never began. Bambuddy now says so and names the two causes: AMS power adapter not connected, or the printer not idle. Unlike the dry_sf_reason guard this is model-agnostic.
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Feedback for the AMS drying start/stop buttons (#2533).
|
||||
*
|
||||
* The reporter's P1S accepted `ams_filament_drying` with result=success and
|
||||
* then never dried. Two gaps: the Start button gave no confirmation at all,
|
||||
* and Bambuddy treated the MQTT ack as proof the cycle had begun. These tests
|
||||
* cover the toast and the post-ack watcher that catches a printer which takes
|
||||
* the command and drops it.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from '../utils';
|
||||
import { PrintersPage } from '../../pages/PrintersPage';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../mocks/server';
|
||||
|
||||
const mockShowToast = vi.fn();
|
||||
vi.mock('../../contexts/ToastContext', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../contexts/ToastContext')>();
|
||||
return { ...actual, useToast: () => ({ showToast: mockShowToast }) };
|
||||
});
|
||||
|
||||
const mockPrinter = {
|
||||
id: 1,
|
||||
name: 'P1S',
|
||||
ip_address: '192.168.1.100',
|
||||
serial_number: '01P00A000000001',
|
||||
access_code: '12345678',
|
||||
model: 'P1S',
|
||||
enabled: true,
|
||||
nozzle_diameter: 0.4,
|
||||
nozzle_type: 'stainless_steel',
|
||||
location: 'Workshop',
|
||||
auto_archive: true,
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
const baseTray = {
|
||||
tray_color: 'FF0000FF',
|
||||
tray_type: 'PLA',
|
||||
tray_sub_brands: 'PLA Basic',
|
||||
tray_id_name: 'A00-R0',
|
||||
tray_info_idx: 'GFA00',
|
||||
remain: 80,
|
||||
k: 0.02,
|
||||
cali_idx: null,
|
||||
tag_uid: null,
|
||||
tray_uuid: null,
|
||||
nozzle_temp_min: 190,
|
||||
nozzle_temp_max: 230,
|
||||
drying_temp: null,
|
||||
drying_time: null,
|
||||
state: 3,
|
||||
};
|
||||
|
||||
/** AMS 2 Pro (n3f) on an idle printer — the reporter's hardware. */
|
||||
function makeStatus(dry: { dry_time: number; dry_status: number }) {
|
||||
return {
|
||||
connected: true,
|
||||
state: 'IDLE',
|
||||
progress: 0,
|
||||
layer_num: 0,
|
||||
total_layers: 0,
|
||||
temperatures: { nozzle: 25, bed: 25, chamber: 25 },
|
||||
remaining_time: 0,
|
||||
filename: null,
|
||||
wifi_signal: -29,
|
||||
speed_level: 2,
|
||||
supports_drying: true,
|
||||
vt_tray: [],
|
||||
ams: [
|
||||
{
|
||||
id: 0,
|
||||
humidity: 30,
|
||||
temp: 33,
|
||||
is_ams_ht: false,
|
||||
serial_number: 'AMS00',
|
||||
sw_ver: '03.00.21.29',
|
||||
dry_sub_status: 0,
|
||||
dry_sf_reason: [],
|
||||
module_type: 'n3f',
|
||||
...dry,
|
||||
tray: [
|
||||
{ id: 0, ...baseTray },
|
||||
{ id: 1, ...baseTray },
|
||||
{ id: 2, ...baseTray },
|
||||
{ id: 3, ...baseTray },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const IDLE = makeStatus({ dry_time: 0, dry_status: 0 });
|
||||
const DRYING = makeStatus({ dry_time: 720, dry_status: 2 });
|
||||
|
||||
/**
|
||||
* Open the drying popover and press Start. The card renders the AMS in two
|
||||
* layouts, so the flame icon appears more than once — either opens the same
|
||||
* popover.
|
||||
*/
|
||||
async function startDrying(user: ReturnType<typeof userEvent.setup>) {
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTitle('Start Drying').length).toBeGreaterThan(0);
|
||||
});
|
||||
await user.click(screen.getAllByTitle('Start Drying')[0]);
|
||||
await user.click(await screen.findByTestId('drying-start-confirm'));
|
||||
}
|
||||
|
||||
describe('PrintersPage - AMS drying feedback (#2533)', () => {
|
||||
beforeEach(() => {
|
||||
mockShowToast.mockClear();
|
||||
server.use(
|
||||
http.get('/api/v1/printers/', () => HttpResponse.json([mockPrinter])),
|
||||
http.get('/api/v1/queue/', () => HttpResponse.json([])),
|
||||
http.post('/api/v1/printers/:id/drying/start', () =>
|
||||
HttpResponse.json({ status: 'drying_started', ams_id: 0, temp: 45, duration: 12 }),
|
||||
),
|
||||
http.post('/api/v1/printers/:id/drying/stop', () =>
|
||||
HttpResponse.json({ status: 'drying_stopped', ams_id: 0 }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('confirms the start command with a toast', async () => {
|
||||
const user = userEvent.setup();
|
||||
server.use(http.get('/api/v1/printers/:id/status', () => HttpResponse.json(IDLE)));
|
||||
|
||||
render(<PrintersPage />);
|
||||
await startDrying(user);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Drying started', 'success');
|
||||
});
|
||||
});
|
||||
|
||||
it('confirms the stop command with a toast', async () => {
|
||||
const user = userEvent.setup();
|
||||
server.use(http.get('/api/v1/printers/:id/status', () => HttpResponse.json(DRYING)));
|
||||
|
||||
render(<PrintersPage />);
|
||||
|
||||
// While a cycle is live the same button becomes Stop Drying.
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTitle('Stop Drying').length).toBeGreaterThan(0);
|
||||
});
|
||||
await user.click(screen.getAllByTitle('Stop Drying')[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Drying stopped', 'success');
|
||||
});
|
||||
});
|
||||
|
||||
it('warns when the printer acks the command but the AMS never starts drying', async () => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
// Status keeps reporting dry_status 0 — the reporter's exact symptom.
|
||||
server.use(http.get('/api/v1/printers/:id/status', () => HttpResponse.json(IDLE)));
|
||||
|
||||
render(<PrintersPage />);
|
||||
await startDrying(user);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Drying started', 'success');
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(31_000);
|
||||
|
||||
expect(mockShowToast).toHaveBeenCalledWith(
|
||||
'The printer accepted the command but the AMS never started drying. Check that the AMS power adapter is connected and that the printer is idle.',
|
||||
'error',
|
||||
);
|
||||
});
|
||||
|
||||
it('stays quiet when the AMS does enter a drying cycle', async () => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
|
||||
|
||||
// Idle until the command lands, then the AMS reports a live cycle — which is
|
||||
// what the mutation's cache invalidation refetches.
|
||||
let started = false;
|
||||
server.use(
|
||||
http.get('/api/v1/printers/:id/status', () => HttpResponse.json(started ? DRYING : IDLE)),
|
||||
http.post('/api/v1/printers/:id/drying/start', () => {
|
||||
started = true;
|
||||
return HttpResponse.json({ status: 'drying_started', ams_id: 0, temp: 45, duration: 12 });
|
||||
}),
|
||||
);
|
||||
|
||||
render(<PrintersPage />);
|
||||
await startDrying(user);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith('Drying started', 'success');
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(31_000);
|
||||
|
||||
expect(mockShowToast).not.toHaveBeenCalledWith(expect.stringContaining('never started drying'), 'error');
|
||||
});
|
||||
});
|
||||
@@ -569,6 +569,9 @@ export default {
|
||||
notSupported: 'Trocknung nicht unterstützt',
|
||||
powerRequired: 'AMS-Netzteil anschließen, um Trocknung zu aktivieren',
|
||||
startingDrying: 'Trocknung wird gestartet...',
|
||||
toastStarted: 'Trocknung gestartet',
|
||||
toastStopped: 'Trocknung gestoppt',
|
||||
toastNotStarted: 'Der Drucker hat den Befehl angenommen, aber das AMS hat die Trocknung nicht gestartet. Prüfe, ob das AMS-Netzteil angeschlossen ist und der Drucker im Leerlauf ist.',
|
||||
stoppingDrying: 'Trocknung wird gestoppt...',
|
||||
rotateTray: 'Spule während der Trocknung drehen',
|
||||
rotateUnavailableReason: 'Nicht verfügbar — in diesem AMS ist ein Slot zum Druckkopf hin geladen. Die Spule ist durch den Zuführschlauch blockiert und kann nicht rotieren. Filament zuerst zurückziehen.',
|
||||
|
||||
@@ -572,6 +572,9 @@ export default {
|
||||
notSupported: 'Drying not supported',
|
||||
powerRequired: 'Connect AMS power adapter to enable drying',
|
||||
startingDrying: 'Starting drying...',
|
||||
toastStarted: 'Drying started',
|
||||
toastStopped: 'Drying stopped',
|
||||
toastNotStarted: 'The printer accepted the command but the AMS never started drying. Check that the AMS power adapter is connected and that the printer is idle.',
|
||||
stoppingDrying: 'Stopping drying...',
|
||||
rotateTray: 'Rotate spool during drying',
|
||||
rotateUnavailableReason: 'Unavailable — a slot in this AMS is loaded to the toolhead. The spool is locked by the feed tube and cannot rotate. Retract the filament first.',
|
||||
|
||||
@@ -569,6 +569,9 @@ export default {
|
||||
notSupported: 'Secado no compatible',
|
||||
powerRequired: 'Conecte el adaptador de corriente del AMS para activar el secado',
|
||||
startingDrying: 'Iniciando el secado...',
|
||||
toastStarted: 'Secado iniciado',
|
||||
toastStopped: 'Secado detenido',
|
||||
toastNotStarted: 'La impresora aceptó el comando, pero el AMS no inició el secado. Comprueba que el adaptador de corriente del AMS esté conectado y que la impresora esté inactiva.',
|
||||
stoppingDrying: 'Deteniendo el secado...',
|
||||
rotateTray: 'Girar la bobina durante el secado',
|
||||
rotateUnavailableReason: 'No disponible — un slot de este AMS está cargado hacia el cabezal. La bobina está bloqueada por el tubo de alimentación y no puede girar. Retira el filamento primero.',
|
||||
|
||||
@@ -569,6 +569,9 @@ export default {
|
||||
notSupported: 'Séchage non pris en charge',
|
||||
powerRequired: 'Brancher l\'adaptateur secteur AMS pour activer le séchage',
|
||||
startingDrying: 'Démarrage du séchage...',
|
||||
toastStarted: 'Séchage démarré',
|
||||
toastStopped: 'Séchage arrêté',
|
||||
toastNotStarted: 'L\'imprimante a accepté la commande, mais l\'AMS n\'a pas démarré le séchage. Vérifiez que l\'adaptateur secteur de l\'AMS est branché et que l\'imprimante est inactive.',
|
||||
stoppingDrying: 'Arrêt du séchage...',
|
||||
rotateTray: 'Tourner la bobine pendant le séchage',
|
||||
rotateUnavailableReason: 'Indisponible — un emplacement de cet AMS est chargé vers la tête d\'impression. La bobine est bloquée par le tube d\'alimentation et ne peut pas tourner. Rétractez d\'abord le filament.',
|
||||
|
||||
@@ -569,6 +569,9 @@ export default {
|
||||
notSupported: 'Essiccazione non supportata',
|
||||
powerRequired: 'Collegare l\'alimentatore AMS per abilitare l\'asciugatura',
|
||||
startingDrying: 'Avvio essiccazione...',
|
||||
toastStarted: 'Essiccazione avviata',
|
||||
toastStopped: 'Essiccazione interrotta',
|
||||
toastNotStarted: 'La stampante ha accettato il comando, ma l\'AMS non ha avviato l\'essiccazione. Verifica che l\'alimentatore dell\'AMS sia collegato e che la stampante sia inattiva.',
|
||||
stoppingDrying: 'Arresto essiccazione...',
|
||||
rotateTray: 'Ruota la bobina durante l\'essiccazione',
|
||||
rotateUnavailableReason: 'Non disponibile — uno slot di questo AMS è caricato verso la testa di stampa. La bobina è bloccata dal tubo di alimentazione e non può ruotare. Ritrai prima il filamento.',
|
||||
|
||||
@@ -568,6 +568,9 @@ export default {
|
||||
notSupported: '乾燥非対応',
|
||||
powerRequired: 'AMS電源アダプターを接続して乾燥を有効にしてください',
|
||||
startingDrying: '乾燥を開始しています...',
|
||||
toastStarted: '乾燥を開始しました',
|
||||
toastStopped: '乾燥を停止しました',
|
||||
toastNotStarted: 'プリンターはコマンドを受け付けましたが、AMS は乾燥を開始しませんでした。AMS の電源アダプターが接続されているか、プリンターがアイドル状態かを確認してください。',
|
||||
stoppingDrying: '乾燥を停止しています...',
|
||||
rotateTray: '乾燥中にスプールを回転',
|
||||
rotateUnavailableReason: '利用不可 — このAMSのスロットがツールヘッドにロードされています。スプールが供給チューブで固定されているため回転できません。先にフィラメントを引き戻してください。',
|
||||
|
||||
@@ -532,6 +532,9 @@ export default {
|
||||
notSupported: '건조 지원 안 됨',
|
||||
powerRequired: '건조를 활성화하려면 AMS 전원 어댑터를 연결하세요',
|
||||
startingDrying: '건조 시작 중...',
|
||||
toastStarted: '건조를 시작했습니다',
|
||||
toastStopped: '건조를 중지했습니다',
|
||||
toastNotStarted: '프린터가 명령을 수락했지만 AMS가 건조를 시작하지 않았습니다. AMS 전원 어댑터가 연결되어 있는지, 프린터가 대기 상태인지 확인하십시오.',
|
||||
stoppingDrying: '건조 정지 중...',
|
||||
rotateTray: '건조 중 스풀 회전',
|
||||
rotateUnavailableReason: '사용할 수 없음 — 이 AMS의 슬롯이 툴헤드로 로드되어 있습니다. 스풀이 공급 튜브에 의해 고정되어 회전할 수 없습니다. 먼저 필라멘트를 뺀 후 다시 시도하십시오.'
|
||||
|
||||
@@ -569,6 +569,9 @@ export default {
|
||||
notSupported: 'Secagem não suportada',
|
||||
powerRequired: 'Conecte o adaptador de energia AMS para ativar a secagem',
|
||||
startingDrying: 'Iniciando secagem...',
|
||||
toastStarted: 'Secagem iniciada',
|
||||
toastStopped: 'Secagem interrompida',
|
||||
toastNotStarted: 'A impressora aceitou o comando, mas o AMS não iniciou a secagem. Verifique se o adaptador de energia do AMS está conectado e se a impressora está ociosa.',
|
||||
stoppingDrying: 'Parando secagem...',
|
||||
rotateTray: 'Girar o carretel durante a secagem',
|
||||
rotateUnavailableReason: 'Indisponível — um slot deste AMS está carregado em direção ao cabeçote. O carretel está travado pelo tubo de alimentação e não pode girar. Retraia o filamento primeiro.',
|
||||
|
||||
@@ -569,6 +569,9 @@ export default {
|
||||
notSupported: 'Kurutma desteklenmiyor',
|
||||
powerRequired: 'Kurutmayı etkinleştirmek için AMS güç adaptörünü bağlayın',
|
||||
startingDrying: 'Kurutma başlatılıyor...',
|
||||
toastStarted: 'Kurutma başlatıldı',
|
||||
toastStopped: 'Kurutma durduruldu',
|
||||
toastNotStarted: 'Yazıcı komutu kabul etti ancak AMS kurutmayı başlatmadı. AMS güç adaptörünün bağlı olduğundan ve yazıcının boşta olduğundan emin olun.',
|
||||
stoppingDrying: 'Kurutma durduruluyor...',
|
||||
rotateTray: 'Kurutma sırasında makarayı döndür',
|
||||
rotateUnavailableReason: 'Kullanılamaz — bu AMS\'nin bir yuvası kafaya doğru yüklenmiş durumda. Makara besleme borusu tarafından kilitlendiği için döndürülemez. Önce filamenti geri çekin.',
|
||||
|
||||
@@ -569,6 +569,9 @@ export default {
|
||||
notSupported: '不支持干燥',
|
||||
powerRequired: '连接AMS电源适配器以启用干燥',
|
||||
startingDrying: '正在启动干燥...',
|
||||
toastStarted: '已开始干燥',
|
||||
toastStopped: '已停止干燥',
|
||||
toastNotStarted: '打印机已接受命令,但 AMS 未开始干燥。请检查 AMS 电源适配器是否已连接,以及打印机是否处于空闲状态。',
|
||||
stoppingDrying: '正在停止干燥...',
|
||||
rotateTray: '干燥时旋转料盘',
|
||||
rotateUnavailableReason: '不可用 — 此 AMS 中有插槽已装入打印头。料盘被送料管固定,无法旋转。请先回退耗材。',
|
||||
|
||||
@@ -569,6 +569,9 @@ export default {
|
||||
notSupported: '不支援乾燥',
|
||||
powerRequired: '連線AMS電源介面卡以啟用乾燥',
|
||||
startingDrying: '正在啟動乾燥...',
|
||||
toastStarted: '已開始乾燥',
|
||||
toastStopped: '已停止乾燥',
|
||||
toastNotStarted: '印表機已接受命令,但 AMS 未開始乾燥。請檢查 AMS 電源變壓器是否已連接,以及印表機是否處於閒置狀態。',
|
||||
stoppingDrying: '正在停止乾燥...',
|
||||
rotateTray: '乾燥時旋轉料盤',
|
||||
rotateUnavailableReason: '無法使用 — 此 AMS 中有插槽已裝入列印頭。料盤被進料管固定,無法旋轉。請先退回耗材。',
|
||||
|
||||
@@ -875,6 +875,12 @@ function getEmptySlotKind(tray: { tray_type?: string | null; state?: number | nu
|
||||
return (tray?.state === 9 || tray?.state === 10) ? 'physical' : 'reset';
|
||||
}
|
||||
|
||||
// How long to wait for an AMS to report a live drying cycle after the printer
|
||||
// acked the start command (#2533). Firmware moves to DryStatus 1 (Checking)
|
||||
// within a couple of seconds; 30s covers the slowest observed push cadence
|
||||
// without leaving the user waiting on a verdict.
|
||||
const DRY_START_CONFIRM_MS = 30_000;
|
||||
|
||||
|
||||
function CoverImage({
|
||||
url,
|
||||
@@ -1843,6 +1849,10 @@ function PrinterCard({
|
||||
const [dryingDuration, setDryingDuration] = useState(4);
|
||||
const [dryingRotateTray, setDryingRotateTray] = useState(false);
|
||||
const [dryingPopoverPos, setDryingPopoverPos] = useState<{ top: number; left: number } | null>(null);
|
||||
// Which AMS we are waiting on to actually enter a drying cycle (#2533). Held as
|
||||
// an object rather than a bare id so restarting drying on the SAME unit produces
|
||||
// a new identity and rearms the timeout below.
|
||||
const [dryStartWatch, setDryStartWatch] = useState<{ amsId: number } | null>(null);
|
||||
const [isDraggingFile, setIsDraggingFile] = useState(false);
|
||||
const [isDropUploading, setIsDropUploading] = useState(false);
|
||||
const printerActionsMenuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -2056,6 +2066,29 @@ function PrinterCard({
|
||||
}, [status?.ams]);
|
||||
const amsData = (status?.ams && status.ams.length > 0) ? status.ams : cachedAmsData.current;
|
||||
|
||||
// Confirm a drying cycle actually started (#2533). Firmware answers
|
||||
// ams_filament_drying with result=success even when it then silently declines,
|
||||
// and P1-family firmware never publishes dry_sf_reason — so the server-side
|
||||
// reason guard is blind there and the card just sits unchanged. Watch the unit
|
||||
// after the ack: leaving DryStatus 0 means the cycle is live, staying there
|
||||
// means the printer took the command and dropped it.
|
||||
useEffect(() => {
|
||||
if (!dryStartWatch) return;
|
||||
const unit = amsData.find(a => a.id === dryStartWatch.amsId);
|
||||
if (unit && (unit.dry_time > 0 || unit.dry_status > 0)) setDryStartWatch(null);
|
||||
}, [dryStartWatch, amsData]);
|
||||
|
||||
// Deps deliberately exclude amsData: status pushes arrive continuously and would
|
||||
// otherwise rearm the timeout on every frame, so it would never elapse.
|
||||
useEffect(() => {
|
||||
if (!dryStartWatch) return;
|
||||
const timer = setTimeout(() => {
|
||||
setDryStartWatch(null);
|
||||
showToast(t('printers.drying.toastNotStarted'), 'error');
|
||||
}, DRY_START_CONFIRM_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [dryStartWatch, showToast, t]);
|
||||
|
||||
// Cache tray_now to prevent flickering when undefined values come in
|
||||
// Valid tray IDs: 0-253 for AMS, 254 for external spool
|
||||
// tray_now=255 means "no tray loaded" (Bambu protocol sentinel) — never active
|
||||
@@ -2214,8 +2247,10 @@ function PrinterCard({
|
||||
const startDryingMutation = useMutation({
|
||||
mutationFn: ({ amsId, temp, duration, filament, rotateTray }: { amsId: number; temp: number; duration: number; filament: string; rotateTray: boolean }) =>
|
||||
api.startDrying(printer.id, amsId, temp, duration, filament, rotateTray),
|
||||
onSuccess: () => {
|
||||
onSuccess: (_data, { amsId }) => {
|
||||
setDryingPopoverAmsId(null);
|
||||
setDryStartWatch({ amsId });
|
||||
showToast(t('printers.drying.toastStarted'), 'success');
|
||||
queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] });
|
||||
},
|
||||
onError: (error: Error) => showToast(error.message || t('printers.toast.failedToSendCommand'), 'error'),
|
||||
@@ -2224,6 +2259,8 @@ function PrinterCard({
|
||||
const stopDryingMutation = useMutation({
|
||||
mutationFn: (amsId: number) => api.stopDrying(printer.id, amsId),
|
||||
onSuccess: () => {
|
||||
setDryStartWatch(null);
|
||||
showToast(t('printers.drying.toastStopped'), 'success');
|
||||
queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] });
|
||||
},
|
||||
onError: (error: Error) => showToast(error.message || t('printers.toast.failedToSendCommand'), 'error'),
|
||||
@@ -6462,6 +6499,7 @@ function PrinterCard({
|
||||
}
|
||||
}}
|
||||
disabled={startDryingMutation.isPending}
|
||||
data-testid="drying-start-confirm"
|
||||
className="w-full py-1.5 bg-bambu-green hover:bg-bambu-green/80 text-white text-xs font-medium rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{startDryingMutation.isPending ? t('printers.drying.startingDrying') : t('printers.drying.start')}
|
||||
|
||||
Reference in New Issue
Block a user