From 7b95f0ca176333dc45c1bbc23a6ca7599dcf6173 Mon Sep 17 00:00:00 2001
From: Maxon Phong <55618346+Aealen@users.noreply.github.com>
Date: Fri, 31 Jul 2026 16:24:29 +0800
Subject: [PATCH] fix(import): preserve DataGrip groups and Kingbase
connections
---
apps/desktop/src/i18n/locales/en.ts | 2 +
apps/desktop/src/i18n/locales/es.ts | 2 +
apps/desktop/src/i18n/locales/it.ts | 2 +
apps/desktop/src/i18n/locales/ja.ts | 2 +
apps/desktop/src/i18n/locales/pt-BR.ts | 2 +
apps/desktop/src/i18n/locales/zh-CN.ts | 2 +
apps/desktop/src/i18n/locales/zh-TW.ts | 2 +
.../__tests__/imports/datagripImport.spec.ts | 125 +++++++++++++++++-
.../desktop/src/lib/imports/datagripImport.ts | 46 ++++++-
apps/desktop/src/stores/connectionStore.ts | 42 +++---
10 files changed, 208 insertions(+), 19 deletions(-)
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index bf161a93b..d363bc61c 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -1680,6 +1680,8 @@ export default {
importSuccess: "Imported {count} connection(s)",
importNavicatSuccess: "Imported {count} Navicat connection(s). Fill in any connection whose password is still empty before testing.",
importDatagripSuccess: "Imported {count} DataGrip connection(s), filled {filled} password(s) from macOS Keychain.",
+ importDatagripSelectFiles: "Please select dataSources.xml (required), and optionally dataSources.local.xml / db-forest-config.xml",
+ importDatagripDialogTitle: "Select DataGrip configuration files",
importDbeaverSuccess: "Imported {count} DBeaver connection(s). Fill in any connection whose password is still empty before testing.",
importNone: "No new connections to import",
importLayoutConfirm: "The imported file contains connection groups. Apply them?",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 13da31e6b..dc9fbd2a3 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -1627,6 +1627,8 @@ export default withEnglishFallback({
importLayoutConfirm: "El archivo importado contiene grupos de conexiones. ¿Deseas aplicarlos?",
importLayoutTitle: "Importar grupos",
importLayoutApply: "Aplicar",
+ importDatagripSelectFiles: "Seleccione dataSources.xml (obligatorio); también puede seleccionar dataSources.local.xml y db-forest-config.xml.",
+ importDatagripDialogTitle: "Seleccionar archivos de configuración de DataGrip",
},
ai: {
placeholder: "Describe tu consulta en lenguaje natural...",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index d2521043b..59353aefd 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -1625,6 +1625,8 @@ export default withEnglishFallback({
importLayoutConfirm: "Il file importato contiene gruppi di connessioni. Vuoi applicarli?",
importLayoutTitle: "Importa Gruppi",
importLayoutApply: "Applica",
+ importDatagripSelectFiles: "Seleziona dataSources.xml (obbligatorio), puoi selezionare anche dataSources.local.xml e db-forest-config.xml",
+ importDatagripDialogTitle: "Seleziona i file di configurazione DataGrip",
},
ai: {
placeholder: "Descrivi la tua query in linguaggio naturale...",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index cb4ee9c80..341650145 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -1652,6 +1652,8 @@ export default withEnglishFallback({
importLayoutConfirm: "インポートされたファイルに接続グループが含まれています。適用しますか?",
importLayoutTitle: "グループをインポート",
importLayoutApply: "適用",
+ importDatagripSelectFiles: "dataSources.xml(必須)を選択してください。dataSources.local.xml と db-forest-config.xml も同時に選択できます。",
+ importDatagripDialogTitle: "DataGrip 設定ファイルを選択",
},
ai: {
placeholder: "自然言語でクエリを説明してください...",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 74c0101d6..ed8918fef 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -1627,6 +1627,8 @@ export default withEnglishFallback({
importLayoutConfirm: "O arquivo importado contém grupos de conexões. Aplicá-los?",
importLayoutTitle: "Importar Grupos",
importLayoutApply: "Aplicar",
+ importDatagripSelectFiles: "Selecione dataSources.xml (obrigatório), você também pode selecionar dataSources.local.xml e db-forest-config.xml",
+ importDatagripDialogTitle: "Selecione o arquivo de configuração do DataGrip",
},
ai: {
placeholder: "Descreva sua consulta em linguagem natural...",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 893ee2fa9..b346deb0c 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -1681,6 +1681,8 @@ export default withEnglishFallback({
importSuccess: "已导入 {count} 个连接",
importNavicatSuccess: "已导入 {count} 个 Navicat 连接,若个别连接密码为空请补填后测试连接",
importDatagripSuccess: "已导入 {count} 个 DataGrip 连接,已自动读取 {filled} 个密码(来自 macOS 钥匙串)",
+ importDatagripSelectFiles: "请选择 dataSources.xml(必需),可同时选择 dataSources.local.xml 和 db-forest-config.xml",
+ importDatagripDialogTitle: "选择 DataGrip 配置文件",
importDbeaverSuccess: "已导入 {count} 个 DBeaver 连接,若个别连接密码为空请补填后测试连接",
importNone: "没有新的连接需要导入",
importLayoutConfirm: "导入文件包含连接分组信息,是否一并应用?",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 016840c90..ac243bd05 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -1621,6 +1621,8 @@ export default withEnglishFallback({
importSuccess: "已匯入 {count} 個連線",
importNavicatSuccess: "已匯入 {count} 個 Navicat 連線,若個別連線密碼為空請補填後測試連線",
importDatagripSuccess: "已匯入 {count} 個 DataGrip 連線,已自動讀取 {filled} 個密碼(來自 macOS 鑰匙圈)",
+ importDatagripSelectFiles: "請選擇 dataSources.xml(必需),可同時選擇 dataSources.local.xml 和 db-forest-config.xml",
+ importDatagripDialogTitle: "選擇 DataGrip 設定檔",
importDbeaverSuccess: "已匯入 {count} 個 DBeaver 連線,若個別連線密碼為空請補填後測試連線",
importNone: "沒有新的連線需要匯入",
importLayoutConfirm: "匯入檔案包含連線群組資訊,是否一併套用?",
diff --git a/apps/desktop/src/lib/__tests__/imports/datagripImport.spec.ts b/apps/desktop/src/lib/__tests__/imports/datagripImport.spec.ts
index 8d5633f5a..e83e548ce 100644
--- a/apps/desktop/src/lib/__tests__/imports/datagripImport.spec.ts
+++ b/apps/desktop/src/lib/__tests__/imports/datagripImport.spec.ts
@@ -2,7 +2,7 @@
import { describe, expect, it } from "vitest";
import type { SidebarLayout, SidebarOrderEntry } from "@/types/database";
-import { parseDataGripConnections, parseDataGripImport, type DataGripImportPayload } from "@/lib/imports/datagripImport";
+import { matchDataGripImportFiles, parseDataGripConnections, parseDataGripImport, type DataGripImportPayload } from "@/lib/imports/datagripImport";
function payload(dataSources: string, dataSourcesLocal?: string, dbForestConfig?: string): DataGripImportPayload {
return { format: "datagrip-import", dataSources, dataSourcesLocal, dbForestConfig };
@@ -65,6 +65,59 @@ describe("DataGrip connection import", () => {
});
});
+ it("imports Kingbase data sources configured by URL without a driver-ref", () => {
+ // DataGrip has no built-in Kingbase driver, so every Kingbase connection is
+ // a custom driver. Real exports use true
+ // with NO element — the driver identity lives only in
+ // + . Such connections must still import.
+ const connections = parseDataGripConnections(
+ payload(`
+
+
+
+ true
+ com.kingbase8.Driver
+ jdbc:kingbase8://192.0.2.1:54321/app
+
+
+
+ `),
+ );
+
+ expect(connections).toHaveLength(1);
+ expect(connections[0]).toMatchObject({
+ name: "Kingbase Dev",
+ db_type: "kingbase",
+ driver_profile: "kingbase",
+ driver_label: "KingbaseES",
+ host: "192.0.2.1",
+ port: 54321,
+ database: "app",
+ username: "SYSTEM",
+ });
+ });
+
+ it("drops unknown custom drivers configured by URL", () => {
+ // No + an unrecognised driver class/subprotocol must NOT leak
+ // in as a generic JDBC connection. It stays dropped. This guards the
+ // mergeFragments "" sentinel contract (see parseDataGripImport guard).
+ const connections = parseDataGripConnections(
+ payload(`
+
+
+
+ true
+ com.example.mystery.Driver
+ jdbc:mystery://10.0.0.1:9999/db
+
+
+
+ `),
+ );
+
+ expect(connections).toHaveLength(0);
+ });
+
it("preserves DataGrip connection groups as sidebar groups", () => {
const result = parseDataGripImport(
payload(
@@ -136,6 +189,34 @@ describe("DataGrip connection import", () => {
expect(layoutLabels(result.layout!, names)).toEqual([{ group: "Legacy Group", children: ["Legacy"] }]);
});
+ it("preserves the modern DataGrip group attribute", () => {
+ // Real DataGrip exports use a `group` attribute on (not the
+ // legacy `group-name`). Connections sharing a group land under one folder.
+ const result = parseDataGripImport(
+ payload(`
+
+
+
+ mysql
+ jdbc:mysql://prod.example.com:3306/app
+
+
+ mysql
+ jdbc:mysql://dev.example.com:3306/app
+
+
+ mysql
+ jdbc:mysql://localhost:3306/app
+
+
+
+ `),
+ );
+
+ const names = new Map(result.connections.map((connection) => [connection.id, connection.name]));
+ expect(layoutLabels(result.layout!, names)).toEqual([{ group: "production", children: ["Prod"] }, { group: "development", children: ["Dev"] }, "Lonely"]);
+ });
+
it("keeps the connection-only API and empty layout behavior", () => {
const importPayload = payload(`
@@ -152,3 +233,45 @@ describe("DataGrip connection import", () => {
expect(parseDataGripImport(importPayload).layout).toBeUndefined();
});
});
+
+describe("matchDataGripImportFiles", () => {
+ it("picks the three DataGrip config files by name regardless of order", () => {
+ const paths = ["C:/proj/.idea/db-forest-config.xml", "C:/proj/.idea/dataSources.local.xml", "C:/proj/.idea/dataSources.xml"];
+ expect(matchDataGripImportFiles(paths)).toEqual({
+ dataSources: "C:/proj/.idea/dataSources.xml",
+ local: "C:/proj/.idea/dataSources.local.xml",
+ forest: "C:/proj/.idea/db-forest-config.xml",
+ });
+ });
+
+ it("allows missing optional local and forest files", () => {
+ expect(matchDataGripImportFiles(["C:/proj/.idea/dataSources.xml"])).toEqual({
+ dataSources: "C:/proj/.idea/dataSources.xml",
+ local: undefined,
+ forest: undefined,
+ });
+ });
+
+ it("throws a coded error when dataSources.xml is not among the selected files", () => {
+ let caught: unknown;
+ try {
+ matchDataGripImportFiles(["C:/proj/other.xml", "C:/proj/dataSources.local.xml"]);
+ } catch (error) {
+ caught = error;
+ }
+ expect((caught as Error).message).toMatch(/dataSources\.xml/i);
+ expect((caught as Error & { code?: string }).code).toBe("DATAGRIP_IMPORT_MISSING_DATASOURCES");
+ });
+
+ it("matches file names case-insensitively", () => {
+ const result = matchDataGripImportFiles(["C:/proj/.idea/DataSources.XML", "C:/proj/.idea/datasources.local.xml"]);
+ expect(result.dataSources).toBe("C:/proj/.idea/DataSources.XML");
+ expect(result.local).toBe("C:/proj/.idea/datasources.local.xml");
+ });
+
+ it("handles Windows backslash paths", () => {
+ const result = matchDataGripImportFiles(["C:\\proj\\.idea\\dataSources.xml", "C:\\proj\\.idea\\dataSources.local.xml"]);
+ expect(result.dataSources).toBe("C:\\proj\\.idea\\dataSources.xml");
+ expect(result.local).toBe("C:\\proj\\.idea\\dataSources.local.xml");
+ });
+});
diff --git a/apps/desktop/src/lib/imports/datagripImport.ts b/apps/desktop/src/lib/imports/datagripImport.ts
index 22cbb8af9..9972a05db 100644
--- a/apps/desktop/src/lib/imports/datagripImport.ts
+++ b/apps/desktop/src/lib/imports/datagripImport.ts
@@ -300,7 +300,9 @@ function parseDataSourcesXml(xml: string): Map. `group-name` is kept as a legacy fallback for older exports.
+ const groupName = element.getAttribute("group") || element.getAttribute("group-name") || undefined;
if (groupName) fragment.groupName = groupName;
result.set(uuidVal, fragment);
@@ -419,11 +421,11 @@ function mergeFragments(shared: Map>, local:
// Resolve and filter
const resolved: DataSourceFragment[] = [];
for (const frag of merged.values()) {
- if (!frag.uuid || !frag.driverRef || !frag.jdbcUrl) continue;
+ if (!frag.uuid || !frag.jdbcUrl) continue;
resolved.push({
uuid: frag.uuid,
name: frag.name || frag.uuid,
- driverRef: frag.driverRef,
+ driverRef: frag.driverRef || "",
jdbcUrl: frag.jdbcUrl,
driverClass: frag.driverClass || "",
username: frag.username || "",
@@ -494,6 +496,13 @@ export function parseDataGripImport(payload: DataGripImportPayload): DataGripImp
for (const fragment of fragments) {
const config = buildConnection(fragment);
+ // Custom-driver sources without a are kept only when a concrete
+ // database type is recognised. DataGrip ships no built-in Kingbase driver, so
+ // every Kingbase connection is custom (configured by URL, no driver-ref).
+ // An unrecognised custom driver falls back to "jdbc" and is dropped here to
+ // avoid importing unusable half-baked JDBC connections. `fragment.driverRef
+ // === ""` is the sentinel mergeFragments sets when was absent.
+ if (fragment.driverRef === "" && config.db_type === "jdbc") continue;
const key = [config.name, config.db_type, config.host, config.port, config.database || ""].join("\u0000");
if (seen.has(key)) continue;
seen.add(key);
@@ -516,6 +525,34 @@ export function parseDataGripConnections(payload: DataGripImportPayload): Connec
return parseDataGripImport(payload).connections;
}
+/**
+ * Pick DataGrip config file paths by name from a dialog multi-select list.
+ * `dataSources.xml` is required; `dataSources.local.xml` (usernames) and
+ * `db-forest-config.xml` (legacy group tree) are optional and stay undefined
+ * when not selected. Names are matched case-insensitively on both path styles.
+ */
+export function matchDataGripImportFiles(paths: string[]): {
+ dataSources: string;
+ local?: string;
+ forest?: string;
+} {
+ const fileName = (path: string) => (path.split(/[\\/]/).pop() ?? "").toLowerCase();
+ const find = (name: string) => paths.find((path) => fileName(path) === name.toLowerCase());
+ const dataSources = find("dataSources.xml");
+ if (!dataSources) {
+ // Library layer keeps a readable fallback message; UI layers translate the
+ // coded error (see readDataGripImportFile) instead of showing this raw text.
+ const error = new Error("Select dataSources.xml (required); optionally dataSources.local.xml and db-forest-config.xml");
+ (error as Error & { code?: string }).code = "DATAGRIP_IMPORT_MISSING_DATASOURCES";
+ throw error;
+ }
+ return {
+ dataSources,
+ local: find("dataSources.local.xml"),
+ forest: find("db-forest-config.xml"),
+ };
+}
+
/** Returns a map of dedup key (name\0host\0port\0db) → DataGrip UUID for Keychain lookup. */
export function getDataGripUuidMap(payload: DataGripImportPayload): Map {
const shared = parseDataSourcesXml(payload.dataSources);
@@ -527,6 +564,9 @@ export function getDataGripUuidMap(payload: DataGripImportPayload): Map) that don't resolve to a concrete database type.
+ if (fragment.driverRef === "" && profile.dbType === "jdbc") continue;
const parsed = parseJdbcUrl(fragment.jdbcUrl);
const host = parsed.host || (profile.dbType === "sqlite" ? "" : "127.0.0.1");
const port = parsed.port || profile.port;
diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts
index 04b15b3f9..95f8b522d 100644
--- a/apps/desktop/src/stores/connectionStore.ts
+++ b/apps/desktop/src/stores/connectionStore.ts
@@ -6332,23 +6332,35 @@ export const useConnectionStore = defineStore("connection", () => {
if (isTauriRuntime()) {
const { open } = await import("@tauri-apps/plugin-dialog");
const { readTextFile } = await import("@tauri-apps/plugin-fs");
- const path = await open({
- filters: [{ name: "DataGrip dataSources.xml", extensions: ["xml"] }],
- multiple: false,
+ const { matchDataGripImportFiles } = await import("@/lib/imports/datagripImport");
+ const paths = await open({
+ multiple: true,
+ filters: [{ name: "DataGrip configuration files", extensions: ["xml"] }],
+ title: i18n.global.t("configExport.importDatagripDialogTitle"),
});
- if (!path) return null;
- dataSources = await readTextFile(path as string);
- // Auto-load dataSources.local.xml from the same directory
- const dir = (path as string).replace(/[^/\\]*$/, "");
+ if (!paths || paths.length === 0) return null;
+ // Tauri's fs scope authorizes only the exact paths picked in the dialog,
+ // so every file read below must be explicitly selected — sibling files in
+ // the same directory (e.g. dataSources.local.xml) are NOT readable.
+ let picked: { dataSources: string; local?: string; forest?: string };
try {
- dataSourcesLocal = await readTextFile(dir + "dataSources.local.xml");
- } catch {
- dataSourcesLocal = "";
+ picked = matchDataGripImportFiles(Array.isArray(paths) ? paths : [paths]);
+ } catch (error) {
+ if ((error as Error & { code?: string })?.code === "DATAGRIP_IMPORT_MISSING_DATASOURCES") {
+ throw new Error(i18n.global.t("configExport.importDatagripSelectFiles"));
+ }
+ throw error;
}
- try {
- dbForestConfig = await readTextFile(dir + "db-forest-config.xml");
- } catch {
- dbForestConfig = "";
+ dataSources = await readTextFile(picked.dataSources);
+ if (picked.local) {
+ dataSourcesLocal = await readTextFile(picked.local);
+ } else {
+ console.warn("[DataGrip Import] dataSources.local.xml not selected; usernames will fall back to defaults");
+ }
+ if (picked.forest) {
+ dbForestConfig = await readTextFile(picked.forest);
+ } else {
+ console.warn("[DataGrip Import] db-forest-config.xml not selected; legacy group tree skipped");
}
} else {
const files = await new Promise((resolve, reject) => {
@@ -6366,7 +6378,7 @@ export const useConnectionStore = defineStore("connection", () => {
input.click();
});
const fileList = Array.from(files);
- const dsFile = fileList.find((f) => /^dataSources\.xml$/i.test(f.name)) || fileList[0];
+ const dsFile = fileList.find((f) => /^dataSources\.xml$/i.test(f.name));
const localFile = fileList.find((f) => /^dataSources\.local\.xml$/i.test(f.name));
const forestFile = fileList.find((f) => /^db-forest-config\.xml$/i.test(f.name));
if (!dsFile) throw new Error("Select dataSources.xml");