fix(desktop): support legacy macOS WebViews

This commit is contained in:
知食分子
2026-08-18 15:36:37 +08:00
committed by GitHub
parent 5acdbfa61e
commit 5196e92b24
8 changed files with 92 additions and 6 deletions
@@ -921,7 +921,7 @@ async function saveConfigTask() {
<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-[1120px] max-h-[80vh] flex flex-col overflow-hidden" @interact-outside.prevent>
<DialogContent class="dbx-transfer-dialog sm:max-w-[1120px] max-h-[80vh] flex flex-col overflow-hidden" @interact-outside.prevent>
<DialogHeader class="shrink-0">
<DialogTitle class="flex items-center gap-2">
<ArrowRightLeft class="w-4 h-4" />
@@ -1250,3 +1250,12 @@ async function saveConfigTask() {
</DialogContent>
</Dialog>
</template>
<style>
@media (min-width: 640px) {
html.dbx-legacy-webview [data-slot="dialog-content"].dbx-transfer-dialog[class~="max-w-sm"] {
width: calc(100vw - 2rem) !important;
max-width: 1120px !important;
}
}
</style>
@@ -1,7 +1,15 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { canFormatSqlForDatabaseType, formatSqlForDisplay, formatSqlForEditing, formatSqlText, MAX_SQL_FORMAT_CHARS, sqlFormatDialectForDbType, UnsupportedStructuredInputError } from "@/lib/sql/sqlFormatter";
const sqlFormatterSource = readFileSync(new URL("../../sql/sqlFormatter.ts", import.meta.url), "utf8");
describe("sqlFormatter", () => {
it("does not use lookbehind regular expressions in the startup path", () => {
expect(sqlFormatterSource).not.toContain("(?<!");
expect(sqlFormatterSource).not.toContain("(?<=");
});
it("disables SQL formatting for VictoriaMetrics queries", () => {
expect(canFormatSqlForDatabaseType("victoriametrics")).toBe(false);
expect(canFormatSqlForDatabaseType("mysql")).toBe(true);
@@ -0,0 +1,30 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { identifierInSql } from "@/lib/diagram/fieldLineage";
const fieldLineageSource = readFileSync(new URL("../fieldLineage.ts", import.meta.url), "utf8");
describe("identifierInSql", () => {
it("matches unquoted identifiers at the start or after punctuation", () => {
expect(identifierInSql("customer_id = 1", "customer_id")).toBe(true);
expect(identifierInSql("SELECT account.customer_id", "customer_id")).toBe(true);
expect(identifierInSql("SELECT (CUSTOMER_ID)", "customer_id")).toBe(true);
});
it("does not match identifiers embedded in longer identifiers", () => {
expect(identifierInSql("SELECT customer_identifier", "customer_id")).toBe(false);
expect(identifierInSql("SELECT archived_customer_id", "customer_id")).toBe(false);
expect(identifierInSql("SELECT customer_id_v2", "customer_id")).toBe(false);
expect(identifierInSql("SELECT $customer_id", "customer_id")).toBe(false);
});
it("matches identifiers quoted with supported SQL delimiters", () => {
expect(identifierInSql('SELECT "customer_id"', "customer_id")).toBe(true);
expect(identifierInSql("SELECT `customer_id`", "customer_id")).toBe(true);
expect(identifierInSql("SELECT [customer_id]", "customer_id")).toBe(true);
});
it("does not use runtime lookbehind in the legacy WebView path", () => {
expect(fieldLineageSource).not.toContain("(?<");
});
});
+1 -1
View File
@@ -65,7 +65,7 @@ export function summarizeLineageCounts(items: FieldLineageItem[]) {
export function identifierInSql(sql: string, identifier: string): boolean {
const escaped = escapeRegExp(identifier);
const quoted = [`"${escapeRegExp(identifier)}"`, `\`${escapeRegExp(identifier)}\``, `\\[${escapeRegExp(identifier)}\\]`];
const pattern = `(?:${quoted.join("|")}|(?<![\\w$])${escaped}(?![\\w$]))`;
const pattern = `(?:${quoted.join("|")}|(?:^|[^\\w$])${escaped}(?![\\w$]))`;
return new RegExp(pattern, "i").test(sql);
}
+1 -1
View File
@@ -348,7 +348,7 @@ function normalizeLikeOperatorCase(sql: string, settings: SqlFormatterSettings,
// identifiers under their existing function/identifier case settings.
const { masked, spans } = maskStringAndCommentSpans(sql, dialect);
const keyword = settings.keywordCase === "upper" ? "LIKE" : "like";
const normalized = masked.replace(/(?<![.\w$:@{#])LIKE\b(?!\s*\()/gi, keyword);
const normalized = masked.replace(/(^|[^.\w$:@{#])LIKE\b(?!\s*\()/gi, (_match, prefix: string) => `${prefix}${keyword}`);
return restoreSpans(normalized, spans);
}
+20 -3
View File
@@ -3,7 +3,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { applyLegacyWebViewClass, isLegacyWebView, LEGACY_WEBVIEW_CLASS, missingLegacyWebViewCapabilities } from "@/lib/ui/legacyWebView";
const supportedFeatures = new Set(["oklch", "color-mix-oklab", "color-mix-oklch", "has-selector", "dynamic-viewport", "min-function"]);
const supportedFeatures = new Set(["oklch", "color-mix-oklab", "color-mix-oklch", "has-selector", "dynamic-viewport", "min-function", "media-query-range"]);
function mockCssSupports(features: Set<string>) {
vi.stubGlobal("CSS", {
@@ -27,6 +27,16 @@ function mockCssSupports(features: Set<string>) {
return features.has(key);
},
});
vi.stubGlobal("matchMedia", (query: string) => ({
matches: query === "(width >= 0px)" ? features.has("media-query-range") : false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
}
afterEach(() => {
@@ -45,14 +55,21 @@ describe("legacy WebView detection", () => {
mockCssSupports(new Set(["oklch"]));
expect(isLegacyWebView()).toBe(true);
expect(missingLegacyWebViewCapabilities()).toEqual(["color-mix-oklab", "color-mix-oklch", "has-selector", "dynamic-viewport", "min-function"]);
expect(missingLegacyWebViewCapabilities()).toEqual(["color-mix-oklab", "color-mix-oklch", "has-selector", "dynamic-viewport", "min-function", "media-query-range"]);
});
it("does not treat one color-mix interpolation space as the other", () => {
mockCssSupports(new Set(["oklch", "color-mix-oklab", "has-selector", "dynamic-viewport", "min-function"]));
expect(isLegacyWebView()).toBe(true);
expect(missingLegacyWebViewCapabilities()).toEqual(["color-mix-oklch"]);
expect(missingLegacyWebViewCapabilities()).toEqual(["color-mix-oklch", "media-query-range"]);
});
it("marks a WebView without media query range syntax as legacy", () => {
mockCssSupports(new Set(["oklch", "color-mix-oklab", "color-mix-oklch", "has-selector", "dynamic-viewport", "min-function"]));
expect(isLegacyWebView()).toBe(true);
expect(missingLegacyWebViewCapabilities()).toEqual(["media-query-range"]);
});
it("adds and removes the root class idempotently", () => {
+13
View File
@@ -5,6 +5,7 @@ type CssSupportCheck = {
property?: string;
value?: string;
condition?: string;
test?: () => boolean;
};
const MODERN_CSS_FEATURES: CssSupportCheck[] = [
@@ -14,9 +15,21 @@ const MODERN_CSS_FEATURES: CssSupportCheck[] = [
{ name: "has-selector", condition: "selector(:has(*))" },
{ name: "dynamic-viewport", property: "height", value: "100dvh" },
{ name: "min-function", property: "width", value: "min(100%, 1px)" },
{
name: "media-query-range",
test: () => {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
try {
return window.matchMedia("(width >= 0px)").matches;
} catch {
return false;
}
},
},
];
function supports(check: CssSupportCheck): boolean {
if (check.test) return check.test();
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return false;
try {
@@ -6,6 +6,7 @@ const globalsCss = readCascadeCss();
const dialogContentSource = readFileSync(new URL("../../components/ui/dialog/DialogContent.vue", import.meta.url), "utf8");
const dialogScrollContentSource = readFileSync(new URL("../../components/ui/dialog/DialogScrollContent.vue", import.meta.url), "utf8");
const dialogOverlaySource = readFileSync(new URL("../../components/ui/dialog/DialogOverlay.vue", import.meta.url), "utf8");
const dataTransferDialogSource = readFileSync(new URL("../../components/transfer/DataTransferDialog.vue", import.meta.url), "utf8");
const connectionDialogSource = readFileSync(new URL("../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
const connectionTreeSource = readFileSync(new URL("../../components/sidebar/ConnectionTree.vue", import.meta.url), "utf8");
const scheduledDatabaseBackupSource = readFileSync(new URL("../../components/backup/ScheduledDatabaseBackupSettings.vue", import.meta.url), "utf8");
@@ -79,6 +80,14 @@ describe("legacy WebView CSS fallbacks", () => {
expect(globalsCss).not.toContain("filter: blur(4px);");
});
it("keeps the data transfer dialog width fallback scoped to legacy WebViews", () => {
expect(dataTransferDialogSource).toContain('class="dbx-transfer-dialog sm:max-w-[1120px] max-h-[80vh] flex flex-col overflow-hidden"');
expect(dataTransferDialogSource).toContain('html.dbx-legacy-webview [data-slot="dialog-content"].dbx-transfer-dialog[class~="max-w-sm"]');
expect(dataTransferDialogSource).toContain("width: calc(100vw - 2rem) !important;");
expect(dataTransferDialogSource).toContain("max-width: 1120px !important;");
expect(globalsCss).not.toContain(".dbx-transfer-dialog");
});
it("keeps primary alpha utilities readable in legacy WebViews", () => {
expect(globalsCss).toContain("--dbx-primary-rgb: 23, 23, 23;");
expect(globalsCss).toContain("--dbx-primary-rgb: 46, 95, 166;");