From 7a19c9d770159cc1bfcfc0a46b4234e161787c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=A5=E9=A3=9F=E5=88=86=E5=AD=90?= <54768887+wzlstudy@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:22:16 +0800 Subject: [PATCH] fix(desktop): improve legacy WebView style compatibility --- .../public/connection-dialog-legacy.css | 25 + .../ScheduledDatabaseBackupSettings.vue | 6 +- .../ScheduledDatabaseBackupSettings.spec.ts | 115 ++ .../components/config/DriverStoreDialog.vue | 73 +- .../connection/ConnectionDialog.vue | 63 +- .../connection/TunnelProfileManager.vue | 26 +- .../editor/EditorSettingsDialog.vue | 151 ++- .../components/settings/ChangelogPanel.vue | 4 +- .../SidebarTreeItemDialogs.mongoIndex.spec.ts | 2 + .../components/ui/dialog/DialogContent.vue | 2 +- .../ui/dialog/DialogScrollContent.vue | 2 +- .../ui/input/__tests__/Input.spec.ts | 43 + .../src/components/ui/switch/Switch.vue | 24 + apps/desktop/src/i18n/locales/en.ts | 1 + apps/desktop/src/i18n/locales/es.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/zh-CN.ts | 1 + apps/desktop/src/i18n/locales/zh-TW.ts | 1 + .../lib/ui/__tests__/legacyWebView.spec.ts | 69 + apps/desktop/src/lib/ui/legacyWebView.ts | 41 + apps/desktop/src/main.ts | 2 + .../__tests__/legacyWebviewFallback.spec.ts | 242 +++- apps/desktop/src/styles/globals.css | 1169 +++++++++++------ apps/desktop/src/styles/tokens.css | 2 + crates/dbx-core/assets/docs-export.css | 2 +- .../dbx-core/assets/docs-export.manifest.json | 2 +- docs/local-development-guide.zh-CN.md | 188 +++ 30 files changed, 1802 insertions(+), 459 deletions(-) create mode 100644 apps/desktop/src/components/backup/__tests__/ScheduledDatabaseBackupSettings.spec.ts create mode 100644 apps/desktop/src/components/ui/input/__tests__/Input.spec.ts create mode 100644 apps/desktop/src/lib/ui/__tests__/legacyWebView.spec.ts create mode 100644 apps/desktop/src/lib/ui/legacyWebView.ts create mode 100644 docs/local-development-guide.zh-CN.md diff --git a/apps/desktop/public/connection-dialog-legacy.css b/apps/desktop/public/connection-dialog-legacy.css index 215beda9e9..0af2f5d488 100644 --- a/apps/desktop/public/connection-dialog-legacy.css +++ b/apps/desktop/public/connection-dialog-legacy.css @@ -73,3 +73,28 @@ grid-template-columns: repeat(5, minmax(0, 1fr)) !important; } } + +@media (min-width: 640px) { + html.dbx-legacy-webview .connection-db-picker-grid { + grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)) !important; + } + + html.dbx-legacy-webview [data-slot="dialog-content"].connection-dialog-content--config { + width: calc(100vw - 2rem) !important; + min-width: 38rem !important; + height: 720px !important; + max-width: 880px !important; + } + + html.dbx-legacy-webview [data-slot="dialog-content"].connection-dialog-content--config .connection-form-body { + align-content: start !important; + } + + html.dbx-legacy-webview [data-slot="dialog-content"].connection-dialog-content--config .connection-url-params-row--compact { + align-items: center !important; + } + + html.dbx-legacy-webview [data-slot="dialog-content"].connection-dialog-content--config .connection-url-params-row--with-hint .connection-url-params-label { + margin-top: 0.5rem !important; + } +} diff --git a/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue b/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue index 9c4d548749..3295ecbccd 100644 --- a/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue +++ b/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue @@ -41,6 +41,7 @@ const tablePatternsInput = ref(""); const expandedRunIds = reactive(new Set()); const sqlConnections = computed(() => connectionStore.connections.filter((connection) => supportsScheduledDatabaseBackup(connection.db_type))); +const canCreateSchedule = computed(() => sqlConnections.value.length > 0); const sortedRuns = computed(() => [...runs.value].sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt))); const weekdays = computed(() => [ { value: 0, label: t("databaseBackup.weekdays.sunday") }, @@ -302,8 +303,9 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {

{{ t("databaseBackup.schedules") }}

{{ t("databaseBackup.runtimeRequirement") }}

+

{{ t("databaseBackup.noSupportedConnections") }}

- @@ -412,7 +414,7 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) { - + {{ editingScheduleId ? t("databaseBackup.editSchedule") : t("databaseBackup.addSchedule") }} diff --git a/apps/desktop/src/components/backup/__tests__/ScheduledDatabaseBackupSettings.spec.ts b/apps/desktop/src/components/backup/__tests__/ScheduledDatabaseBackupSettings.spec.ts new file mode 100644 index 0000000000..da37dbd9c7 --- /dev/null +++ b/apps/desktop/src/components/backup/__tests__/ScheduledDatabaseBackupSettings.spec.ts @@ -0,0 +1,115 @@ +// @vitest-environment happy-dom + +import { createApp, nextTick, type App } from "vue"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import i18n from "@/i18n"; +import ScheduledDatabaseBackupSettings from "@/components/backup/ScheduledDatabaseBackupSettings.vue"; + +const mocks = vi.hoisted(() => ({ + connections: [] as Array<{ id: string; name: string; db_type: string }>, + ensureConnected: vi.fn(async () => {}), + listDatabases: vi.fn(async () => [{ name: "app" }]), + toast: vi.fn(), + saveSchedule: vi.fn(), + setScheduleEnabled: vi.fn(), + deleteSchedule: vi.fn(), + deleteRun: vi.fn(), + runSchedule: vi.fn(), + cancelRun: vi.fn(), +})); + +vi.mock("@/stores/connectionStore", () => ({ + useConnectionStore: () => ({ + connections: mocks.connections, + ensureConnected: mocks.ensureConnected, + getConfig: (connectionId: string) => mocks.connections.find((connection) => connection.id === connectionId), + sqlFileSource: null, + }), +})); + +vi.mock("@/composables/useScheduledDatabaseBackups", () => ({ + useScheduledDatabaseBackups: () => ({ + schedules: { __v_isRef: true, value: [] }, + runs: { __v_isRef: true, value: [] }, + activeScheduleIds: new Set(), + activeRunIds: new Set(), + activeRuns: { __v_isRef: true, value: [] }, + saveSchedule: mocks.saveSchedule, + setScheduleEnabled: mocks.setScheduleEnabled, + deleteSchedule: mocks.deleteSchedule, + deleteRun: mocks.deleteRun, + runSchedule: mocks.runSchedule, + cancelRun: mocks.cancelRun, + }), +})); + +vi.mock("@/composables/useToast", () => ({ + useToast: () => ({ toast: mocks.toast }), +})); + +vi.mock("@/lib/backend/api", () => ({ + listDatabases: mocks.listDatabases, + deleteDatabaseBackupFiles: vi.fn(), + revealPathInFileManager: vi.fn(), +})); + +const mountedApps: App[] = []; + +async function flush() { + await nextTick(); + await new Promise((resolve) => setTimeout(resolve, 0)); + await nextTick(); +} + +async function mountSettings() { + const container = document.createElement("div"); + document.body.append(container); + const app = createApp(ScheduledDatabaseBackupSettings); + mountedApps.push(app); + app.use(i18n); + app.mount(container); + await flush(); +} + +function addScheduleButton(): HTMLButtonElement { + const label = String(i18n.global.t("databaseBackup.addSchedule")); + const button = Array.from(document.body.querySelectorAll("button")).find((item) => item.textContent?.includes(label)); + if (!button) throw new Error("Add schedule button not found"); + return button; +} + +afterEach(() => { + for (const app of mountedApps.splice(0)) app.unmount(); + document.body.innerHTML = ""; + mocks.connections.splice(0); + mocks.ensureConnected.mockClear(); + mocks.listDatabases.mockClear(); + mocks.toast.mockClear(); +}); + +describe("ScheduledDatabaseBackupSettings schedule dialog", () => { + it("opens the create schedule dialog for supported SQL connections", async () => { + mocks.connections.push({ id: "mysql-1", name: "Local MySQL", db_type: "mysql" }); + await mountSettings(); + + const button = addScheduleButton(); + expect(button.disabled).toBe(false); + + button.click(); + await flush(); + + const dialog = document.body.querySelector('[data-slot="dialog-content"]'); + expect(dialog?.textContent).toContain(String(i18n.global.t("databaseBackup.addSchedule"))); + expect(dialog?.textContent).toContain(String(i18n.global.t("databaseBackup.scheduleName"))); + expect(mocks.ensureConnected).toHaveBeenCalledWith("mysql-1"); + expect(mocks.listDatabases).toHaveBeenCalledWith("mysql-1"); + }); + + it("disables create schedule when there are no supported backup connections", async () => { + mocks.connections.push({ id: "oracle-1", name: "Oracle", db_type: "oracle" }); + await mountSettings(); + + expect(addScheduleButton().disabled).toBe(true); + expect(document.body.textContent).toContain(String(i18n.global.t("databaseBackup.noSupportedConnections"))); + }); +}); diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue index 6ce5354f80..e0270c260e 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -1476,7 +1476,7 @@ watch(driverStoreTab, (tab) => { v-if="!driver.installed && !isManagedJdbcBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)" size="sm" variant="ghost" - class="h-7 w-7 rounded-md text-xs text-muted-foreground" + class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground" :title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')" :disabled="upgradingAll || installing !== null || agentImportBusy" @click="importDriverFile(driver)" @@ -1533,7 +1533,7 @@ watch(driverStoreTab, (tab) => { -
+
{{ t("common.loading") }} @@ -1594,7 +1594,7 @@ watch(driverStoreTab, (tab) => { v-if="!driver.installed && !isManagedJdbcBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)" size="sm" variant="ghost" - class="h-7 w-7 rounded-md text-xs text-muted-foreground" + class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground" :title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')" :disabled="upgradingAll || installing !== null || agentImportBusy" @click="importDriverFile(driver)" @@ -1678,7 +1678,7 @@ watch(driverStoreTab, (tab) => { v-if="!driver.installed && !isManagedJdbcBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)" size="sm" variant="ghost" - class="h-7 w-7 rounded-md text-xs text-muted-foreground" + class="driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground" :title="importingDriver === driver.db_type ? t('driverStore.importing') : t('driverStore.importLocalJar')" :disabled="upgradingAll || installing !== null || agentImportBusy" @click="importDriverFile(driver)" @@ -2140,6 +2140,71 @@ watch(driverStoreTab, (tab) => { height: 2rem !important; } +html.dbx-legacy-webview .driver-store-tab { + margin-top: 1.25rem !important; +} + +html.dbx-legacy-webview .driver-store-agent-tab:not([hidden]), +html.dbx-legacy-webview .driver-store-jdbc-tab:not([hidden]), +html.dbx-legacy-webview .driver-store-storage-tab:not([hidden]) { + display: flex !important; + flex-direction: column !important; + gap: 1.25rem !important; +} + +html.dbx-legacy-webview .driver-store-agent-tab > :not([hidden]) ~ :not([hidden]), +html.dbx-legacy-webview .driver-store-jdbc-tab > :not([hidden]) ~ :not([hidden]), +html.dbx-legacy-webview .driver-store-storage-tab > :not([hidden]) ~ :not([hidden]) { + margin-top: 0 !important; +} + +html.dbx-legacy-webview .driver-store-local-import-button { + width: 2rem !important; + height: 2rem !important; + padding: 0 !important; +} + +html.dbx-legacy-webview .driver-store-local-import-button svg { + width: 1rem !important; + height: 1rem !important; +} + +@media (min-width: 640px) { + html.dbx-legacy-webview [data-driver-category-nav] { + width: 10rem !important; + flex-direction: column !important; + overflow-x: hidden !important; + overflow-y: auto !important; + border-right-width: 1px !important; + border-bottom-width: 0 !important; + padding-top: 0.125rem !important; + padding-right: 0.875rem !important; + padding-bottom: 0.125rem !important; + } + + html.dbx-legacy-webview [data-driver-category-nav] > button { + width: 100% !important; + align-self: stretch !important; + } + + html.dbx-legacy-webview [data-driver-category-nav] > button[aria-current="page"] { + background-color: rgba(23, 23, 23, 0.08) !important; + color: rgb(23, 23, 23) !important; + } + + html.dbx-legacy-webview.dark [data-driver-category-nav] > button[aria-current="page"] { + background-color: rgba(255, 255, 255, 0.1) !important; + color: rgb(244, 244, 245) !important; + } + + html.dbx-legacy-webview .driver-store-agent-results { + width: 0 !important; + min-width: 0 !important; + flex: 1 1 0% !important; + padding-left: 1rem !important; + } +} + @media (max-width: 900px) { .driver-store-header { align-items: flex-start !important; diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index a0543f45f2..039afb11de 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -3034,6 +3034,7 @@ const tlsCapableDatabaseTypes = new Set([ const supportsTlsToggle = computed(() => tlsCapableDatabaseTypes.has(form.value.db_type)); const supportsCaCertificatePath = computed(() => form.value.db_type === "clickhouse" || form.value.db_type === "victoriametrics"); const supportsGenericUrlParams = computed(() => form.value.db_type !== "manticoresearch" && form.value.db_type !== "hbase"); +const showGenericUrlParamsHint = computed(() => form.value.db_type === "mysql" || form.value.db_type === "doris" || form.value.db_type === "starrocks"); const bareMysqlProfiles = new Set(["doris", "selectdb", "oceanbase"]); const supportsMysqlTlsOptions = computed(() => form.value.db_type === "starrocks" || (form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value))); const supportsMysqlCleartextPasswordAuth = computed(() => form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value)); @@ -7021,8 +7022,8 @@ function openExternalUrl(url: string) {
-
- +
+
-

+

{{ t("connection.localInfilePathHint") }}

@@ -7791,8 +7792,8 @@ function openExternalUrl(url: string) { :key="hop.id" type="button" draggable="true" - class="flex min-h-10 items-center gap-2 rounded-md border px-2 text-left text-xs transition-colors" - :class="hop.id === selectedTransportLayer?.id ? 'border-primary bg-primary/5' : 'hover:bg-muted/50'" + class="connection-transport-layer-option flex min-h-10 items-center gap-2 rounded-md border px-2 text-left text-xs transition-colors" + :class="hop.id === selectedTransportLayer?.id ? 'connection-transport-layer-option--selected border-primary bg-primary/5' : 'hover:bg-muted/50'" @click="selectedTransportLayerId = hop.id" @dragstart="draggedTransportLayerId = hop.id" @dragover.prevent @@ -8376,26 +8377,44 @@ function openExternalUrl(url: string) { /* Legacy responsive layout rules live in public/connection-dialog-legacy.css * so the production build cannot rewrite their classic media queries. */ -@supports not (color: oklch(0.5 0.1 180)) { - .connection-db-category-option--selected { - color: rgb(23, 23, 23) !important; - background-color: rgba(23, 23, 23, 0.08) !important; - } +html.dbx-legacy-webview .connection-db-category-option--selected { + color: rgb(23, 23, 23) !important; + background-color: rgba(23, 23, 23, 0.08) !important; +} - .connection-db-category-option--selected:hover { - color: rgb(23, 23, 23) !important; - background-color: rgba(23, 23, 23, 0.12) !important; - } +html.dbx-legacy-webview .connection-db-category-option--selected:hover { + color: rgb(23, 23, 23) !important; + background-color: rgba(23, 23, 23, 0.12) !important; +} - .dark .connection-db-category-option--selected { - color: rgb(244, 244, 245) !important; - background-color: rgba(255, 255, 255, 0.1) !important; - } +html.dbx-legacy-webview .connection-transport-layer-option--selected { + color: rgb(23, 23, 23) !important; + border-color: rgb(23, 23, 23) !important; + background-color: rgba(23, 23, 23, 0.08) !important; +} - .dark .connection-db-category-option--selected:hover { - color: rgb(244, 244, 245) !important; - background-color: rgba(255, 255, 255, 0.14) !important; - } +html.dbx-legacy-webview .connection-transport-layer-option--selected:hover { + background-color: rgba(23, 23, 23, 0.12) !important; +} + +html.dbx-legacy-webview.dark .connection-db-category-option--selected { + color: rgb(244, 244, 245) !important; + background-color: rgba(255, 255, 255, 0.1) !important; +} + +html.dbx-legacy-webview.dark .connection-db-category-option--selected:hover { + color: rgb(244, 244, 245) !important; + background-color: rgba(255, 255, 255, 0.14) !important; +} + +html.dbx-legacy-webview.dark .connection-transport-layer-option--selected { + color: rgb(244, 244, 245) !important; + border-color: rgb(244, 244, 245) !important; + background-color: rgba(255, 255, 255, 0.1) !important; +} + +html.dbx-legacy-webview.dark .connection-transport-layer-option--selected:hover { + background-color: rgba(255, 255, 255, 0.14) !important; } .connection-db-picker-option { diff --git a/apps/desktop/src/components/connection/TunnelProfileManager.vue b/apps/desktop/src/components/connection/TunnelProfileManager.vue index 4dcccb476a..873ff1b8a6 100644 --- a/apps/desktop/src/components/connection/TunnelProfileManager.vue +++ b/apps/desktop/src/components/connection/TunnelProfileManager.vue @@ -190,7 +190,14 @@ async function testSelected() {

{{ t("settings.tunnelsEmpty") }}

-
+ + diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 374b095a36..66d05e075e 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -3901,12 +3901,16 @@ onUnmounted(() => { {{ t("settings.sqlFormatterEditorShortcuts") }}
-
-
+
+
-
-
+
+
{ type="button" variant="ghost" size="icon" - class="h-7 w-7 shrink-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100" + class="settings-shortcut-action-button h-7 w-7 shrink-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100" :aria-label="t('settings.shortcutPressShortcut')" @click="focusShortcutInput(definition.id)" > @@ -3939,7 +3943,7 @@ onUnmounted(() => { type="button" variant="ghost" size="icon" - class="h-7 w-7 shrink-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100" + class="settings-shortcut-action-button h-7 w-7 shrink-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100" :aria-label="t('settings.reset')" @click="resetShortcut(definition.id)" > @@ -3950,7 +3954,7 @@ onUnmounted(() => { type="button" variant="ghost" size="icon" - class="h-7 w-7 shrink-0 text-muted-foreground opacity-0 transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100" + class="settings-shortcut-action-button h-7 w-7 shrink-0 text-muted-foreground opacity-0 transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100" :aria-label="t('settings.shortcutClear')" @click="clearShortcut(definition.id)" > @@ -4924,7 +4928,7 @@ onUnmounted(() => {
- +