From bb58cb3ba8e5245b7dd2e6771ff134a6b3f8e2ac Mon Sep 17 00:00:00 2001 From: isra el Date: Wed, 22 Jul 2026 15:47:18 +0300 Subject: [PATCH] fix(web): keep long message text inside the details dialog DialogContent is a CSS grid, and an implicit grid track has a min-content floor. `break-words` does not reduce min-content size, so a long unbreakable URL in any child pushed every child past the card's max-width instead of wrapping. In message history that left the message box, the delivery error panel and the action button rendered hundreds of pixels outside the dialog, off the side of the screen. An explicit minmax(0,1fr) column removes that floor, after which break-words can do its job. This fixes every dialog in the app, not just this one: any long URL, API key or gateway ID hit the same track. Also makes the details dialog the single scroll container, matching api-keys and the webhook payload modal, instead of capping the message body at 224px behind a scrollbar nested in a scrollbar. And the list row preview now wraps, so a URL-heavy message fills both clamped lines rather than being cut off partway through the first. Covered by an e2e regression test at 375px and 1280px that asserts the dialog does not scroll sideways and that the action button stays inside it. Verified failing before this change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../message-history/message-row.tsx | 4 +- .../message-history/sms-details-dialog.tsx | 9 ++- web/components/ui/dialog.tsx | 5 +- web/e2e/message-history.spec.ts | 78 +++++++++++++++++++ 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/web/app/(app)/dashboard/(components)/message-history/message-row.tsx b/web/app/(app)/dashboard/(components)/message-history/message-row.tsx index 234dc9f..89a19bf 100644 --- a/web/app/(app)/dashboard/(components)/message-history/message-row.tsx +++ b/web/app/(app)/dashboard/(components)/message-history/message-row.tsx @@ -66,7 +66,9 @@ export function MessageRow({ message, device, onSelect }: MessageRowProps) { - + {/* break-words so a message that is one long URL fills both clamped + lines instead of being cut off partway through the first. */} + {message.message} diff --git a/web/app/(app)/dashboard/(components)/message-history/sms-details-dialog.tsx b/web/app/(app)/dashboard/(components)/message-history/sms-details-dialog.tsx index 63cdd04..f17f0f3 100644 --- a/web/app/(app)/dashboard/(components)/message-history/sms-details-dialog.tsx +++ b/web/app/(app)/dashboard/(components)/message-history/sms-details-dialog.tsx @@ -54,7 +54,10 @@ export default function SmsDetailsDialog({ return ( <> - + {/* The dialog is the only scroll container. Capping the message body + instead put a scrollbar inside a scrollbar, which is easy to miss + on touch. */} + {/* text-left overrides the primitive's mobile centring, which left the title on the left and the date centred under it. */} @@ -92,7 +95,7 @@ export default function SmsDetailsDialog({ {/* The message body leads: it is the reason this dialog is open. */}
-
+
{message.message}
)} {message.errorMessage && ( -

+

{message.errorMessage}

)} diff --git a/web/components/ui/dialog.tsx b/web/components/ui/dialog.tsx index a40c17e..fb5b7c8 100644 --- a/web/components/ui/dialog.tsx +++ b/web/components/ui/dialog.tsx @@ -37,8 +37,11 @@ const DialogContent = React.forwardRef< { expect(await read()).toBe('665f1c2a9b1e4a0012ab34cd') }) + // Support alerts carry a full conversation URL, which has no break + // opportunity. That single token used to set a min-content floor on the + // dialog's grid track, pushing the message box, the error panel and the + // action button hundreds of pixels outside the card. + for (const viewport of [ + { name: 'mobile', width: 375, height: 800 }, + { name: 'desktop', width: 1280, height: 800 }, + ]) { + test(`a long unbreakable URL stays inside the details dialog on ${viewport.name}`, async ({ + page, + context, + }) => { + await page.setViewportSize({ + width: viewport.width, + height: viewport.height, + }) + await authenticate(context) + await mockApi(page) + + await page.route('**/api/v1/gateway/devices/*/messages*', (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + data: [ + { + _id: 'longurl_1', + recipient: '+251912657519', + message: + 'Support request - textbee support\nUrgency: Urgent\nView conversation:\nhttps://dash.supporthq.app/dashboard/projects/69a591f28bdb2cda5452246b/conversations/69c2a0a058b1a815140a9be5', + status: 'failed', + type: 'sent', + errorCode: '1', + errorMessage: + 'SMS failed on device. Common causes: no SMS credit on SIM, weak signal, or carrier blocked. Check SIM balance and signal, then try again. (code 0)', + device: { _id: 'device_1', brand: 'samsung', model: 'SM-A346E' }, + requestedAt: new Date().toISOString(), + createdAt: new Date().toISOString(), + }, + ], + meta: { total: 1, page: 1, limit: 20, totalPages: 1 }, + }), + }) + ) + + await page.goto('/dashboard/messaging/history') + await page.getByRole('button', { name: /Support request/ }).click() + + const dialog = page.getByRole('dialog') + await expect(dialog).toBeVisible() + + const overflows = await dialog.evaluate( + (el) => el.scrollWidth > el.clientWidth + 1 + ) + expect(overflows, 'details dialog must not scroll sideways').toBe(false) + + // The visible symptom was the action button rendering outside the card, + // which a scrollWidth check alone would miss if a parent clipped it. + const dialogBox = await dialog.boundingBox() + const buttonBox = await dialog + .getByRole('button', { name: 'Follow up' }) + .boundingBox() + expect(dialogBox && buttonBox).toBeTruthy() + expect( + buttonBox!.x + buttonBox!.width, + 'the action button must stay inside the dialog' + ).toBeLessThanOrEqual(dialogBox!.x + dialogBox!.width + 1) + + // The page must not gain a sideways scrollbar either. + const pageOverflow = await page.evaluate( + () => document.documentElement.scrollWidth - window.innerWidth + ) + expect(pageOverflow, 'page must not scroll sideways').toBeLessThanOrEqual( + 0 + ) + }) + } + test('replying from a received message keeps a device selected', async ({ page, context,