Fix cross-model queue items being misrepresented and editable into a broken state (#671)

The edit dialog offered a printer picker and target-model dropdown for
an item with alternatives. Saving left a row with variants AND a
printer_id, and the scheduler's fixed-printer branch wins that race, so
it dispatched a row whose library_file_id is still null and failed in
the upload. PATCH now refuses printer/model changes on such an item —
comparing against the current value, since the dialog re-sends
target_model unchanged — and the route eager-loads variants, without
which the guard could not see them and every PATCH response dropped the
alternatives from its payload.

Names come from a shared helper now. A cross-model item holds neither
archive_id nor library_file_id until dispatch, so five separate inlined
fallbacks all rendered "File #null"; they now read "x1c.gcode.3mf +1
more".

The queue also grouped these under "Any H2D" — the first candidate
mirrored onto the row — filing a job under a printer it might never run
on. It groups as "Any H2D / X1C", matching the row beneath it.
This commit is contained in:
maziggy
2026-08-03 12:33:19 +02:00
parent ef7c1b21f1
commit ea63355fde
25 changed files with 593 additions and 198 deletions
+44
View File
@@ -0,0 +1,44 @@
/**
* Display name for a queue item, wherever one is shown.
*
* Every surface used to inline the same fallback chain, which produced
* `File #null` for a cross-model item (#671): those deliberately hold neither
* `archive_id` nor `library_file_id` until dispatch resolves a candidate, so
* that the ON DELETE CASCADE on `library_file_id` can't destroy the whole job
* when one alternative is deleted. Nothing to point at is the design working;
* the label just had nowhere to look.
*/
interface NameableQueueItem {
archive_name?: string | null;
library_file_name?: string | null;
archive_id?: number | null;
library_file_id?: number | null;
variants?: Array<{ filename: string }>;
}
/**
* @param item the queue item to name
* @param moreLabel formats the "+N more" suffix for a cross-model item; pass
* the caller's `t` binding so the count stays translated.
* Omitted in compact surfaces that only have room for a name.
*/
export function queueItemDisplayName(
item: NameableQueueItem,
moreLabel?: (count: number) => string,
): string {
if (item.archive_name) return item.archive_name;
if (item.library_file_name) return item.library_file_name;
// Cross-model item: name it after the candidate the user put first — the one
// the scheduler will try first — and say how many others are behind it.
const variants = item.variants ?? [];
if (variants.length > 0) {
const first = variants[0].filename;
const others = variants.length - 1;
if (others > 0 && moreLabel) return `${first} ${moreLabel(others)}`;
return first;
}
return `File #${item.archive_id ?? item.library_file_id}`;
}