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;
}, {}),
};