mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
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.
This commit is contained in:
@@ -486,3 +486,90 @@ describe('VirtualPrinterCard - access code inherits from target', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -255,7 +255,14 @@ export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps)
|
||||
|
||||
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 */}
|
||||
<div
|
||||
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>
|
||||
<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()}>
|
||||
<button
|
||||
onClick={handleToggleEnabled}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -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-Bmu-wyBZ.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DhpDphGT.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BkuH4t27.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user