Explorar el Código

fix(virtual-printer): wrap the card header instead of overflowing it (#2808)

Every item in the collapsed header was flex-shrink-0, so the row was as
wide as its contents and the Card doesn't clip -- the remote-interface IP
and the enable toggle painted outside the card border. The name's
`truncate` couldn't save it: a flex item defaults to min-width:auto, so
it never shrank below its text (`flex-shrink-0 truncate` on the target
name was self-cancelling for the same reason).

Move the metadata into a flex-1 min-w-0 flex-wrap group so it wraps to a
second line, and keep the chevron, dot and toggle outside it. Wrapping
rather than truncating: the bind and remote-interface addresses are what
the page exists to show.

Needs three things at once, hence the report -- both IPs set (Bambuddy
and printer on different subnets), a target named "Printer at <ip>" from
discovery, and the 3-column card grid.

overflow-hidden is scoped to this card, not added to Card: half the cards
in the app render menus that deliberately paint outside their bounds.
maziggy hace 3 semanas
padre
commit
b63adb8151

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 0
CHANGELOG.md


+ 87 - 0
frontend/src/__tests__/components/VirtualPrinterCard.test.tsx

@@ -486,3 +486,90 @@ describe('VirtualPrinterCard - access code inherits from target', () => {
     expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
     expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
   });
   });
 });
 });
+
+// The collapsed header used to be a single flex row of flex-shrink-0 items,
+// so it was as wide as its contents and spilled past the card's border --
+// the reporter of #2808 saw an IP address and the enable toggle floating on
+// the page background. jsdom does no layout, so these pin the CSS contract
+// that makes wrapping possible rather than the pixels: a flexible, wrappable
+// metadata group, and no item that both refuses to shrink and claims it will
+// truncate.
+describe('VirtualPrinterCard - header does not overflow the card', () => {
+  const printers = [
+    {
+      id: 9,
+      // The name a printer adopted without one gets (discovery.py) -- 25
+      // unbreakable characters, and half of why this overflowed.
+      name: 'Printer at 192.168.30.210',
+      ip_address: '192.168.30.210',
+      access_code: 'TGTCODE1',
+      serial_number: '01P00A391800001',
+      model: 'H2C',
+      is_active: true,
+    },
+  ];
+
+  const twoVlanPrinter = () =>
+    createMockPrinter({
+      name: 'H2C',
+      mode: 'proxy',
+      model_name: 'H2C',
+      target_printer_id: 9,
+      // Both are populated only when Bambuddy and the printer sit on
+      // different subnets -- the reporter's case.
+      bind_ip: '192.168.20.175',
+      remote_interface_ip: '192.168.30.210',
+    });
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.mocked(multiVirtualPrinterApi.update).mockResolvedValue(createMockPrinter());
+    vi.mocked(api.getPrinters).mockResolvedValue(printers as unknown as Awaited<ReturnType<typeof api.getPrinters>>);
+  });
+
+  it('keeps both IPs visible instead of truncating them away', async () => {
+    render(<VirtualPrinterCard printer={twoVlanPrinter()} models={models} />);
+
+    await waitFor(() => {
+      expect(screen.getByText('192.168.20.175')).toBeInTheDocument();
+    });
+    // An operator opens this page to check exactly these two values, so the
+    // fix has to wrap them, not hide them.
+    expect(screen.getByText('192.168.30.210')).toBeInTheDocument();
+  });
+
+  it('puts the metadata in a shrinkable, wrapping group', async () => {
+    render(<VirtualPrinterCard printer={twoVlanPrinter()} models={models} />);
+
+    const bindIp = await screen.findByText('192.168.20.175');
+    const group = bindIp.parentElement as HTMLElement;
+
+    expect(group.className).toContain('flex-wrap');
+    // Without min-w-0 a flex item cannot shrink below its content, which is
+    // what defeated the name's `truncate` before.
+    expect(group.className).toContain('min-w-0');
+    expect(group.className).toContain('flex-1');
+  });
+
+  it('has no header item that is both unshrinkable and truncating', async () => {
+    render(<VirtualPrinterCard printer={twoVlanPrinter()} models={models} />);
+
+    const bindIp = await screen.findByText('192.168.20.175');
+    const group = bindIp.parentElement as HTMLElement;
+
+    const selfCancelling = Array.from(group.children).filter(
+      (el) => el.className.includes('flex-shrink-0') && el.className.includes('truncate')
+    );
+    expect(selfCancelling).toHaveLength(0);
+  });
+
+  it('clips at the card border as a backstop', async () => {
+    const { container } = render(<VirtualPrinterCard printer={twoVlanPrinter()} models={models} />);
+
+    await screen.findByText('192.168.20.175');
+    // Scoped to this card on purpose: cards elsewhere render menus that paint
+    // outside their own bounds, so this must not migrate into `Card`.
+    const card = container.querySelector('.rounded-xl') as HTMLElement;
+    expect(card.className).toContain('overflow-hidden');
+  });
+});

