diff --git a/CHANGELOG.md b/CHANGELOG.md index c000452bf..2c9009040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file. ## [1.2.6b1] - Unreleased ### Fixed +- **The print dialog clipped the per-filament gram usage when the material name was long, especially on mobile (#2669, reporter @apizz)** — In the Print dialog's Filament Mapping, each required filament shows its name and the grams the job needs, e.g. `Bambu PLA Basic (281.2g)`. The name and the gram figure lived in a single fixed-width column that truncated as one unit, so a long name (e.g. `Polymaker PLA Matte`) pushed the `(…g)` off the end and cut it off — partially on a wide screen, entirely in mobile portrait. The gram usage is the more important number here (it's what tells you whether a spool has enough left), so hiding it was the wrong thing to drop. **Fix.** The gram usage is now pinned and never shrinks or truncates; only the material name truncates (with the full name on hover), so the `(…g)` stays fully visible at every width. Applied to both the Specific-Printer and "Any [model]" mapping panels. Frontend-only, no behaviour change beyond layout. Covered by a test asserting the gram figure renders in its own non-truncating element separate from the truncating name. - **A printer's nozzle size got overwritten to the wrong value (often 0.8mm), then blocked prints as a nozzle mismatch (#2663, reporter @huykent)** — A1 printers with a 0.4mm nozzle intermittently showed **0.8mm** (or no size at all) on the dashboard, and since 1.2.5 that wrong value made the nozzle-mismatch guard (#1899) refuse to dispatch the job — "File sliced for a 0.4mm nozzle, but the printer has 0.8mm installed." It was intermittent and could flip *after* a job was sent. **Root cause.** Bambuddy fetches K-profiles by probing every nozzle size in turn — it sends an `extrusion_cali_get` request for 0.2, 0.4, 0.6 **and** 0.8mm. The printer's response to each echoes the *requested* nozzle diameter at the top level, and the MQTT handler passed every `print` message — including these K-profile responses — through `_update_state`, which treats a top-level `nozzle_diameter` as the installed hardware. So the last size probed (0.8) clobbered the real nozzle size in memory; a later genuine status push would correct it, and the next K-profile fetch would break it again, which is why it flickered and "changed after the job was sent." The raw MQTT status always reported the correct 0.4 — only the derived hardware-nozzle field was corrupted. **Fix.** `extrusion_cali_get` responses are now handled *only* by the K-profile parser and no longer fed to `_update_state`, so they can't touch the nozzle hardware state — mirroring the existing guard that already stops `get_accessories` responses from doing the same thing. The installed nozzle size now comes solely from the printer's real status push, where it was always correct. No configuration or migration needed: the value lives in memory and self-corrects on the next status push after updating. Covered by tests: a 0.8mm K-profile response leaves a 0.4mm nozzle untouched, the response's profiles are still parsed into `state.kprofiles`, and a genuine status push still sets (and corrects) the nozzle. - **The print queue couldn't be reordered on a phone, and the reorder controls were invisible in portrait (#2667, reporter @aporlebeke)** — On mobile there was no way to reorder the queue: in portrait the reorder controls simply weren't visible, and even in landscape (where the desktop drag handle appears) touch-dragging didn't move anything. **Root cause.** The drag grip and selection checkbox on every pending row are `hidden sm:flex`, so below the 640px breakpoint (phone portrait) they disappear entirely — there's no affordance to grab. Above it (landscape phone/tablet) the grip shows, but it carried `touch-action: manipulation` and the only drag sensor is dnd-kit's `PointerSensor` with an 8px activation distance, so on touch the browser claimed the vertical gesture as a scroll before the drag ever started. The whole reorder mechanism was effectively mouse-only. **Fix.** Pending rows now get tap-friendly **up/down arrow buttons** on mobile (the "arrow select" the reporter asked for), shown below `sm` where the drag handle is hidden. They move a row one step among its siblings — standalone items, whole batches, and items within a batch, in both the flat and per-printer layouts — and persist through the same `POST /queue/reorder` path as drag, so arrows and drag agree. Arrows appear only in the manual "position" sort (with shortest-job-first off), where a position actually has meaning, and are gated on the same `queue:reorder` permission; the up arrow on the first row and the down arrow on the last are shown disabled. Separately, the desktop drag handle's `touch-action` is now `none`, so mouse-style drag also works on touch (landscape phones, tablets). Reuses the existing `queue.moveUp` / `queue.moveDown` translations (already present in all locales). Covered by tests: the controls render for pending items, moving the first item down persists the swapped order, and the boundary arrows are disabled. - **3D Preview plate thumbnails were broken (401) in File Manager when login was enabled (#2661, reporter @fbordonaro)** — Opening a multi-plate 3MF via **File Manager → 3D Preview** showed broken-image icons for every plate thumbnail, and the network tab showed `GET /api/v1/library/files//plate-thumbnail/` returning **401 "Valid camera stream token required."** The Slice dialog displayed the same file's thumbnails correctly, which is what made it look inconsistent. **Root cause.** The plate-thumbnail endpoints (both archive and library) are gated behind a **camera stream token** passed as a `?token=` query param, because an `` tag can't send an `Authorization: Bearer` header. Every place that renders these thumbnails is supposed to append the token via the `withStreamToken()` helper — `PlatePickerModal` (the Slice dialog's multi-plate picker) and the Print modal's `PlateSelector` both do — but the **3D Preview dialog** (`ModelViewerModal`) rendered the raw `thumbnail_url` with no token, so with auth enabled the browser fetched without one and got a 401. **Fix.** `ModelViewerModal` now wraps the plate thumbnail `src` in `withStreamToken()`, matching the two existing call sites. The token is already synced app-wide (the same global the working pickers read), and `withStreamToken()` is a no-op when auth is off, so nothing changes for non-auth setups. Covered by a component test asserting the plate thumbnail `` carries the `?token=` query param. diff --git a/frontend/src/__tests__/components/FilamentMapping.test.tsx b/frontend/src/__tests__/components/FilamentMapping.test.tsx index 713be6025..6095819be 100644 --- a/frontend/src/__tests__/components/FilamentMapping.test.tsx +++ b/frontend/src/__tests__/components/FilamentMapping.test.tsx @@ -297,4 +297,47 @@ describe('FilamentMapping — FTS routing', () => { expect(swatch).toBeInTheDocument(); }); }); + + it('pins the gram usage so a long name cannot clip it (#2669)', async () => { + // Long resolved name + gram usage. The name must be the truncating + // element; the "(25g)" must sit in its own non-truncating, shrink-0 span + // so it stays visible on narrow/mobile widths. + server.use( + http.get('/api/v1/printers/:id/status', () => HttpResponse.json(createStatus({}))), + http.get('/api/v1/cloud/builtin-filaments', () => + HttpResponse.json([{ filament_id: 'GFA01', name: 'Polymaker PLA Matte' }]), + ), + http.get('/api/v1/cloud/filament-id-map', () => HttpResponse.json({})), + http.get('/api/v1/inventory/colors/by-material', () => HttpResponse.json({ color_name: null })), + ); + + render( + {}} + currencySymbol="$" + defaultCostPerKg={0} + defaultExpanded + />, + ); + + const grams = await screen.findByText('(25g)'); + // The gram usage never truncates and never shrinks away. + expect(grams.className).toContain('shrink-0'); + expect(grams.className).not.toContain('truncate'); + + // The name is the element that truncates instead. + const name = await screen.findByText('Polymaker PLA Matte'); + expect(name.className).toContain('truncate'); + // Name and grams are separate siblings, so the name shrinking can't take + // the grams with it. + expect(name).not.toBe(grams); + expect(grams.parentElement).toBe(name.parentElement); + }); }); diff --git a/frontend/src/components/PrintModal/FilamentMapping.tsx b/frontend/src/components/PrintModal/FilamentMapping.tsx index 462db4101..659aac46e 100644 --- a/frontend/src/components/PrintModal/FilamentMapping.tsx +++ b/frontend/src/components/PrintModal/FilamentMapping.tsx @@ -243,8 +243,10 @@ export function FilamentMapping({ - {/* Required type + grams + nozzle badge */} - + {/* Required type + grams + nozzle badge. Only the name + truncates; the gram usage is pinned (shrink-0) so it never + clips on narrow/mobile widths (#2669). */} + {isDualNozzle && item.nozzle_id != null && ( )} - {resolvedName} ({item.used_grams}g) + {resolvedName} + ({item.used_grams}g) {/* Arrow */} → diff --git a/frontend/src/components/PrintModal/PrinterSelector.tsx b/frontend/src/components/PrintModal/PrinterSelector.tsx index f0b5186f9..26b16769e 100644 --- a/frontend/src/components/PrintModal/PrinterSelector.tsx +++ b/frontend/src/components/PrintModal/PrinterSelector.tsx @@ -161,8 +161,11 @@ function InlineMappingEditor({ - - {req.type} ({req.used_grams}g) + {/* Only the name truncates; the gram usage is pinned (shrink-0) so + it never clips on narrow/mobile widths (#2669). */} + + {req.type} + ({req.used_grams}g) →