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: "略過重複列",