+ 46 - 18
frontend/src/components/VirtualPrinterCard.tsx

@@ -255,7 +255,14 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
 
 
   return (
   return (
     <>
     <>
-      <Card>
+      {/*
+        Clip at the border as a backstop, so a value longer than anything
+        anticipated above lands inside the card instead of on the page
+        background (#2808). Scoped here rather than added to `Card` itself:
+        half the cards in the app render dropdowns and menus that deliberately
+        paint outside their bounds, and this one has none.
+      */}
+      <Card className="overflow-hidden">
         {/* Collapsed header - always visible, clickable to expand */}
         {/* Collapsed header - always visible, clickable to expand */}
         <div
         <div
           className="px-4 py-3 flex items-center gap-3 cursor-pointer select-none"
           className="px-4 py-3 flex items-center gap-3 cursor-pointer select-none"
@@ -268,23 +275,44 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
             }
             }
           </button>
           </button>
           <span className={`w-2 h-2 rounded-full flex-shrink-0 ${isRunning ? 'bg-green-400 animate-pulse' : 'bg-gray-500'}`} />
           <span className={`w-2 h-2 rounded-full flex-shrink-0 ${isRunning ? 'bg-green-400 animate-pulse' : 'bg-gray-500'}`} />
-          <span className="text-white font-medium truncate">{printer.name}</span>
-          <span className="text-xs text-bambu-gray flex-shrink-0">{modeLabel}</span>
-          {printer.model_name && (
-            <span className="text-xs text-bambu-gray flex-shrink-0">{printer.model_name}</span>
-          )}
-          {targetPrinterName && (
-            <span className="text-xs text-bambu-gray flex-shrink-0 truncate">
-              {localMode === 'proxy' && <ArrowRightLeft className="w-3 h-3 inline mr-1" />}
-              {targetPrinterName}
-            </span>
-          )}
-          {localBindIp && (
-            <span className="text-[10px] text-bambu-gray flex-shrink-0 font-mono">{localBindIp}</span>
-          )}
-          {localRemoteInterfaceIp && (
-            <span className="text-[10px] text-bambu-gray flex-shrink-0 font-mono">{localRemoteInterfaceIp}</span>
-          )}
+          {/*
+            Metadata wraps rather than overflowing (#2808). Every item in this
+            row used to be flex-shrink-0, so nothing could give and the row was
+            as wide as its contents -- which the Card doesn't clip, so the last
+            IP and the toggle were painted outside the card's border. The name's
+            `truncate` didn't save it either: a flex item defaults to
+            min-width:auto, so it couldn't shrink below its text and the
+            ellipsis never engaged (`flex-shrink-0 truncate` on the target name
+            was self-cancelling for the same reason).
+
+            It needs three things at once to overflow, which is why it took a
+            two-VLAN setup to surface: bind_ip and remote_interface_ip are both
+            set only when Bambuddy and the printer are on different subnets, a
+            printer adopted without a name is called "Printer at <ip>" (25
+            characters of unbreakable text), and the cards sit in a 3-column
+            grid. Wrapping keeps every value readable -- these are the addresses
+            an operator came to this page to check, so truncating them away
+            would trade one bug for a quieter one.
+          */}
+          <div className="flex-1 min-w-0 flex flex-wrap items-center gap-x-3 gap-y-1">
+            <span className="text-white font-medium truncate max-w-full">{printer.name}</span>
+            <span className="text-xs text-bambu-gray flex-shrink-0">{modeLabel}</span>
+            {printer.model_name && (
+              <span className="text-xs text-bambu-gray flex-shrink-0">{printer.model_name}</span>
+            )}
+            {targetPrinterName && (
+              <span className="text-xs text-bambu-gray truncate max-w-full">
+                {localMode === 'proxy' && <ArrowRightLeft className="w-3 h-3 inline mr-1" />}
+                {targetPrinterName}
+              </span>
+            )}
+            {localBindIp && (
+              <span className="text-[10px] text-bambu-gray flex-shrink-0 font-mono">{localBindIp}</span>
+            )}
+            {localRemoteInterfaceIp && (
+              <span className="text-[10px] text-bambu-gray flex-shrink-0 font-mono">{localRemoteInterfaceIp}</span>
+            )}
+          </div>
           <div className="ml-auto flex items-center gap-2 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
           <div className="ml-auto flex items-center gap-2 flex-shrink-0" onClick={(e) => e.stopPropagation()}>
             <button
             <button
               onClick={handleToggleEnabled}
               onClick={handleToggleEnabled}

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


+ 1 - 1
static/index.html

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

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