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,