From babd8c10cb3d9193b2694b98b50b10af961ed022 Mon Sep 17 00:00:00 2001 From: wzl <1152831376@qq.com> Date: Sat, 1 Aug 2026 09:25:31 +0800 Subject: [PATCH 1/2] fix(desktop): stabilize legacy WebView compatibility --- .../public/connection-dialog-legacy.css | 35 +- .../ScheduledDatabaseBackupSettings.vue | 4 +- .../ScheduledDatabaseBackupSettings.spec.ts | 115 ++++ .../components/config/DriverStoreDialog.vue | 82 ++- .../connection/ConnectionDialog.vue | 31 +- .../connection/TunnelProfileManager.vue | 19 +- .../editor/EditorSettingsDialog.vue | 171 +++++- .../components/settings/ChangelogPanel.vue | 4 +- .../desktop/src/components/ui/input/Input.vue | 169 +++++- .../ui/input/__tests__/Input.spec.ts | 79 +++ 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 + .../__tests__/legacyWebviewFallback.spec.ts | 148 +++++ apps/desktop/src/styles/globals.css | 72 +++ docs/local-development-guide.zh-CN.md | 511 ++++++++++++++++++ 21 files changed, 1397 insertions(+), 51 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 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..a4e868f83d 100644 --- a/apps/desktop/public/connection-dialog-legacy.css +++ b/apps/desktop/public/connection-dialog-legacy.css @@ -68,8 +68,37 @@ } } -@media (min-width: 1024px) { - .connection-db-picker-grid { - grid-template-columns: repeat(5, minmax(0, 1fr)) !important; +@supports (color: oklch(0.5 0.1 180)) { + @media (min-width: 1024px) { + .connection-db-picker-grid { + grid-template-columns: repeat(5, minmax(0, 1fr)) !important; + } + } +} + +@supports not (color: oklch(0.5 0.1 180)) { + @media (min-width: 640px) { + .connection-db-picker-grid { + grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)) !important; + } + + [data-slot="dialog-content"].connection-dialog-content--config { + width: calc(100vw - 2rem) !important; + min-width: 38rem !important; + height: 720px !important; + max-width: 880px !important; + } + + [data-slot="dialog-content"].connection-dialog-content--config .connection-form-body { + align-content: start !important; + } + + [data-slot="dialog-content"].connection-dialog-content--config .connection-url-params-row--compact { + align-items: center !important; + } + + [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 5a7970cab6..c8debf3f05 100644 --- a/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue +++ b/apps/desktop/src/components/backup/ScheduledDatabaseBackupSettings.vue @@ -37,6 +37,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") }, @@ -281,8 +282,9 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {

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

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

+

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

- 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 d6e794fa19..3e38248530 100644 --- a/apps/desktop/src/components/config/DriverStoreDialog.vue +++ b/apps/desktop/src/components/config/DriverStoreDialog.vue @@ -1532,7 +1532,7 @@ watch(driverStoreTab, (tab) => { -
+
{{ t("common.loading") }} @@ -2139,6 +2139,86 @@ watch(driverStoreTab, (tab) => { height: 2rem !important; } +@supports not (color: oklch(0.5 0.1 180)) { + .driver-store-tab { + margin-top: 1.25rem !important; + } + + .driver-store-agent-tab:not([hidden]), + .driver-store-jdbc-tab:not([hidden]), + .driver-store-storage-tab:not([hidden]) { + display: flex !important; + flex-direction: column !important; + gap: 1.25rem !important; + } + + .driver-store-agent-tab > :not([hidden]) ~ :not([hidden]), + .driver-store-jdbc-tab > :not([hidden]) ~ :not([hidden]), + .driver-store-storage-tab > :not([hidden]) ~ :not([hidden]) { + margin-top: 0 !important; + } + + .driver-store-tab .space-y-1 > * + * { + margin-top: 0.25rem !important; + } + + .driver-store-tab .space-y-2 > * + * { + margin-top: 0.5rem !important; + } + + .driver-store-tab .space-y-2\.5 > * + * { + margin-top: 0.625rem !important; + } + + .driver-store-tab .space-y-3 > * + * { + margin-top: 0.75rem !important; + } + + .driver-store-tab .space-y-4 > * + * { + margin-top: 1rem !important; + } + + .driver-store-tab .space-y-5 > * + * { + margin-top: 1.25rem !important; + } + + @media (min-width: 640px) { + [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; + } + + [data-driver-category-nav] > button { + width: 100% !important; + align-self: stretch !important; + } + + [data-driver-category-nav] > button[aria-current="page"] { + background-color: rgba(23, 23, 23, 0.08) !important; + color: rgb(23, 23, 23) !important; + } + + .dark [data-driver-category-nav] > button[aria-current="page"] { + background-color: rgba(255, 255, 255, 0.1) !important; + color: rgb(244, 244, 245) !important; + } + + .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 f577b79a4e..8ded9e519f 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -2555,6 +2555,7 @@ const tlsCapableDatabaseTypes = new Set(["mysql", "starrocks", "po const supportsTlsToggle = computed(() => tlsCapableDatabaseTypes.has(form.value.db_type)); const supportsCaCertificatePath = computed(() => form.value.db_type === "clickhouse"); 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)); @@ -6132,8 +6133,8 @@ function openExternalUrl(url: string) {
-
- +
+
-

+

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

@@ -6768,8 +6769,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 @@ -7295,6 +7296,16 @@ function openExternalUrl(url: string) { background-color: rgba(23, 23, 23, 0.12) !important; } + .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; + } + + .connection-transport-layer-option--selected:hover { + 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; @@ -7304,6 +7315,16 @@ function openExternalUrl(url: string) { color: rgb(244, 244, 245) !important; background-color: rgba(255, 255, 255, 0.14) !important; } + + .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; + } + + .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 528ab0dc8e..d423c5dbc1 100644 --- a/apps/desktop/src/components/connection/TunnelProfileManager.vue +++ b/apps/desktop/src/components/connection/TunnelProfileManager.vue @@ -190,7 +190,7 @@ 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 506e95e96c..1cb1a10a86 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -3475,12 +3475,16 @@ onUnmounted(cleanupPreviewEditor); {{ t("settings.sqlFormatterEditorShortcuts") }}
-
-
+
+
-
-
+
+
@@ -3513,7 +3517,7 @@ onUnmounted(cleanupPreviewEditor); 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)" > @@ -3524,7 +3528,7 @@ onUnmounted(cleanupPreviewEditor); 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)" > @@ -4458,7 +4462,7 @@ onUnmounted(cleanupPreviewEditor);
- +