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 116539004 fix(csv): preserve literal formula apostrophes (#2758)
116539004 is described below

commit 116539004dc290f0738979fa9c70f9d4bedca293
Author: btlqql <[email protected]>
AuthorDate: Wed Sep 2 17:13:02 2026 +0800

    fix(csv): preserve literal formula apostrophes (#2758)
---
 web/src/utils/download.test.ts          |  8 +++++++-
 web/src/utils/download.ts               |  5 ++++-
 web/src/utils/resourceCsvImport.test.ts | 12 ++++++++++--
 web/src/utils/resourceCsvImport.ts      |  9 ++++-----
 4 files changed, 25 insertions(+), 9 deletions(-)

diff --git a/web/src/utils/download.test.ts b/web/src/utils/download.test.ts
index 04ae3a066..35d0ed6ca 100644
--- a/web/src/utils/download.test.ts
+++ b/web/src/utils/download.test.ts
@@ -27,11 +27,17 @@ describe('buildCsv', () => {
       [
         { name: '=SUM(A1:A2)', remark: 'hello, "mq"' },
         { name: '\nline-feed', remark: null },
+        { name: "'=literal", remark: "''+two-apostrophes" },
       ],
     );
 
     expect(csv).toBe(
-      ['"Name","Remark"', '"\'=SUM(A1:A2)","hello, ""mq"""', 
'"\'\nline-feed",""'].join('\n'),
+      [
+        '"Name","Remark"',
+        '"\'=SUM(A1:A2)","hello, ""mq"""',
+        '"\'\nline-feed",""',
+        '"\'\'=literal","\'\'\'+two-apostrophes"',
+      ].join('\n'),
     );
   });
 });
diff --git a/web/src/utils/download.ts b/web/src/utils/download.ts
index 61dba59f8..51f502f96 100644
--- a/web/src/utils/download.ts
+++ b/web/src/utils/download.ts
@@ -37,7 +37,10 @@ export interface CsvColumn<T> {
 
 export const escapeCsvCell = (value: unknown) => {
   const text = value == null ? '' : String(value);
-  const formulaSafeText = /^[=+\-@\t\r\n]/.test(text) ? `'${text}` : text;
+  // Prefix both formulas and literal apostrophe-prefixed formulas. The 
importer removes
+  // exactly one protection apostrophe, so any apostrophes supplied by the 
user survive a
+  // complete export/import round trip.
+  const formulaSafeText = /^(?:[=+\-@\t\r\n]|'+[=+\-@\t\r\n])/.test(text) ? 
`'${text}` : text;
   return `"${formulaSafeText.replace(/"/g, '""')}"`;
 };
 
diff --git a/web/src/utils/resourceCsvImport.test.ts 
b/web/src/utils/resourceCsvImport.test.ts
index 1f0381cab..55ff443dd 100644
--- a/web/src/utils/resourceCsvImport.test.ts
+++ b/web/src/utils/resourceCsvImport.test.ts
@@ -23,6 +23,7 @@ import {
   validateResourceName,
   validateTopicCsvImport,
 } from './resourceCsvImport';
+import { buildCsv } from './download';
 
 describe('resourceCsvImport', () => {
   it('parses RFC4180 CSV with BOM, CRLF, quoted commas, newlines, and escaped 
quotes', () => {
@@ -57,13 +58,20 @@ describe('resourceCsvImport', () => {
   });
 
   it('round-trips formula-safe apostrophes from exported cells', () => {
-    const records = 
parseCsvTable('"Name","Remark"\n"\'-topic","\'=keep-original"');
+    const csv = buildCsv(
+      [
+        { header: 'Name', value: (row: { name: string; remark: string }) => 
row.name },
+        { header: 'Remark', value: (row: { name: string; remark: string }) => 
row.remark },
+      ],
+      [{ name: '-topic', remark: "'=keep-original" }],
+    );
+    const records = parseCsvTable(csv);
     const validation = validateTopicCsvImport(records, 'instance-1');
 
     expect(validation.errors).toEqual([]);
     expect(validation.rows[0].payload).toMatchObject({
       name: '-topic',
-      remark: '=keep-original',
+      remark: "'=keep-original",
       instanceId: 'instance-1',
     });
   });
diff --git a/web/src/utils/resourceCsvImport.ts 
b/web/src/utils/resourceCsvImport.ts
index c10314fb6..dc8b1f036 100644
--- a/web/src/utils/resourceCsvImport.ts
+++ b/web/src/utils/resourceCsvImport.ts
@@ -43,7 +43,7 @@ interface ParsedCsvRow {
   cells: string[];
 }
 
-const FORMULA_SAFE_PREFIX_PATTERN = /^'(?=[=+\-@\t\r\n])/;
+const FORMULA_SAFE_PREFIX_PATTERN = /^'(?='*[=+\-@\t\r\n])/;
 
 // Aligned with RocketMQ's TopicValidator/GroupValidator: a shared character 
set (letters,
 // digits, underscore, hyphen, % and |) with per-kind length caps. Topics cap 
at 127 and
@@ -79,8 +79,7 @@ const restoreFormulaSafeCell = (value: string): string =>
 
 const normalizeHeader = (header: string): string => 
restoreFormulaSafeCell(header).trim();
 
-const normalizeValue = (value: string | undefined): string =>
-  restoreFormulaSafeCell(value ?? '').trim();
+const normalizeValue = (value: string | undefined): string => (value ?? 
'').trim();
 
 const normalizeDeliveryOrderType = (value: string): string =>
   value === 'MESSAGES ORDER' ? 'MESSAGES_ORDER' : value;
@@ -122,7 +121,7 @@ const readCsvRows = (content: string): ParsedCsvRow[] => {
     if (nextCells.some((value) => value.trim() !== '')) {
       rows.push({
         lineNumber: rowStartLine,
-        cells: nextCells.map(restoreFormulaSafeCell),
+        cells: nextCells,
       });
     }
     cells = [];
@@ -216,7 +215,7 @@ export const parseCsvTable = (content: string): CsvRecord[] 
=> {
     return {
       lineNumber: row.lineNumber,
       values: headers.reduce<Record<string, string>>((acc, header, index) => {
-        acc[header] = normalizeValue(row.cells[index]);
+        acc[header] = restoreFormulaSafeCell(row.cells[index] ?? '').trim();
         return acc;
       }, {}),
     };

Reply via email to