This is an automated email from the ASF dual-hosted git repository.
voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new d276aad338 [ZEPPELIN-6699] React remote CSV export omits the UTF-8 BOM
that the Angular and classic paths write
d276aad338 is described below
commit d276aad3380cada5ee0e9bc8eb0ce0be18d07fcf
Author: Lee SuJung <[email protected]>
AuthorDate: Thu Sep 10 20:27:37 2026 +0900
[ZEPPELIN-6699] React remote CSV export omits the UTF-8 BOM that the
Angular and classic paths write
### What is this PR for?
The React remote builds its CSV export by hand and hands the string
straight to a `Blob` with no UTF-8 BOM, so Excel reads it in the system code
page and garbles non-ASCII data. The classic path prepends one through
`saveAsService`, and the Angular path gets one from `XLSX.writeFile()`, so only
this export was missing it. ZEPPELIN-672 added the BOM for exactly this reason.
```diff
- const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
+ const BOM = '\uFEFF';
+ const blob = new Blob([BOM, content], { type: 'text/plain;charset=utf-8'
});
```
The xlsx branch is left alone, since that format carries its own encoding.
### What type of PR is it?
Bug Fix
### Todos
* [x] Prepend the BOM in the React remote CSV export
* [x] Add a regression test asserting the saved bytes
### What is the Jira issue?
* [ZEPPELIN-6699](https://issues.apache.org/jira/browse/ZEPPELIN-6699)
### How should this be tested?
New spec `exportFile.spec.ts` mocks `file-saver` and inspects the bytes
handed to `saveAs`, since `Blob.text()` decodes and drops a leading BOM.
```
cd zeppelin-web-angular/projects/zeppelin-react && npm test
```
Result: `Tests 1 failed | 67 passed (68)`. The one failure is
`HTMLRenderer.spec.tsx > highlights a code block`, which fails the same way on
`origin/master` without this change. Running the new spec alone passes:
```
npx vitest run src/utils/exportFile.spec.ts → Tests 2 passed (2)
```
Reverting only the production change makes it fail with `expected [ 110,
97, 109 ] to deeply equal [ 239, 187, 191 ]`, so it does catch the defect.
Manual: run a paragraph whose table result contains non-ASCII text, open
the published paragraph with `?react=true`, and choose "Export all data as
csv". The file now starts with `EF BB BF` and opens in Excel with the text
intact.
### Screenshots (if appropriate)
N/A
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No — the BOM only makes the
React export match what the other two paths already write
* Does this needs documentation? No
Note: the issue scopes this to CSV and TSV, but the React remote has no TSV
path, since `exportFile()` only takes `'csv' | 'xlsx'`. I'll file that parity
gap separately; the BOM sits on the shared branch, so a TSV path added there
inherits it.
Closes #5466 from xhaktm00/ZEPPELIN-6699.
Signed-off-by: YONGJAE LEE <[email protected]>
---
.../zeppelin-react/src/utils/exportFile.spec.ts | 48 ++++++++++++++++++++++
.../zeppelin-react/src/utils/exportFile.ts | 5 ++-
2 files changed, 52 insertions(+), 1 deletion(-)
diff --git
a/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.spec.ts
b/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.spec.ts
new file mode 100644
index 0000000000..adc654cbff
--- /dev/null
+++ b/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.spec.ts
@@ -0,0 +1,48 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { exportFile } from './exportFile';
+
+const saveAs = vi.fn();
+vi.mock('file-saver', () => ({ saveAs: (...args: unknown[]) => saveAs(...args)
}));
+
+const UTF8_BOM = [0xef, 0xbb, 0xbf];
+
+/** The saved bytes, since Blob.text() decodes and drops a leading BOM. */
+async function savedBytes(): Promise<Uint8Array> {
+ expect(saveAs).toHaveBeenCalledOnce();
+ const blob = saveAs.mock.calls[0][0] as Blob;
+ return new Uint8Array(await blob.arrayBuffer());
+}
+
+describe('exportFile', () => {
+ beforeEach(() => {
+ saveAs.mockClear();
+ });
+
+ it('prepends a UTF-8 BOM to the CSV so Excel reads it as UTF-8', async () =>
{
+ await exportFile({ columnNames: ['name', 'city'], rows: [['alice', '서울']]
}, 'csv');
+
+ const bytes = await savedBytes();
+ expect([...bytes.subarray(0, 3)]).toEqual(UTF8_BOM);
+ expect(new
TextDecoder().decode(bytes.subarray(3))).toBe('name,city\nalice,서울');
+ expect(saveAs.mock.calls[0][1]).toBe('export.csv');
+ });
+
+ it('does not export an empty table', async () => {
+ await exportFile({ columnNames: ['name'], rows: [] }, 'csv');
+
+ expect(saveAs).not.toHaveBeenCalled();
+ });
+});
diff --git
a/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.ts
b/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.ts
index c59525309f..8b82c61075 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.ts
+++ b/zeppelin-web-angular/projects/zeppelin-react/src/utils/exportFile.ts
@@ -42,7 +42,10 @@ export const exportFile = async (tableData: TableData, type:
'csv' | 'xlsx') =>
const rows = tableData.rows.map(row => row.join(separator));
const content = [header, ...rows].join('\n');
- const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
+ // Excel reads a CSV without a BOM in the system code page, garbling
non-ASCII data. The
+ // Angular and classic exports write one for the same reason
(ZEPPELIN-672).
+ const BOM = '\uFEFF';
+ const blob = new Blob([BOM, content], { type: 'text/plain;charset=utf-8'
});
saveAs(blob, `export.${type}`);
}
};