Files
bambuddy/frontend/src/components/PrinterQueueWidget.tsx
T
maziggy 574b39aee5 change(workflow): default Plate-Clear Confirmation to off on fresh installs
New users repeatedly reported queued prints "not starting" because the
  confirmation prompt was waiting on an ack they didn't know existed.
  Flip the default in the settings schema and in the frontend fallbacks
  so a missing/unset value reads as disabled. Existing installs keep
  their saved preference.
2026-04-14 08:27:00 +02:00

146 lines
6.4 KiB
TypeScript

import { useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Clock, Calendar, ChevronRight, Loader2, CircleCheck } from 'lucide-react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { api } from '../api/client';
import { useAuth } from '../contexts/AuthContext';
import { useToast } from '../contexts/ToastContext';
import { formatRelativeTime } from '../utils/date';
import { filterCompatibleQueueItems } from '../utils/printer';
interface PrinterQueueWidgetProps {
printerId: number;
printerModel?: string | null;
/** @deprecated use awaitingPlateClear — kept so existing callers/tests still compile */
printerState?: string | null;
awaitingPlateClear?: boolean;
requirePlateClear?: boolean;
loadedFilamentTypes?: Set<string>;
loadedFilaments?: Set<string>; // "TYPE:rrggbb" pairs for filament override color matching
}
export function PrinterQueueWidget({ printerId, printerModel, awaitingPlateClear, requirePlateClear = false, loadedFilamentTypes, loadedFilaments }: PrinterQueueWidgetProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { showToast } = useToast();
const { hasPermission } = useAuth();
const { data: queue } = useQuery({
queryKey: ['queue', printerId, 'pending', printerModel],
queryFn: () => api.getQueue(printerId, 'pending', printerModel || undefined),
refetchInterval: 30000,
});
const clearPlateMutation = useMutation({
mutationFn: () => api.clearPlate(printerId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['queue', printerId] });
queryClient.invalidateQueries({ queryKey: ['printerStatus', printerId] });
showToast(t('queue.clearPlateSuccess'), 'success');
},
onError: (err: Error) => {
showToast(err.message, 'error');
},
});
// Reset mutation state when the awaiting flag clears so the button is clickable
// again after the next finished print (fixes #912). The flag is the authoritative
// signal — state alone is not reliable across power cycles (#961).
useEffect(() => {
if (!awaitingPlateClear) {
clearPlateMutation.reset();
}
}, [awaitingPlateClear, clearPlateMutation]);
// Filter queue to items this printer can actually print (filament type + color check)
const compatibleQueue = queue ? filterCompatibleQueueItems(queue, loadedFilamentTypes, loadedFilaments) : undefined;
// Split into auto-dispatchable vs staged (manual_start) items
const autoDispatchQueue = compatibleQueue?.filter(item => !item.manual_start) ?? [];
const totalPending = compatibleQueue?.length || 0;
if (totalPending === 0) {
return null;
}
const nextAutoItem = autoDispatchQueue[0];
const nextItem = compatibleQueue?.[0];
// Prompt "Clear Plate & Start Next" whenever the backend flags the printer as awaiting
// acknowledgment. Don't gate on reported state: after Auto Off cycles the printer, it
// boots into IDLE while still awaiting — the prompt must survive that (#961). The flag
// is cleared by the backend on ack or when the next print dispatches.
const needsClearPlate = requirePlateClear && !!awaitingPlateClear && autoDispatchQueue.length > 0;
if (needsClearPlate) {
const displayItem = nextAutoItem || nextItem;
return (
<div className="mb-3 p-3 bg-bambu-dark rounded-lg border border-yellow-400/30">
<div className="flex items-center gap-3 mb-2">
<Calendar className="w-5 h-5 text-yellow-400 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-xs text-bambu-gray">{t('queue.nextInQueue')}</p>
<p className="text-sm text-white truncate">
{displayItem?.archive_name || displayItem?.library_file_name || `File #${displayItem?.archive_id || displayItem?.library_file_id}`}
</p>
</div>
{totalPending > 1 && (
<span className="text-xs px-1.5 py-0.5 bg-yellow-400/20 text-yellow-400 rounded flex-shrink-0">
+{totalPending - 1}
</span>
)}
</div>
{clearPlateMutation.isSuccess ? (
<div className="w-full py-2 px-3 rounded-lg bg-bambu-green/10 border border-bambu-green/20 text-bambu-green text-sm flex items-center justify-center gap-2">
<CircleCheck className="w-4 h-4" />
{t('queue.plateReady')}
</div>
) : (
<button
onClick={() => clearPlateMutation.mutate()}
disabled={clearPlateMutation.isPending || !hasPermission('printers:clear_plate')}
className="w-full py-2 px-3 rounded-lg bg-bambu-green/20 border border-bambu-green/40 text-bambu-green hover:bg-bambu-green/30 transition-colors text-sm font-medium flex items-center justify-center gap-2 disabled:opacity-50"
>
{clearPlateMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<CircleCheck className="w-4 h-4" />
)}
{t('queue.clearPlate')}
</button>
)}
</div>
);
}
return (
<Link
to="/queue"
className="block mb-3 p-3 bg-bambu-dark rounded-lg hover:bg-bambu-dark-tertiary transition-colors"
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3 min-w-0 flex-1">
<Calendar className="w-5 h-5 text-yellow-400 flex-shrink-0" />
<div className="min-w-0 flex-1">
<p className="text-xs text-bambu-gray">{t('queue.nextInQueue')}</p>
<p className="text-sm text-white truncate">
{nextItem?.archive_name || nextItem?.library_file_name || `File #${nextItem?.archive_id || nextItem?.library_file_id}`}
</p>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<span className="text-xs text-bambu-gray flex items-center gap-1">
<Clock className="w-3 h-3" />
{nextItem?.scheduled_time ? formatRelativeTime(nextItem.scheduled_time, 'system', t) : t('time.waiting')}
</span>
{totalPending > 1 && (
<span className="text-xs px-1.5 py-0.5 bg-yellow-400/20 text-yellow-400 rounded">
+{totalPending - 1}
</span>
)}
<ChevronRight className="w-4 h-4 text-bambu-gray" />
</div>
</div>
</Link>
);
}