Fix "Clear Plate" button shown on printers without matching filament color (#486)

When a print was queued to "any printer" with a filament color override
(e.g., white PETG), the "Clear Plate & Start Next" button appeared on
printers that had the right type but wrong color. The backend scheduler
already rejected color mismatches, but the frontend only checked filament
types. Now passes loaded type+color pairs from AMS status to the widget
and filters against filament_overrides, mirroring the backend's
_count_override_color_matches() logic.
This commit is contained in:
maziggy
2026-02-27 08:03:21 +01:00
parent 2c8fbf3e33
commit ad6108c8a7
6 changed files with 314 additions and 110 deletions
@@ -363,4 +363,169 @@ describe('PrinterQueueWidget - Clear Plate', () => {
});
});
});
describe('filament override color filtering', () => {
const whitePetgOverrideItem = [
{
id: 20,
printer_id: null,
archive_id: 20,
position: 1,
status: 'pending',
archive_name: 'White PETG Print',
printer_name: null,
print_time_seconds: 3600,
scheduled_time: null,
required_filament_types: ['PETG'],
filament_overrides: [{ slot_id: 1, type: 'PETG', color: '#FFFFFF' }],
},
];
it('hides widget when override color does not match loaded filaments', async () => {
server.use(
http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
);
const { container } = render(
<PrinterQueueWidget
printerId={1}
printerState="FINISH"
loadedFilamentTypes={new Set(['PETG'])}
loadedFilaments={new Set(['PETG:0000ff'])}
/>
);
await waitFor(() => {
expect(container.querySelector('button')).not.toBeInTheDocument();
});
expect(screen.queryByText('White PETG Print')).not.toBeInTheDocument();
});
it('shows widget when override color matches loaded filaments', async () => {
server.use(
http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
);
render(
<PrinterQueueWidget
printerId={1}
printerState="FINISH"
loadedFilamentTypes={new Set(['PETG'])}
loadedFilaments={new Set(['PETG:ffffff'])}
/>
);
await waitFor(() => {
expect(screen.getByText('White PETG Print')).toBeInTheDocument();
expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
});
});
it('normalizes override color format (strips # and lowercases)', async () => {
const upperCaseColorItem = [
{
id: 21,
printer_id: null,
archive_id: 21,
position: 1,
status: 'pending',
archive_name: 'Red PLA Print',
printer_name: null,
print_time_seconds: 3600,
scheduled_time: null,
required_filament_types: ['PLA'],
filament_overrides: [{ slot_id: 1, type: 'PLA', color: '#FF0000' }],
},
];
server.use(
http.get('/api/v1/queue/', () => HttpResponse.json(upperCaseColorItem))
);
render(
<PrinterQueueWidget
printerId={1}
printerState="FINISH"
loadedFilamentTypes={new Set(['PLA'])}
loadedFilaments={new Set(['PLA:ff0000'])}
/>
);
await waitFor(() => {
expect(screen.getByText('Red PLA Print')).toBeInTheDocument();
});
});
it('shows widget when no loadedFilaments prop is provided (no color filtering)', async () => {
server.use(
http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
);
render(
<PrinterQueueWidget
printerId={1}
printerState="FINISH"
loadedFilamentTypes={new Set(['PETG'])}
/>
);
await waitFor(() => {
expect(screen.getByText('White PETG Print')).toBeInTheDocument();
});
});
it('shows widget when queue item has no filament overrides', async () => {
// Default mockQueueItems have no filament_overrides
render(
<PrinterQueueWidget
printerId={1}
printerState="FINISH"
loadedFilaments={new Set(['PLA:000000'])}
/>
);
await waitFor(() => {
expect(screen.getByText('First Print')).toBeInTheDocument();
});
});
it('matches any override when multiple overrides exist', async () => {
const multiOverrideItem = [
{
id: 22,
printer_id: null,
archive_id: 22,
position: 1,
status: 'pending',
archive_name: 'Multi Color Print',
printer_name: null,
print_time_seconds: 3600,
scheduled_time: null,
required_filament_types: ['PLA'],
filament_overrides: [
{ slot_id: 1, type: 'PLA', color: '#FF0000' },
{ slot_id: 2, type: 'PLA', color: '#00FF00' },
],
},
];
server.use(
http.get('/api/v1/queue/', () => HttpResponse.json(multiOverrideItem))
);
// Printer has green PLA but not red — should still match (at least one override)
render(
<PrinterQueueWidget
printerId={1}
printerState="FINISH"
loadedFilamentTypes={new Set(['PLA'])}
loadedFilaments={new Set(['PLA:00ff00'])}
/>
);
await waitFor(() => {
expect(screen.getByText('Multi Color Print')).toBeInTheDocument();
});
});
});
});
+20 -5
View File
@@ -13,9 +13,10 @@ interface PrinterQueueWidgetProps {
printerState?: string | null;
plateCleared?: boolean;
loadedFilamentTypes?: Set<string>;
loadedFilaments?: Set<string>; // "TYPE:rrggbb" pairs for filament override color matching
}
export function PrinterQueueWidget({ printerId, printerModel, printerState, plateCleared, loadedFilamentTypes }: PrinterQueueWidgetProps) {
export function PrinterQueueWidget({ printerId, printerModel, printerState, plateCleared, loadedFilamentTypes, loadedFilaments }: PrinterQueueWidgetProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { showToast } = useToast();
@@ -38,11 +39,25 @@ export function PrinterQueueWidget({ printerId, printerModel, printerState, plat
},
});
// Filter queue to items this printer can actually print (filament type check)
// Filter queue to items this printer can actually print (filament type + color check)
const compatibleQueue = queue?.filter(item => {
if (!item.required_filament_types?.length) return true;
if (!loadedFilamentTypes?.size) return true;
return item.required_filament_types.every((t: string) => loadedFilamentTypes.has(t.toUpperCase()));
// Type check: all required filament types must be loaded
if (item.required_filament_types?.length && loadedFilamentTypes?.size) {
if (!item.required_filament_types.every((t: string) => loadedFilamentTypes.has(t.toUpperCase()))) {
return false;
}
}
// Color check: if filament overrides specify colors, at least one must match
// Mirrors backend _count_override_color_matches() logic
if (item.filament_overrides?.length && loadedFilaments?.size) {
const hasColorMatch = item.filament_overrides.some(o => {
const oType = (o.type || '').toUpperCase();
const oColor = (o.color || '').replace('#', '').toLowerCase().slice(0, 6);
return loadedFilaments.has(`${oType}:${oColor}`);
});
if (!hasColorMatch) return false;
}
return true;
});
const nextItem = compatibleQueue?.[0];
+24 -1
View File
@@ -1556,6 +1556,29 @@ function PrinterCard({
return types;
}, [status?.ams, status?.vt_tray]);
// Collect loaded filament type+color pairs for queue widget override matching
// Format: "TYPE:rrggbb" (e.g., "PETG:ffffff") — mirrors backend _count_override_color_matches()
const loadedFilaments = useMemo(() => {
const filaments = new Set<string>();
if (status?.ams) {
for (const ams of status.ams) {
for (const tray of ams.tray || []) {
if (tray.tray_type && tray.tray_color) {
const color = tray.tray_color.replace('#', '').toLowerCase().slice(0, 6);
filaments.add(`${tray.tray_type.toUpperCase()}:${color}`);
}
}
}
}
for (const vt of status?.vt_tray ?? []) {
if (vt.tray_type && vt.tray_color) {
const color = vt.tray_color.replace('#', '').toLowerCase().slice(0, 6);
filaments.add(`${vt.tray_type.toUpperCase()}:${color}`);
}
}
return filaments;
}, [status?.ams, status?.vt_tray]);
// Fetch cloud filament info for tooltips (name includes color, also has K value)
const { data: filamentInfo } = useQuery({
queryKey: ['filamentInfo', trayInfoIds],
@@ -2489,7 +2512,7 @@ function PrinterCard({
</div>
{/* Queue Widget - always visible when there are pending items */}
<PrinterQueueWidget printerId={printer.id} printerModel={printer.model} printerState={status.state} plateCleared={status.plate_cleared} loadedFilamentTypes={loadedFilamentTypes} />
<PrinterQueueWidget printerId={printer.id} printerModel={printer.model} printerState={status.state} plateCleared={status.plate_cleared} loadedFilamentTypes={loadedFilamentTypes} loadedFilaments={loadedFilaments} />
</>
)}