This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 9d288da90 fix(web): preserve the original message body in downloads 
and copies (#4777)
9d288da90 is described below

commit 9d288da909fe1a258f72d35ff1b9adea5321e528
Author: Wang1rrr <[email protected]>
AuthorDate: Thu Sep 24 10:47:58 2026 +0800

    fix(web): preserve the original message body in downloads and copies (#4777)
    
    The download and copy actions reused the display formatter, which parses 
the body as JSON and re-serialises it, rounding unsafe integers and rewriting 
whitespace even though the received body is already a string. Both now use the 
received body directly; the display policy is unchanged.
---
 .../pages/instance/__tests__/MessagePage.test.tsx  | 66 ++++++++++++++++++++++
 web/src/pages/instance/message.tsx                 |  4 +-
 2 files changed, 68 insertions(+), 2 deletions(-)

diff --git a/web/src/pages/instance/__tests__/MessagePage.test.tsx 
b/web/src/pages/instance/__tests__/MessagePage.test.tsx
index b737ab01b..963f0a79f 100644
--- a/web/src/pages/instance/__tests__/MessagePage.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePage.test.tsx
@@ -23,6 +23,7 @@ import { MemoryRouter } from 'react-router-dom';
 import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 
'vitest';
 import type { MessageRecord } from '../../../api/message';
 import { LangProvider } from '../../../i18n/LangContext';
+import * as downloadUtils from '../../../utils/download';
 
 const messageServiceMocks = vi.hoisted(() => ({
   consumeMessageDirectly: vi.fn(),
@@ -276,6 +277,71 @@ describe('Message page query history', () => {
     expect(messageServiceMocks.queryMessages).not.toHaveBeenCalled();
   });
 
+  it.each([
+    ['UnsafeInteger', '{"orderId":9007199254740993}'],
+    ['Int64Max', '{"orderId":9223372036854775807}'],
+    ['SafeInteger', '{\n  "orderId": 9007199254740991\n}'],
+    ['QuotedId', '{\n  "orderId": "9223372036854775807"\n}'],
+    ['JsonWhitespace', '{\r\n\t"message": "你好",  "enabled": true\r\n}\r\n'],
+    ['PlainText', '订单状态: ready\r\n  next line\r\n'],
+  ])('preservesOriginal%sBodyWhenDownloadingTest', async (_name, body) => {
+    const user = userEvent.setup();
+    const download = vi.spyOn(downloadUtils, 
'downloadBlob').mockImplementation(() => {});
+    const msgId = 'MID-DOWNLOAD';
+    messageServiceMocks.queryMessages.mockResolvedValue([{ 
...createMessage(msgId), body }]);
+    renderWithProviders(<MessagePage />);
+
+    await user.click(lastElement(screen.getAllByRole('combobox')));
+    await user.click(lastElement(await screen.findAllByText('order-create')));
+    await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+
+    const row = await screen.findByRole('row', { name: new RegExp(msgId) });
+    await user.click(within(row).getByRole('button', { name: /下载/ }));
+
+    expect(download).toHaveBeenCalledTimes(1);
+    const [blob, filename] = download.mock.calls[0];
+    expect(filename).toBe(`${msgId}.json`);
+    expect(blob.type).toBe('application/json');
+    await expect(blob.text()).resolves.toBe(body);
+  });
+
+  it('copiesOriginalBodyFromMessageDetailsTest', async () => {
+    const user = userEvent.setup();
+    const body = '{ "orderId":9223372036854775807 }\r\n';
+    messageServiceMocks.queryMessages.mockResolvedValue([{ 
...createMessage('MID-COPY'), body }]);
+    renderWithProviders(<MessagePage />);
+
+    await user.click(lastElement(screen.getAllByRole('combobox')));
+    await user.click(lastElement(await screen.findAllByText('order-create')));
+    await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+    const row = await screen.findByRole('row', { name: /MID-COPY/ });
+    await user.click(within(row).getByRole('button', { name: /详情/ }));
+
+    const dialog = await screen.findByRole('dialog', { name: '消息详情' });
+    const bodyParagraph = within(dialog).getByText(/"orderId":/);
+    const originalExecCommand = Object.getOwnPropertyDescriptor(document, 
'execCommand');
+    let copiedText: string | undefined;
+    const execCommand = vi.fn(() => {
+      copiedText = document.getSelection()?.toString();
+      return true;
+    });
+    Object.defineProperty(document, 'execCommand', {
+      configurable: true,
+      value: execCommand,
+    });
+    try {
+      await user.click(within(bodyParagraph).getByRole('button'));
+      expect(execCommand).toHaveBeenCalledWith('copy');
+      expect(copiedText).toBe(body);
+    } finally {
+      if (originalExecCommand) {
+        Object.defineProperty(document, 'execCommand', originalExecCommand);
+      } else {
+        Reflect.deleteProperty(document, 'execCommand');
+      }
+    }
+  });
+
   it('shows the redelivery count on the message detail panel', async () => {
     const user = userEvent.setup({ pointerEventsCheck: 0 });
     messageServiceMocks.queryMessages.mockResolvedValue([
diff --git a/web/src/pages/instance/message.tsx 
b/web/src/pages/instance/message.tsx
index 4806404d4..921f42dd6 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -728,7 +728,7 @@ const MessagePageContent = ({
   };
 
   const handleDownload = (record: MessageRecord) => {
-    const blob = new Blob([formatBody(record.body)], { type: 
'application/json' });
+    const blob = new Blob([record.body], { type: 'application/json' });
     downloadBlob(blob, `${record.msgId}.json`);
     message.success(t('messagePage.downloadSuccess'));
   };
@@ -943,7 +943,7 @@ const MessagePageContent = ({
             {t('topic.messageBody')}
           </Typography.Title>
           <Paragraph
-            copyable
+            copyable={{ text: selectedMsg.body }}
             style={{
               background: '#f5f5f5',
               padding: '12px 16px',

Reply via email to