Explorar el Código

fix(print): keep filament gram usage visible when the name is long (#2669)

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)". Name and
grams shared one fixed-width column with truncate on the whole string, so a
long name pushed the "(...g)" off the end and clipped it -- partially on a
wide screen, entirely in mobile portrait. The gram usage is the number that
matters (does the spool have enough left?), so it shouldn't be the part that
gets dropped.

Pin the gram usage (shrink-0, whitespace-nowrap) and let only the name
truncate, with the full name on hover. Applied to both the Specific-Printer
(FilamentMapping) and Any-model (PrinterSelector) panels. Layout only.
maziggy hace 1 mes
padre
commit
aa443c6e83

+ 1 - 0
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/<id>/plate-thumbnail/<plate>` 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 `<img>` 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 `<img>` carries the `?token=` query param.

+ 43 - 0
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(
+      <FilamentMapping
+        printerId={1}
+        filamentReqs={{
+          filaments: [
+            { slot_id: 1, type: 'PLA', color: '#000000', used_grams: 25, used_meters: 8.5, nozzle_id: 1, tray_info_idx: 'GFA01' },
+          ],
+        }}
+        manualMappings={{}}
+        onManualMappingChange={() => {}}
+        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);
+  });
 });

+ 6 - 3
frontend/src/components/PrintModal/FilamentMapping.tsx

@@ -243,8 +243,10 @@ export function FilamentMapping({
                 <span title={`Required: ${resolvedName} - ${colorLabel}`}>
                   <Circle className="w-3 h-3" fill={item.color} stroke={item.color} />
                 </span>
-                {/* Required type + grams + nozzle badge */}
-                <span className="text-white truncate flex items-center gap-1">
+                {/* 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). */}
+                <span className="text-white flex items-center gap-1 min-w-0">
                   {isDualNozzle && item.nozzle_id != null && (
                     <span
                       className="inline-flex items-center justify-center w-3.5 h-3.5 rounded text-[9px] font-bold leading-none bg-bambu-gray/20 text-bambu-gray shrink-0"
@@ -253,7 +255,8 @@ export function FilamentMapping({
                       {item.nozzle_id === 1 ? t('printModal.leftNozzle') : t('printModal.rightNozzle')}
                     </span>
                   )}
-                  {resolvedName} <span className="text-bambu-gray">({item.used_grams}g)</span>
+                  <span className="truncate min-w-0" title={resolvedName}>{resolvedName}</span>
+                  <span className="text-bambu-gray shrink-0 whitespace-nowrap">({item.used_grams}g)</span>
                 </span>
                 {/* Arrow */}
                 <span className="text-bambu-gray">→</span>

+ 5 - 2
frontend/src/components/PrintModal/PrinterSelector.tsx

@@ -161,8 +161,11 @@ function InlineMappingEditor({
           <span title={`Required: ${req.type} - ${getColorName(req.color)}`}>
             <Circle className="w-3 h-3" fill={req.color} stroke={req.color} />
           </span>
-          <span className="text-white truncate">
-            {req.type} <span className="text-bambu-gray">({req.used_grams}g)</span>
+          {/* Only the name truncates; the gram usage is pinned (shrink-0) so
+              it never clips on narrow/mobile widths (#2669). */}
+          <span className="text-white flex items-center gap-1 min-w-0">
+            <span className="truncate min-w-0" title={req.type}>{req.type}</span>
+            <span className="text-bambu-gray shrink-0 whitespace-nowrap">({req.used_grams}g)</span>
           </span>
           <span className="text-bambu-gray">→</span>
           <select

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
static/assets/index-BALo978z.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-CZQ6os90.js"></script>
+    <script type="module" crossorigin src="/assets/index-BALo978z.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-whrCxRGI.css">
   </head>
   <body>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio