diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 7e41fa5611..0fd72c0338 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -24,7 +24,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import * as api from "@/lib/api"; import { isTauriRuntime } from "@/lib/tauriRuntime"; import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connectionUrl"; -import type { ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink"; +import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink"; import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connectionPresentation"; import { h2ConnectionModeForConfig, h2FileJdbcUrl, h2FilePathFromJdbcUrl, type H2ConnectionMode } from "@/lib/h2Connection"; import { firstZooKeeperEndpoint, normalizeZooKeeperConnectString } from "@/lib/zookeeperConnection"; @@ -1464,6 +1464,12 @@ async function testConnection() { function applyConnectionUrlToForm(input: string): boolean { try { + const draft = parseConnectionDeepLink(input); + if (draft) { + applyConnectionDraftToForm({ ...draft, oneTime: undefined }); + return true; + } + const parsed = parseConnectionUrl(input, selectedType.value); form.value = applyParsedConnectionUrl(form.value, parsed); selectedType.value = parsed.driverProfile; @@ -2146,8 +2152,7 @@ function submitOneTimePrefill(draft: ConnectionDeepLinkDraft) { void nextTick(() => save()); } -function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) { - resetForm(); +function applyConnectionDraftToForm(draft: ConnectionDeepLinkDraft) { applyProfile(draft.driverProfile); form.value = { ...form.value, @@ -2183,6 +2188,11 @@ function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) { dialogStep.value = "config"; configTab.value = "connection"; resetTestState(); +} + +function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) { + resetForm(); + applyConnectionDraftToForm(draft); submitOneTimePrefill(draft); } diff --git a/apps/desktop/src/lib/connectionDeepLink.ts b/apps/desktop/src/lib/connectionDeepLink.ts index 89c5bc8440..9a6cba0d54 100644 --- a/apps/desktop/src/lib/connectionDeepLink.ts +++ b/apps/desktop/src/lib/connectionDeepLink.ts @@ -48,6 +48,7 @@ function optionalBooleanParam(params: URLSearchParams, ...keys: string[]): boole function draftFromConnectionUrl(value: string, preferredProfile?: string): ConnectionDeepLinkDraft { const parsed = parseConnectionUrl(value, preferredProfile); return { + name: parsed.name, dbType: parsed.dbType, driverProfile: parsed.driverProfile, driverLabel: parsed.driverLabel, diff --git a/apps/desktop/src/lib/connectionUrl.ts b/apps/desktop/src/lib/connectionUrl.ts index cc3a3eb69c..6e11e72131 100644 --- a/apps/desktop/src/lib/connectionUrl.ts +++ b/apps/desktop/src/lib/connectionUrl.ts @@ -1,6 +1,7 @@ import type { ConnectionConfig, DatabaseType } from "@/types/database"; export interface ParsedConnectionUrl { + name?: string; dbType: DatabaseType; driverProfile: string; driverLabel: string; @@ -172,6 +173,28 @@ function queryParamValue(params: string, key: string): string | undefined { return undefined; } +function connectionNameParam(parsed: URL): string | undefined { + for (const [key, value] of parsed.searchParams) { + if (key.toLowerCase() === "name") { + const name = value.trim(); + if (name) return name; + } + } + return undefined; +} + +function stripConnectionNameParam(params: string): string { + if (!params) return params; + return params + .split("&") + .filter((part) => { + if (!part) return true; + const [rawKey] = part.split("="); + return decodeUrlPart(rawKey).trim().toLowerCase() !== "name"; + }) + .join("&"); +} + function extractMysqlCredentialParams(params: string): { username?: string; password?: string; urlParams: string } { let username: string | undefined; let password: string | undefined; @@ -430,8 +453,10 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa } const urlParams = parsed.search.replace(/^\?/, ""); + const name = connectionNameParam(parsed); + const urlParamsWithoutName = stripConnectionNameParam(urlParams); const normalizedFragment = decodeUrlPart(parsed.hash.replace(/^#/, "")).trim().toLowerCase(); - const parsedUrlParams = profile.type === "redis" && normalizedFragment === "insecure" ? [urlParams, "insecure=true"].filter(Boolean).join("&") : urlParams; + const parsedUrlParams = profile.type === "redis" && normalizedFragment === "insecure" ? [urlParamsWithoutName, "insecure=true"].filter(Boolean).join("&") : urlParamsWithoutName; const mysqlCredentials = isJdbcUrl && profile.type === "mysql" ? extractMysqlCredentialParams(parsedUrlParams) : undefined; const effectiveUrlParams = mysqlCredentials?.urlParams ?? parsedUrlParams; if (profile.type === "mongodb") { @@ -452,6 +477,7 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa } if (profile.type === "zookeeper") { return { + ...(name ? { name } : {}), dbType: profile.type, driverProfile: profile.profile, driverLabel: profile.label, @@ -460,13 +486,14 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa username: decodeUrlPart(parsed.username), password: decodeUrlPart(parsed.password), database: undefined, - urlParams, + urlParams: urlParamsWithoutName, ssl: false, connectionString: zookeeperConnectStringFromUrl(parsed, profile.defaultPort), }; } return { + ...(name ? { name } : {}), dbType: profile.type, driverProfile: profile.profile, driverLabel: profile.label, @@ -496,6 +523,7 @@ export function applyParsedConnectionUrl(config: Omit, p driver_label: parsed.driverLabel, host: parsed.host, port: parsed.port, + name: parsed.name?.trim() || config.name, username: parsed.username, password: parsed.password, database: parsed.database, diff --git a/packages/app-tests/connectionDeepLink.test.ts b/packages/app-tests/connectionDeepLink.test.ts index 4814e58c37..522b34d82c 100644 --- a/packages/app-tests/connectionDeepLink.test.ts +++ b/packages/app-tests/connectionDeepLink.test.ts @@ -33,6 +33,23 @@ test("parses encoded database URL with password", () => { assert.equal(draft?.urlParams, "sslmode=require"); }); +test("uses the nested database URL name as connection name", () => { + const nested = encodeURIComponent("mysql://root:123456@localhost/?name=%E5%85%AC%E5%8F%B8+-+%E6%9C%AC%E5%9C%B0Docker&charset=utf8mb4"); + const draft = parseConnectionDeepLink(`dbx://connection/new?url=${nested}`); + + assert.equal(draft?.name, "公司 - 本地Docker"); + assert.equal(draft?.dbType, "mysql"); + assert.equal(draft?.host, "localhost"); + assert.equal(draft?.urlParams, "charset=utf8mb4"); +}); + +test("top-level deep link name overrides nested database URL name", () => { + const nested = encodeURIComponent("mysql://root@localhost/?name=Nested"); + const draft = parseConnectionDeepLink(`dbx://connection/new?url=${nested}&name=Top+Level`); + + assert.equal(draft?.name, "Top Level"); +}); + test("allows password query field to override database URL password", () => { const draft = parseConnectionDeepLink("dbx://connection/new?url=postgres%3A%2F%2Fapp%3Asecret%40db.internal%3A5432%2Forders&password=override"); diff --git a/packages/app-tests/connectionUrl.test.ts b/packages/app-tests/connectionUrl.test.ts index 1a4b9b3ee0..071fff8d65 100644 --- a/packages/app-tests/connectionUrl.test.ts +++ b/packages/app-tests/connectionUrl.test.ts @@ -45,6 +45,32 @@ test("parses mysql URLs with encoded credentials", () => { assert.equal(parsed.urlParams, "charset=utf8mb4"); }); +test("parses mysql URL name as decoded connection name", () => { + const parsed = parseConnectionUrl("mysql://root:123456@localhost/?name=%E5%85%AC%E5%8F%B8+-+%E6%9C%AC%E5%9C%B0Docker&charset=utf8mb4"); + + assert.equal(parsed.name, "公司 - 本地Docker"); + assert.equal(parsed.host, "localhost"); + assert.equal(parsed.username, "root"); + assert.equal(parsed.password, "123456"); + assert.equal(parsed.urlParams, "charset=utf8mb4"); +}); + +test("consumes mysql URL name when it is the only URL param", () => { + const parsed = parseConnectionUrl("mysql://root:123456@localhost/?name=%E5%85%AC%E5%8F%B8+-+%E6%9C%AC%E5%9C%B0Docker"); + + assert.equal(parsed.name, "公司 - 本地Docker"); + assert.equal(parsed.urlParams, ""); +}); + +test("removes only the connection name from URL params", () => { + const parsed = parseConnectionUrl("mysql://root@localhost/app?Name=Analytics+Local&ssl-mode=required"); + + assert.equal(parsed.name, "Analytics Local"); + assert.equal(parsed.database, "app"); + assert.equal(parsed.urlParams, "ssl-mode=required"); + assert.equal(parsed.ssl, true); +}); + test("parses mysql TLS URL params into the SSL switch state", () => { assert.equal(parseConnectionUrl("mysql://root@tidb.example.com:4000/test?ssl-mode=required").ssl, true); assert.equal(parseConnectionUrl("mysql://root@tidb.example.com:4000/test?require_ssl=true").ssl, true);