fix(desktop): improve legacy WebView style compatibility

This commit is contained in:
知食分子 authored and GitHub committed 2026-08-10 18:22:16 +08:00
1 parent 585e2f3c50
commit 7a19c9d770
30 files changed
+1802 -459

No files matched your search

@@ -41,6 +41,7 @@ const tablePatternsInput = ref("");
const expandedRunIds = reactive(new Set<string>());
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) {
<div class="min-w-0">
<h3 class="text-base font-semibold">{{ t("databaseBackup.schedules") }}</h3>
<p class="mt-1 text-sm text-muted-foreground">{{ t("databaseBackup.runtimeRequirement") }}</p>
<p v-if="!canCreateSchedule" class="mt-1 text-xs text-muted-foreground">{{ t("databaseBackup.noSupportedConnections") }}</p>
</div>
<Button size="sm" :disabled="sqlConnections.length === 0" @click="openCreateSchedule">
<Button size="sm" :disabled="!canCreateSchedule" :title="canCreateSchedule ? t('databaseBackup.addSchedule') : t('databaseBackup.noSupportedConnections')" @click="openCreateSchedule">
<Plus class="mr-2 h-4 w-4" />
{{ t("databaseBackup.addSchedule") }}
</Button>
@@ -412,7 +414,7 @@ function restoreBackup(run: DatabaseBackupRun, file: DatabaseBackupFile) {
</div>
<Dialog v-model:open="scheduleDialogOpen">
<DialogContent class="max-h-[min(760px,calc(var(--dbx-viewport-height)-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-y-auto">
<DialogContent class="dbx-form-dialog dbx-form-dialog--lg max-h-[min(760px,calc(var(--dbx-viewport-height)-32px))] max-w-[min(720px,calc(100vw-32px))] overflow-y-auto">
<DialogHeader>
<DialogTitle>{{ editingScheduleId ? t("databaseBackup.editSchedule") : t("databaseBackup.addSchedule") }}</DialogTitle>
</DialogHeader>
@@ -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<string>(),
activeRunIds: new Set<string>(),
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")));
});
});
@@ -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) => {
</nav>
<!-- Driver list area -->
<div class="min-w-0 flex-1 overflow-y-auto sm:pl-4">
<div class="driver-store-agent-results min-w-0 flex-1 overflow-y-auto sm:pl-4">
<!-- Empty: still loading -->
<div v-if="drivers.length === 0" class="py-12 text-center text-sm text-muted-foreground">
{{ 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;
@@ -3034,6 +3034,7 @@ const tlsCapableDatabaseTypes = new Set<DatabaseType>([
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) {
</label>
</div>
<div v-if="supportsGenericUrlParams" class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelClass">{{ t("connection.urlParams") }}</Label>
<div v-if="supportsGenericUrlParams" class="connection-url-params-row grid grid-cols-4 items-start gap-4" :class="{ 'connection-url-params-row--compact': !showGenericUrlParamsHint, 'connection-url-params-row--with-hint': showGenericUrlParamsHint }">
<Label :class="[connectionLabelClass, 'connection-url-params-label']">{{ t("connection.urlParams") }}</Label>
<div class="col-span-3 space-y-1.5">
<Input
v-model="form.url_params"
@@ -7046,7 +7047,7 @@ function openExternalUrl(url: string) {
: 'sslmode=prefer'
"
/>
<p v-if="form.db_type === 'mysql' || form.db_type === 'doris' || form.db_type === 'starrocks'" class="text-xs leading-5 text-muted-foreground">
<p v-if="showGenericUrlParamsHint" class="text-xs leading-5 text-muted-foreground">
{{ t("connection.localInfilePathHint") }}
</p>
</div>
@@ -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 {
@@ -190,7 +190,14 @@ async function testSelected() {
<p v-if="!draft.length" class="rounded-md border border-dashed px-3 py-4 text-center text-xs text-muted-foreground">
{{ t("settings.tunnelsEmpty") }}
</p>
<button v-for="profile in draft" :key="profile.id" type="button" class="flex min-h-10 items-center gap-2 rounded-md border px-3 text-left text-xs transition-colors" :class="profile.id === selectedId ? 'border-primary bg-primary/5' : 'hover:bg-muted/50'" @click="selectedId = profile.id">
<button
v-for="profile in draft"
:key="profile.id"
type="button"
class="flex min-h-10 items-center gap-2 rounded-md border px-3 text-left text-xs transition-colors"
:class="profile.id === selectedId ? 'tunnel-profile-option--selected border-primary bg-primary/5' : 'hover:bg-muted/50'"
@click="selectedId = profile.id"
>
<span class="shrink-0 rounded border bg-muted/40 px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground">{{ profileTypeLabel(profile) }}</span>
<span class="min-w-0 flex-1 truncate">{{ profileDisplayName(profile) }}</span>
<span class="min-w-0 truncate text-muted-foreground">{{ tunnelProfileSummary(profile) }}</span>
@@ -356,3 +363,20 @@ async function testSelected() {
</p>
</div>
</template>
<style>
html.dbx-legacy-webview .tunnel-profile-option--selected {
color: var(--foreground) !important;
border-color: var(--ring) !important;
background-color: var(--muted) !important;
box-shadow: inset 0 0 0 1px var(--border);
}
html.dbx-legacy-webview .tunnel-profile-option--selected:hover {
background-color: var(--accent) !important;
}
html.dbx-legacy-webview .tunnel-profile-option--selected .text-muted-foreground {
color: var(--muted-foreground) !important;
}
</style>
@@ -3901,12 +3901,16 @@ onUnmounted(() => {
{{ t("settings.sqlFormatterEditorShortcuts") }}
</div>
<div class="overflow-hidden rounded-md border border-border/70 bg-background">
<div v-for="definition in formatterEditorShortcutDefinitions" :key="definition.id" class="group -mt-px grid gap-2 border-t border-border/70 px-3 py-2 transition-colors first:mt-0 first:border-t-0 hover:bg-muted/40 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center">
<div class="min-w-0">
<div
v-for="definition in formatterEditorShortcutDefinitions"
:key="definition.id"
class="settings-shortcut-row group -mt-px grid gap-2 border-t border-border/70 px-3 py-2 transition-colors first:mt-0 first:border-t-0 hover:bg-muted/40 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center"
>
<div class="settings-shortcut-label min-w-0">
<Label class="min-w-0 truncate leading-none">{{ t(definition.labelKey) }}</Label>
</div>
<div class="min-w-0 space-y-1">
<div class="flex items-center justify-end gap-1.5">
<div class="settings-shortcut-actions min-w-0 space-y-1 text-right">
<div class="settings-shortcut-controls flex items-center justify-end gap-1.5">
<input
:data-shortcut-input="definition.id"
:value="editingShortcutId === definition.id ? '' : formatShortcutPill(editShortcuts[definition.id])"
@@ -3925,7 +3929,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.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(() => {
<div class="space-y-2">
<Label>{{ t("settings.exportBatchSize") }}</Label>
<div class="flex items-center gap-3">
<Input type="number" list="export-batch-sizes" min="100" max="100000" step="100" v-model.number="editExportBatchSize" class="h-9 w-28 [&::-webkit-inner-spin-button]:appearance-none" />
<Input type="number" list="export-batch-sizes" min="100" max="100000" step="100" v-model.number="editExportBatchSize" class="settings-export-number-input h-9 w-28 [&::-webkit-inner-spin-button]:appearance-none" />
<datalist id="export-batch-sizes">
<option value="500" />
<option value="1000" />
@@ -4947,7 +4951,7 @@ onUnmounted(() => {
<div class="space-y-2">
<Label for="export-row-limit">{{ t("settings.exportRowLimit") }}</Label>
<div class="flex items-center gap-3">
<Input id="export-row-limit" type="number" min="100" max="2147483647" step="100" v-model.number="editExportRowLimit" :disabled="!editExportRowLimitEnabled" class="h-9 w-32 [&::-webkit-inner-spin-button]:appearance-none" />
<Input id="export-row-limit" type="number" min="100" max="2147483647" step="100" v-model.number="editExportRowLimit" :disabled="!editExportRowLimitEnabled" class="settings-export-number-input h-9 w-32 [&::-webkit-inner-spin-button]:appearance-none" />
<span class="text-xs text-muted-foreground">
{{ editExportRowLimitEnabled ? t("settings.exportRowLimitDescription") : t("settings.exportRowLimitUnlimited") }}
</span>
@@ -5082,8 +5086,8 @@ onUnmounted(() => {
<div v-if="filteredShortcutDefinitions.length === 0" class="px-3 py-8 text-center text-sm text-muted-foreground">
{{ t("settings.shortcutSearchNoResults") }}
</div>
<div v-for="definition in filteredShortcutDefinitions" :key="definition.id" class="group -mt-px grid gap-2 border-t border-border/70 px-3 py-2 transition-colors first:mt-0 first:border-t-0 hover:bg-muted/40 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center">
<div class="min-w-0">
<div v-for="definition in filteredShortcutDefinitions" :key="definition.id" class="settings-shortcut-row group -mt-px grid gap-2 border-t border-border/70 px-3 py-2 transition-colors first:mt-0 first:border-t-0 hover:bg-muted/40 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center">
<div class="settings-shortcut-label min-w-0">
<div class="flex min-w-0 items-center gap-2">
<Label class="min-w-0 truncate leading-none">{{ t(definition.labelKey) }}</Label>
<Badge variant="outline" class="h-5 shrink-0 rounded-md border-border/60 px-1.5 text-[11px] font-normal text-muted-foreground">
@@ -5091,8 +5095,8 @@ onUnmounted(() => {
</Badge>
</div>
</div>
<div class="min-w-0 space-y-1">
<div class="flex items-center justify-end gap-1.5">
<div class="settings-shortcut-actions min-w-0 space-y-1 text-right">
<div class="settings-shortcut-controls flex items-center justify-end gap-1.5">
<input
:data-shortcut-input="definition.id"
:value="editingShortcutId === definition.id ? '' : formatShortcutPill(editShortcuts[definition.id])"
@@ -5111,7 +5115,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.shortcutPressShortcut')"
@click="focusShortcutInput(definition.id)"
>
@@ -5125,7 +5129,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)"
>
@@ -5136,7 +5140,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)"
>
@@ -5743,7 +5747,7 @@ onUnmounted(() => {
<!-- Config Edit View -->
<div v-else class="space-y-4">
<div class="flex items-center justify-between">
<Button type="button" variant="ghost" size="sm" @click="aiEnterListMode()">
<Button type="button" variant="ghost" size="sm" class="settings-ai-back-button" @click="aiEnterListMode()">
<ArrowLeft class="mr-1 h-3.5 w-3.5" />
{{ t("common.back") }}
</Button>
@@ -6338,19 +6342,21 @@ onUnmounted(() => {
<section v-else-if="activeSettingsTab === 'about'" data-settings-search-id="about" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('about')]">
<div class="rounded-lg border p-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="settings-about-section-header flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0 space-y-1">
<Label>{{ t("settings.supportInfoTitle") }}</Label>
<p class="text-sm text-muted-foreground">
{{ t("settings.supportInfoDescription") }}
</p>
</div>
<Button type="button" variant="outline" size="sm" class="shrink-0" :disabled="appSupportInfoLoading && !appSupportInfo" @click="copyAppSupportInfo">
<Loader2 v-if="appSupportInfoLoading && !appSupportInfo" class="mr-1 h-3.5 w-3.5 animate-spin" />
<CheckCircle2 v-else-if="appSupportInfoCopied" class="mr-1 h-3.5 w-3.5" />
<Copy v-else class="mr-1 h-3.5 w-3.5" />
{{ appSupportInfoCopied ? t("settings.supportInfoCopied") : t("settings.supportInfoCopy") }}
</Button>
<div class="settings-about-section-actions flex shrink-0 flex-wrap items-center gap-2">
<Button type="button" variant="outline" size="sm" class="shrink-0" :disabled="appSupportInfoLoading && !appSupportInfo" @click="copyAppSupportInfo">
<Loader2 v-if="appSupportInfoLoading && !appSupportInfo" class="mr-1 h-3.5 w-3.5 animate-spin" />
<CheckCircle2 v-else-if="appSupportInfoCopied" class="mr-1 h-3.5 w-3.5" />
<Copy v-else class="mr-1 h-3.5 w-3.5" />
{{ appSupportInfoCopied ? t("settings.supportInfoCopied") : t("settings.supportInfoCopy") }}
</Button>
</div>
</div>
<div v-if="appSupportInfoRows.length" class="mt-4 grid gap-3 sm:grid-cols-2">
<div v-for="row in appSupportInfoRows" :key="row.key" class="min-w-0 rounded-md bg-muted/30 px-3 py-2">
@@ -6690,6 +6696,103 @@ onUnmounted(() => {
margin-top: 0.625rem;
}
html.dbx-legacy-webview .settings-shortcut-row:hover .settings-shortcut-action-button,
html.dbx-legacy-webview .settings-shortcut-row:focus-within .settings-shortcut-action-button {
opacity: 1 !important;
}
html.dbx-legacy-webview .settings-layout [data-slot="select-trigger"][data-size="default"]:not(.h-7) {
height: 2rem !important;
min-height: 2rem !important;
box-sizing: border-box !important;
}
html.dbx-legacy-webview .settings-layout [data-slot="select-trigger"].h-9 {
height: 2rem !important;
min-height: 2rem !important;
box-sizing: border-box !important;
}
html.dbx-legacy-webview .settings-layout [data-slot="select-trigger"][data-size="sm"],
html.dbx-legacy-webview .settings-layout [data-slot="select-trigger"].h-7 {
height: 1.75rem !important;
min-height: 1.75rem !important;
box-sizing: border-box !important;
}
html.dbx-legacy-webview .settings-layout .settings-shortcut-row {
grid-template-columns: minmax(0, 1fr) auto !important;
align-items: center !important;
column-gap: 0.75rem !important;
}
html.dbx-legacy-webview .settings-layout .settings-shortcut-label {
align-self: center !important;
}
html.dbx-legacy-webview .settings-layout .settings-shortcut-actions {
justify-self: end !important;
align-self: center !important;
text-align: right !important;
}
html.dbx-legacy-webview .settings-layout .settings-shortcut-controls {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: flex-end !important;
gap: 0.375rem !important;
}
html.dbx-legacy-webview .settings-layout .settings-export-number-input {
height: 2rem !important;
min-height: 2rem !important;
padding-top: 0.25rem !important;
padding-bottom: 0.25rem !important;
line-height: 1.25rem !important;
font-variant-numeric: tabular-nums;
}
html.dbx-legacy-webview .settings-layout .settings-export-number-input::-webkit-inner-spin-button,
html.dbx-legacy-webview .settings-layout .settings-export-number-input::-webkit-outer-spin-button {
-webkit-appearance: inner-spin-button !important;
appearance: auto !important;
min-height: 1.5rem !important;
opacity: 1 !important;
}
html.dbx-legacy-webview .settings-ai-back-button {
margin-left: -0.625rem !important;
}
html.dbx-legacy-webview .settings-about-section-header {
display: flex !important;
flex-direction: row !important;
align-items: flex-start !important;
justify-content: space-between !important;
gap: 0.75rem !important;
}
html.dbx-legacy-webview .settings-about-section-actions {
display: flex !important;
flex-wrap: wrap !important;
align-items: center !important;
justify-content: flex-end !important;
margin-left: auto !important;
gap: 0.5rem !important;
}
@media (max-width: 640px) {
html.dbx-legacy-webview .settings-about-section-header {
flex-direction: column !important;
}
html.dbx-legacy-webview .settings-about-section-actions {
justify-content: flex-start !important;
margin-left: 0 !important;
}
}
@media (max-width: 760px) {
.settings-appearance-top-grid,
.settings-appearance-theme-grid,
@@ -117,12 +117,12 @@ watch(changelogLang, (lang) => {
<template>
<div class="rounded-lg border p-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="settings-about-section-header flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0 space-y-1">
<Label>{{ t("settings.changelogTitle") }}</Label>
<p class="text-sm text-muted-foreground">{{ t("settings.changelogDescription") }}</p>
</div>
<div class="flex shrink-0 flex-wrap items-center gap-2">
<div class="settings-about-section-actions flex shrink-0 flex-wrap items-center gap-2">
<Button type="button" variant="outline" size="sm" :disabled="loading" @click="openExternalUrl(changelogWebsiteUrl(changelogLang))">
<ExternalLink class="mr-1 h-3.5 w-3.5" />
{{ t("settings.changelogOpenWebsite") }}
@@ -2,6 +2,7 @@
import { createApp, defineComponent, h, nextTick, reactive } from "vue";
import { afterEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n";
import SidebarTreeItemDialogs from "../SidebarTreeItemDialogs.vue";
const mountedApps: ReturnType<typeof createApp>[] = [];
@@ -54,6 +55,7 @@ describe("SidebarTreeItemDialogs MongoDB index form", () => {
}),
);
mountedApps.push(app);
app.use(i18n);
app.mount(document.createElement("div"));
await nextTick();
@@ -36,7 +36,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template>
<DialogPortal>
<DialogOverlay :class="props.overlayClass" />
<div :class="cn('fixed inset-0 z-50 grid place-items-center p-4 pointer-events-none', props.portalClass)">
<div data-slot="dialog-positioner" :class="cn('fixed inset-0 z-50 grid place-items-center p-4 pointer-events-none', props.portalClass)">
<DialogContent
data-slot="dialog-content"
v-bind="{ ...$attrs, ...forwarded }"
@@ -23,7 +23,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template>
<DialogPortal>
<DialogOverlay />
<div class="fixed inset-0 z-50 grid place-items-center p-4 pointer-events-none">
<div data-slot="dialog-positioner" class="fixed inset-0 z-50 grid place-items-center p-4 pointer-events-none">
<DialogContent
data-slot="dialog-content"
:class="cn('relative z-50 grid max-h-[calc(var(--dbx-viewport-height)-6rem)] w-full max-w-lg my-8 gap-4 rounded-lg border border-border bg-background p-4 shadow-lg duration-200 md:w-full pointer-events-auto', props.class)"
@@ -0,0 +1,43 @@
// @vitest-environment happy-dom
import { createApp, nextTick, ref } from "vue";
import { afterEach, describe, expect, it } from "vitest";
import Input from "@/components/ui/input/Input.vue";
const mountedApps: Array<ReturnType<typeof createApp>> = [];
async function mountInput(template: string) {
const host = document.createElement("div");
document.body.appendChild(host);
const app = createApp({
components: { Input },
setup() {
return { value: ref("1") };
},
template,
});
app.mount(host);
mountedApps.push(app);
await nextTick();
return host;
}
afterEach(() => {
for (const app of mountedApps.splice(0)) app.unmount();
document.body.replaceChildren();
});
describe("Input", () => {
it("uses the plain input for non-number fields", async () => {
const host = await mountInput('<Input v-model="value" />');
expect(host.querySelectorAll("input")).toHaveLength(1);
});
it("keeps number fields as native inputs and preserves narrow layout classes", async () => {
const host = await mountInput('<Input v-model="value" type="number" step="2" class="w-14" />');
expect(host.querySelectorAll("input")).toHaveLength(1);
expect(host.querySelector("input")?.className).toContain("w-14");
});
});
@@ -126,4 +126,28 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
.dark .dbx-switch[aria-checked="true"] .dbx-switch-thumb {
background-color: var(--primary-foreground) !important;
}
html.dbx-legacy-webview.dark .dbx-switch[data-state="unchecked"] .dbx-switch-thumb,
html.dbx-legacy-webview.dark .dbx-switch[data-unchecked] .dbx-switch-thumb,
html.dbx-legacy-webview.dark .dbx-switch[aria-checked="false"] .dbx-switch-thumb {
background-color: rgb(215, 215, 219) !important;
}
html.dbx-legacy-webview.dark .dbx-switch {
border-color: rgba(180, 180, 190, 0.46) !important;
background-color: rgba(110, 110, 114, 0.44) !important;
}
html.dbx-legacy-webview.dark .dbx-switch[data-state="checked"],
html.dbx-legacy-webview.dark .dbx-switch[data-checked],
html.dbx-legacy-webview.dark .dbx-switch[aria-checked="true"] {
border-color: rgb(208, 208, 214) !important;
background-color: rgb(208, 208, 214) !important;
}
html.dbx-legacy-webview.dark .dbx-switch[data-state="checked"] .dbx-switch-thumb,
html.dbx-legacy-webview.dark .dbx-switch[data-checked] .dbx-switch-thumb,
html.dbx-legacy-webview.dark .dbx-switch[aria-checked="true"] .dbx-switch-thumb {
background-color: rgb(19, 20, 22) !important;
}
</style>