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}`);
   }
 };

Reply via email to