From c74459842586b4bdf3bdbb45a8f034e1d55e9999 Mon Sep 17 00:00:00 2001
From: lxk955 <164191365+lxk955@users.noreply.github.com>
Date: Sun, 11 Oct 2026 08:46:09 +0800
Subject: [PATCH] feat(import): allow empty string as null for Excel import
---
.../components/import/TableImportDialog.vue | 7 ++++
.../TableImportDialog.batchSelection.spec.ts | 35 +++++++++++++++++++
apps/desktop/src/i18n/locales/az.ts | 1 +
apps/desktop/src/i18n/locales/en.ts | 1 +
apps/desktop/src/i18n/locales/es.ts | 1 +
apps/desktop/src/i18n/locales/id.ts | 1 +
apps/desktop/src/i18n/locales/it.ts | 1 +
apps/desktop/src/i18n/locales/ja.ts | 1 +
apps/desktop/src/i18n/locales/ko.ts | 1 +
apps/desktop/src/i18n/locales/pt-BR.ts | 1 +
apps/desktop/src/i18n/locales/ru.ts | 1 +
apps/desktop/src/i18n/locales/tr.ts | 1 +
apps/desktop/src/i18n/locales/zh-CN.ts | 1 +
apps/desktop/src/i18n/locales/zh-TW.ts | 1 +
14 files changed, 54 insertions(+)
diff --git a/apps/desktop/src/components/import/TableImportDialog.vue b/apps/desktop/src/components/import/TableImportDialog.vue
index b1ca5720b4..4418497351 100644
--- a/apps/desktop/src/components/import/TableImportDialog.vue
+++ b/apps/desktop/src/components/import/TableImportDialog.vue
@@ -1698,6 +1698,13 @@ watch(rawProgressPercent, (percent) => {
+
+
+
{{ t("tableImport.emptyStringAsNullExcelHint") }}
+
diff --git a/apps/desktop/src/components/import/__tests__/TableImportDialog.batchSelection.spec.ts b/apps/desktop/src/components/import/__tests__/TableImportDialog.batchSelection.spec.ts
index 35b0691374..5aedfdf3b1 100644
--- a/apps/desktop/src/components/import/__tests__/TableImportDialog.batchSelection.spec.ts
+++ b/apps/desktop/src/components/import/__tests__/TableImportDialog.batchSelection.spec.ts
@@ -650,4 +650,39 @@ describe("TableImportDialog batch selection", () => {
expect(document.body.textContent).not.toContain("Title row must be before the data start row");
expect(button("Next").disabled).toBe(false);
});
+
+ it("renders empty string as null option for Excel source and passes it to parse options (#11608)", async () => {
+ let capturedOptions: any = null;
+ mocks.previewTableImportFile.mockImplementation((_source, options) => {
+ capturedOptions = options;
+ return Promise.resolve({
+ fileName: "rows.xlsx",
+ filePath: "/tmp/rows.xlsx",
+ sourceRef: "workbook-source",
+ fileType: "excel",
+ sizeBytes: 10,
+ columns: ["id", "name"],
+ rows: [[1, "test"]],
+ totalRows: 1,
+ totalRowsExact: true,
+ sourceFingerprint: "rows-Sheet1",
+ sheets: ["Sheet1"],
+ });
+ });
+ await mountDialog([new File(["workbook"], "rows.xlsx")]);
+ expect(button("Next").disabled).toBe(false);
+
+ const emptyStringCheckbox = [...document.body.querySelectorAll('input[type="checkbox"]')].find((el) => el.parentElement?.textContent?.includes("Empty string as NULL"));
+ expect(emptyStringCheckbox).toBeTruthy();
+ expect(emptyStringCheckbox!.checked).toBe(false);
+
+ emptyStringCheckbox!.click();
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ await flushAsyncUpdates();
+
+ expect(capturedOptions?.parseOptions?.emptyStringAsNull).toBe(true);
+
+ await startImport();
+ expect(mocks.importTableFile.mock.calls.at(-1)?.[0].parseOptions.emptyStringAsNull).toBe(true);
+ });
});
diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts
index 111586511f..da7c0793bf 100644
--- a/apps/desktop/src/i18n/locales/az.ts
+++ b/apps/desktop/src/i18n/locales/az.ts
@@ -6282,6 +6282,7 @@ export default withEnglishFallback({
trimValues: "Dəyərlərin əvvəlindəki və sonundakı boşluqları sil",
emptyStringAsNull: "Boş sətri NULL kimi qəbul et",
emptyStringAsNullHint: "İşarələndikdə boş sahələr NULL kimi idxal olunur və \\N adi mətn olur; əks halda boş sahələr boş sətir qalır və \\N NULL kimi idxal olunur.",
+ emptyStringAsNullExcelHint: "İşarələndikdə boş sətirlər NULL kimi idxal olunur; əks halda boş sətir kimi saxlanılır.",
jsonShape: "JSON sətirləri",
jsonShapeAuto: "Avtomatik müəyyən et",
jsonShapeObjects: "Obyektlər",
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 8a47f20d44..2b2a6ceca2 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -7181,6 +7181,7 @@ export default {
trimValues: "Trim values",
emptyStringAsNull: "Empty string as NULL",
emptyStringAsNullHint: "When checked, empty fields import as NULL and \\N becomes plain text; otherwise empty fields stay as empty strings and \\N imports as NULL.",
+ emptyStringAsNullExcelHint: "When checked, empty strings are imported as NULL; otherwise, they are preserved as empty strings.",
conflictPolicy: "On primary-key conflict",
conflictError: "Stop with an error",
skipDuplicateRows: "Skip duplicate rows",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 5588d33dec..9ecf3aebb3 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -6545,6 +6545,7 @@ export default withEnglishFallback({
trimValues: "Trim values",
emptyStringAsNull: "Empty string as NULL",
emptyStringAsNullHint: "Si se activa, los campos vacíos se importan como NULL y \\N pasa a ser texto; si no, los campos vacíos se mantienen como cadenas vacías y \\N se importa como NULL.",
+ emptyStringAsNullExcelHint: "Si se activa, las cadenas vacías se importan como NULL; si no, se mantienen como cadenas vacías.",
conflictPolicy: "Ante conflicto de clave primaria",
conflictError: "Detener con error",
skipDuplicateRows: "Omitir filas duplicadas",
diff --git a/apps/desktop/src/i18n/locales/id.ts b/apps/desktop/src/i18n/locales/id.ts
index ec706738f0..479ad8bf06 100644
--- a/apps/desktop/src/i18n/locales/id.ts
+++ b/apps/desktop/src/i18n/locales/id.ts
@@ -6936,6 +6936,7 @@ export default withEnglishFallback({
trimValues: "Pangkas nilai",
emptyStringAsNull: "String kosong sebagai NULL",
emptyStringAsNullHint: "Jika dicentang, bidang kosong diimpor sebagai NULL dan \\N menjadi teks biasa; jika tidak, bidang kosong tetap berupa string kosong dan \\N diimpor sebagai NULL.",
+ emptyStringAsNullExcelHint: "Jika dicentang, string kosong diimpor sebagai NULL; jika tidak, tetap berupa string kosong.",
conflictPolicy: "Saat terjadi konflik kunci utama",
conflictError: "Hentikan dengan galat",
skipDuplicateRows: "Lewati baris duplikat",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 7c5071bf9b..d23fa94f2b 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -6347,6 +6347,7 @@ export default withEnglishFallback({
trimValues: "Trim values",
emptyStringAsNull: "Empty string as NULL",
emptyStringAsNullHint: "Se attivo, i campi vuoti vengono importati come NULL e \\N diventa testo normale; altrimenti restano stringhe vuote e \\N viene importato come NULL.",
+ emptyStringAsNullExcelHint: "Se attivo, le stringhe vuote vengono importate come NULL; altrimenti rimangono stringhe vuote.",
jsonShape: "JSON rows",
jsonShapeAuto: "Auto-detect",
jsonShapeObjects: "Objects",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index b3184a4dc8..ba7af5c86c 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -6363,6 +6363,7 @@ export default withEnglishFallback({
trimValues: "Trim values",
emptyStringAsNull: "Empty string as NULL",
emptyStringAsNullHint: "チェックすると空フィールドを NULL として取り込み、\\N は通常のテキストになります。チェックしない場合は空フィールドは空文字列のままとなり、\\N を NULL として取り込みます。",
+ emptyStringAsNullExcelHint: "チェックすると空文字列を NULL として取り込みます。チェックしない場合は空文字列のまま保持します。",
jsonShape: "JSON rows",
jsonShapeAuto: "Auto-detect",
jsonShapeObjects: "Objects",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index eadedbb5a4..c257855823 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -6144,6 +6144,7 @@ export default withEnglishFallback({
trimValues: "값 양끝 공백 제거",
emptyStringAsNull: "빈 문자열을 NULL로",
emptyStringAsNullHint: "체크하면 빈 필드를 NULL로 가져오고 \\N은 일반 텍스트가 됩니다. 해제하면 빈 필드는 빈 문자열로 유지되고 \\N은 NULL로 가져옵니다.",
+ emptyStringAsNullExcelHint: "체크하면 빈 문자열을 NULL로 가져옵니다. 해제하면 빈 문자열로 유지됩니다.",
jsonShape: "JSON 행",
jsonShapeAuto: "자동 감지",
jsonShapeObjects: "객체",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 715bb18991..34b65e6c3c 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -6346,6 +6346,7 @@ export default withEnglishFallback({
trimValues: "Trim values",
emptyStringAsNull: "Empty string as NULL",
emptyStringAsNullHint: "Quando marcado, campos vazios são importados como NULL e \\N passa a ser texto; caso contrário, permanecem como strings vazias e \\N é importado como NULL.",
+ emptyStringAsNullExcelHint: "Quando marcado, strings vazias são importadas como NULL; caso contrário, permanecem como strings vazias.",
jsonShape: "JSON rows",
jsonShapeAuto: "Auto-detect",
jsonShapeObjects: "Objects",
diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts
index 317f6f8f91..2a24283c63 100644
--- a/apps/desktop/src/i18n/locales/ru.ts
+++ b/apps/desktop/src/i18n/locales/ru.ts
@@ -7537,6 +7537,7 @@ export default withEnglishFallback({
trimValues: "Обрезать значения",
emptyStringAsNull: "Пустая строка как NULL",
emptyStringAsNullHint: "Если включено, пустые поля импортируются как NULL, а \\N считается обычным текстом; иначе пустые поля остаются пустыми строками, а \\N импортируется как NULL.",
+ emptyStringAsNullExcelHint: "Если включено, пустые строки импортируются как NULL; иначе сохраняются как пустые строки.",
jsonShape: "Строки JSON",
jsonShapeAuto: "Автоопределение",
jsonShapeObjects: "Объекты",
diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts
index 3443c63c8a..f35a76147d 100644
--- a/apps/desktop/src/i18n/locales/tr.ts
+++ b/apps/desktop/src/i18n/locales/tr.ts
@@ -6163,6 +6163,7 @@ export default withEnglishFallback({
trimValues: "Değerleri kırp",
emptyStringAsNull: "Boş dizeyi NULL say",
emptyStringAsNullHint: "İşaretlendiğinde boş alanlar NULL olarak içe aktarılır ve \\N düz metin olur; aksi halde boş alanlar boş dize kalır ve \\N NULL olarak içe aktarılır.",
+ emptyStringAsNullExcelHint: "İşaretlendiğinde boş dizeler NULL olarak içe aktarılır; aksi halde boş dize olarak korunur.",
jsonShape: "JSON satırları",
jsonShapeAuto: "Otomatik algıla",
jsonShapeObjects: "Nesneler",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index bb63260db8..c903e7c08a 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -7166,6 +7166,7 @@ export default withEnglishFallback({
trimValues: "裁剪空白",
emptyStringAsNull: "空字符串作为 NULL",
emptyStringAsNullHint: "勾选后空字段一律按 NULL 导入(此时文件里的 \\N 按普通文本导入);不勾选则空字段保留为空字符串,并把 \\N 按 NULL 导入。",
+ emptyStringAsNullExcelHint: "勾选后空字符串识别为 NULL;不勾选则保留为空字符串。",
conflictPolicy: "主键冲突时",
conflictError: "停止并报错",
skipDuplicateRows: "跳过重复行",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 1934c2eb67..27d8960441 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -5681,6 +5681,7 @@ export default withEnglishFallback({
trimValues: "裁剪空白",
emptyStringAsNull: "空字串作為 NULL",
emptyStringAsNullHint: "勾選後空欄位一律以 NULL 匯入(此時檔案裡的 \\N 以一般文字匯入);不勾選則空欄位保留為空字串,並把 \\N 以 NULL 匯入。",
+ emptyStringAsNullExcelHint: "勾選後空字串識別為 NULL;不勾選則保留為空字串。",
conflictPolicy: "主鍵衝突時",
conflictError: "停止並回報錯誤",
skipDuplicateRows: "略過重複列",