mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(desktop): improve legacy WebView style compatibility
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+115
@@ -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
@@ -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>
|
||||
|
||||
@@ -6044,6 +6044,7 @@ export default {
|
||||
schedules: "Backup schedules",
|
||||
runtimeRequirement: "Scheduled backups run while DBX is open, including when it is minimized to the tray.",
|
||||
addSchedule: "Add schedule",
|
||||
noSupportedConnections: "Create a MySQL or PostgreSQL connection before adding a backup schedule.",
|
||||
editSchedule: "Edit schedule",
|
||||
defaultScheduleName: "Database backup",
|
||||
noSchedules: "No backup schedules",
|
||||
|
||||
@@ -7091,6 +7091,7 @@ export default withEnglishFallback({
|
||||
schedules: "Planes de copia de seguridad",
|
||||
runtimeRequirement: "DBX ejecutará copias de seguridad programadas cuando esté abierto, incluso cuando esté minimizado en la bandeja.",
|
||||
addSchedule: "Nuevo plan",
|
||||
noSupportedConnections: "Crea una conexión MySQL o PostgreSQL antes de añadir un plan de copia de seguridad.",
|
||||
editSchedule: "Editar plan",
|
||||
defaultScheduleName: "Copia de seguridad de la base de datos",
|
||||
noSchedules: "No hay planes de copia de seguridad",
|
||||
|
||||
@@ -7091,6 +7091,7 @@ export default withEnglishFallback({
|
||||
schedules: "Pianificazioni di backup",
|
||||
runtimeRequirement: "I backup programmati vengono eseguiti quando DBX è aperto, anche se ridotto a icona nella barra delle applicazioni.",
|
||||
addSchedule: "Nuova pianificazione",
|
||||
noSupportedConnections: "Crea una connessione MySQL o PostgreSQL prima di aggiungere una pianificazione di backup.",
|
||||
editSchedule: "Modifica pianificazione",
|
||||
defaultScheduleName: "Backup del database",
|
||||
noSchedules: "Nessuna pianificazione di backup",
|
||||
|
||||
@@ -7146,6 +7146,7 @@ export default withEnglishFallback({
|
||||
schedules: "バックアップスケジュール",
|
||||
runtimeRequirement: "DBXが開いている状態(トレイに最小化したときも含む)で、定期バックアップが実行されます。",
|
||||
addSchedule: "新規スケジュール",
|
||||
noSupportedConnections: "バックアップスケジュールを追加するには、先に MySQL または PostgreSQL 接続を作成してください。",
|
||||
editSchedule: "スケジュール編集",
|
||||
defaultScheduleName: "データベースバックアップ",
|
||||
noSchedules: "バックアップスケジュールがありません",
|
||||
|
||||
@@ -5536,6 +5536,7 @@ export default withEnglishFallback({
|
||||
schedules: "백업 일정",
|
||||
runtimeRequirement: "예약된 백업은 DBX가 열려 있는 동안(트레이에 최소화된 경우 포함) 실행됩니다.",
|
||||
addSchedule: "일정 추가",
|
||||
noSupportedConnections: "백업 일정을 추가하려면 먼저 MySQL 또는 PostgreSQL 연결을 만들어 주세요.",
|
||||
editSchedule: "일정 편집",
|
||||
defaultScheduleName: "데이터베이스 백업",
|
||||
noSchedules: "백업 일정이 없습니다",
|
||||
|
||||
@@ -7093,6 +7093,7 @@ export default withEnglishFallback({
|
||||
schedules: "Planos de backup",
|
||||
runtimeRequirement: "O DBX executará backups agendados quando estiver aberto, inclusive quando minimizado na bandeja.",
|
||||
addSchedule: "Novo plano",
|
||||
noSupportedConnections: "Crie uma conexão MySQL ou PostgreSQL antes de adicionar um plano de backup.",
|
||||
editSchedule: "Editar plano",
|
||||
defaultScheduleName: "Backup de banco de dados",
|
||||
noSchedules: "Nenhum plano de backup",
|
||||
|
||||
@@ -5965,6 +5965,7 @@ export default withEnglishFallback({
|
||||
schedules: "备份计划",
|
||||
runtimeRequirement: "DBX 处于打开状态时会执行定时备份,包括最小化到托盘时。",
|
||||
addSchedule: "新建计划",
|
||||
noSupportedConnections: "请先创建 MySQL 或 PostgreSQL 连接,再新建备份计划。",
|
||||
editSchedule: "编辑计划",
|
||||
defaultScheduleName: "数据库备份",
|
||||
noSchedules: "暂无备份计划",
|
||||
|
||||
@@ -7086,6 +7086,7 @@ export default withEnglishFallback({
|
||||
schedules: "備份計畫",
|
||||
runtimeRequirement: "DBX 處於開啟狀態時會執行定時備份,包含最小化到托盤時。",
|
||||
addSchedule: "新建計畫",
|
||||
noSupportedConnections: "請先建立 MySQL 或 PostgreSQL 連線,再新增備份計畫。",
|
||||
editSchedule: "編輯計畫",
|
||||
defaultScheduleName: "資料庫備份",
|
||||
noSchedules: "暫無備份計畫",
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
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"]);
|
||||
|
||||
function mockCssSupports(features: Set<string>) {
|
||||
vi.stubGlobal("CSS", {
|
||||
supports: (propertyOrCondition: string, value?: string) => {
|
||||
const key =
|
||||
value === undefined
|
||||
? propertyOrCondition === "selector(:has(*))"
|
||||
? "has-selector"
|
||||
: propertyOrCondition
|
||||
: propertyOrCondition === "color"
|
||||
? "oklch"
|
||||
: propertyOrCondition === "background-color"
|
||||
? value?.includes("oklab")
|
||||
? "color-mix-oklab"
|
||||
: "color-mix-oklch"
|
||||
: propertyOrCondition === "height"
|
||||
? "dynamic-viewport"
|
||||
: propertyOrCondition === "width"
|
||||
? "min-function"
|
||||
: "";
|
||||
return features.has(key);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("legacy WebView detection", () => {
|
||||
it("accepts a WebView with the CSS capabilities used by Tailwind v4 fallbacks", () => {
|
||||
mockCssSupports(supportedFeatures);
|
||||
|
||||
expect(isLegacyWebView()).toBe(false);
|
||||
expect(missingLegacyWebViewCapabilities()).toEqual([]);
|
||||
});
|
||||
|
||||
it("marks a partial WebView as legacy when any required capability is missing", () => {
|
||||
mockCssSupports(new Set(["oklch"]));
|
||||
|
||||
expect(isLegacyWebView()).toBe(true);
|
||||
expect(missingLegacyWebViewCapabilities()).toEqual(["color-mix-oklab", "color-mix-oklch", "has-selector", "dynamic-viewport", "min-function"]);
|
||||
});
|
||||
|
||||
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"]);
|
||||
});
|
||||
|
||||
it("adds and removes the root class idempotently", () => {
|
||||
const root = document.createElement("html");
|
||||
mockCssSupports(new Set(["oklch"]));
|
||||
|
||||
expect(applyLegacyWebViewClass(root)).toBe(true);
|
||||
expect(root.classList.contains(LEGACY_WEBVIEW_CLASS)).toBe(true);
|
||||
|
||||
mockCssSupports(supportedFeatures);
|
||||
expect(applyLegacyWebViewClass(root)).toBe(false);
|
||||
expect(root.classList.contains(LEGACY_WEBVIEW_CLASS)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
export const LEGACY_WEBVIEW_CLASS = "dbx-legacy-webview";
|
||||
|
||||
type CssSupportCheck = {
|
||||
name: string;
|
||||
property?: string;
|
||||
value?: string;
|
||||
condition?: string;
|
||||
};
|
||||
|
||||
const MODERN_CSS_FEATURES: CssSupportCheck[] = [
|
||||
{ name: "oklch", property: "color", value: "oklch(0.5 0.1 180)" },
|
||||
{ name: "color-mix-oklab", property: "background-color", value: "color-mix(in oklab, black 10%, transparent)" },
|
||||
{ name: "color-mix-oklch", property: "background-color", value: "color-mix(in oklch, black 10%, transparent)" },
|
||||
{ name: "has-selector", condition: "selector(:has(*))" },
|
||||
{ name: "dynamic-viewport", property: "height", value: "100dvh" },
|
||||
{ name: "min-function", property: "width", value: "min(100%, 1px)" },
|
||||
];
|
||||
|
||||
function supports(check: CssSupportCheck): boolean {
|
||||
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return false;
|
||||
|
||||
try {
|
||||
return check.condition ? CSS.supports(check.condition) : CSS.supports(check.property!, check.value!);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function missingLegacyWebViewCapabilities(): string[] {
|
||||
return MODERN_CSS_FEATURES.filter((feature) => !supports(feature)).map((feature) => feature.name);
|
||||
}
|
||||
|
||||
export function isLegacyWebView(): boolean {
|
||||
return missingLegacyWebViewCapabilities().length > 0;
|
||||
}
|
||||
|
||||
export function applyLegacyWebViewClass(root: Element = document.documentElement): boolean {
|
||||
const legacy = isLegacyWebView();
|
||||
root.classList.toggle(LEGACY_WEBVIEW_CLASS, legacy);
|
||||
return legacy;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
||||
import "./styles/globals.css";
|
||||
import { installDebugLogCapture } from "@/lib/backend/debugLog";
|
||||
import { clearStartupPreloadRetry, retryStartupAfterPreloadFailure } from "@/lib/startup/startupPreloadRecovery";
|
||||
import { applyLegacyWebViewClass } from "@/lib/ui/legacyWebView";
|
||||
|
||||
function startupErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
@@ -95,4 +96,5 @@ async function bootstrap() {
|
||||
|
||||
installDebugLogCapture();
|
||||
installStartupErrorHandlers();
|
||||
applyLegacyWebViewClass();
|
||||
void bootstrap().catch(renderStartupError);
|
||||
|
||||
+228
-14
@@ -7,29 +7,28 @@ const dialogContentSource = readFileSync(new URL("../../components/ui/dialog/Dia
|
||||
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 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");
|
||||
const driverStoreDialogSource = readFileSync(new URL("../../components/config/DriverStoreDialog.vue", import.meta.url), "utf8");
|
||||
const tunnelProfileManagerSource = readFileSync(new URL("../../components/connection/TunnelProfileManager.vue", import.meta.url), "utf8");
|
||||
const changelogPanelSource = readFileSync(new URL("../../components/settings/ChangelogPanel.vue", import.meta.url), "utf8");
|
||||
const editorSettingsDialogSource = readFileSync(new URL("../../components/editor/EditorSettingsDialog.vue", import.meta.url), "utf8");
|
||||
const switchSource = readFileSync(new URL("../../components/ui/switch/Switch.vue", import.meta.url), "utf8");
|
||||
const desktopIndexSource = readFileSync(new URL("../../../index.html", import.meta.url), "utf8");
|
||||
const connectionDialogLegacyCss = readFileSync(new URL("../../../public/connection-dialog-legacy.css", import.meta.url), "utf8");
|
||||
const legacyWebViewSource = readFileSync(new URL("../../lib/ui/legacyWebView.ts", import.meta.url), "utf8");
|
||||
const mainSource = readFileSync(new URL("../../main.ts", import.meta.url), "utf8");
|
||||
|
||||
describe("legacy WebView CSS fallbacks", () => {
|
||||
it("scopes component overrides to WebViews without OKLCH support", () => {
|
||||
const fallbackStart = globalsCss.indexOf("@supports not (color: oklch(0.5 0.1 180))");
|
||||
const tabsOverride = globalsCss.indexOf('[data-slot="tabs-trigger"]');
|
||||
it("scopes component overrides to the runtime legacy WebView class", () => {
|
||||
const fallbackStart = globalsCss.indexOf("html.dbx-legacy-webview .sm\\:block");
|
||||
const tabsOverride = globalsCss.indexOf('html.dbx-legacy-webview [data-slot="tabs-trigger"]');
|
||||
const splitpanesStart = globalsCss.indexOf("/* Splitpanes */");
|
||||
|
||||
expect(fallbackStart).toBeGreaterThan(-1);
|
||||
expect(tabsOverride).toBeGreaterThan(fallbackStart);
|
||||
expect(splitpanesStart).toBeGreaterThan(tabsOverride);
|
||||
|
||||
let nestingDepth = 0;
|
||||
let tabsNestingDepth = 0;
|
||||
for (let index = globalsCss.indexOf("{", fallbackStart); index < splitpanesStart; index++) {
|
||||
if (globalsCss[index] === "{") nestingDepth++;
|
||||
if (globalsCss[index] === "}") nestingDepth--;
|
||||
if (index === tabsOverride) tabsNestingDepth = nestingDepth;
|
||||
}
|
||||
|
||||
expect(tabsNestingDepth).toBeGreaterThan(0);
|
||||
expect(nestingDepth).toBe(0);
|
||||
expect(globalsCss.slice(fallbackStart, splitpanesStart)).toContain('html.dbx-legacy-webview [data-slot="tabs-trigger"]');
|
||||
});
|
||||
|
||||
it("falls back to the legacy viewport height when dynamic viewport units are unavailable", () => {
|
||||
@@ -45,6 +44,32 @@ describe("legacy WebView CSS fallbacks", () => {
|
||||
expect(connectionDialogSource).toContain("max-height: calc(var(--dbx-viewport-height) - 2rem);");
|
||||
});
|
||||
|
||||
it("centralizes legacy dialog positioning and layout utility fallbacks", () => {
|
||||
const fallbackStart = globalsCss.indexOf("html.dbx-legacy-webview .sm\\:block");
|
||||
const splitpanesStart = globalsCss.indexOf("/* Splitpanes */");
|
||||
const fallback = globalsCss.slice(fallbackStart, splitpanesStart);
|
||||
|
||||
expect(dialogContentSource).toContain('data-slot="dialog-positioner"');
|
||||
expect(dialogScrollContentSource).toContain('data-slot="dialog-positioner"');
|
||||
expect(fallback).toContain('[data-slot="dialog-positioner"]');
|
||||
expect(fallback).toContain("display: flex !important;");
|
||||
expect(fallback).toContain("align-items: center !important;");
|
||||
expect(fallback).toContain("justify-content: center !important;");
|
||||
expect(fallback).toContain(".sm\\:grid-cols-2");
|
||||
expect(fallback).toContain("grid-template-columns: repeat(2, minmax(0, 1fr)) !important;");
|
||||
expect(fallback).toContain(".sm\\:grid-cols-\\[minmax\\(0\\,200px\\)_minmax\\(0\\,1fr\\)\\]");
|
||||
expect(fallback).toContain(".space-y-1\\.5 > * + *");
|
||||
expect(fallback).toContain(".space-y-2\\.5 > * + *");
|
||||
expect(scheduledDatabaseBackupSource).toContain("dbx-form-dialog dbx-form-dialog--lg");
|
||||
expect(scheduledDatabaseBackupSource).toContain("max-w-[min(720px,calc(100vw-32px))]");
|
||||
expect(fallback).toContain('[data-slot="dialog-content"].dbx-form-dialog');
|
||||
expect(fallback).toContain('[data-slot="dialog-content"].dbx-form-dialog--lg');
|
||||
expect(fallback).toContain("max-width: 45rem !important;");
|
||||
expect(fallback).toContain('[data-slot="dialog-content"].dbx-form-dialog [data-slot="select-trigger"]');
|
||||
expect(fallback).toContain("height: 2rem !important;");
|
||||
expect(fallback).toContain('[data-slot="dialog-content"][class*="max-w-[min(720px"]');
|
||||
});
|
||||
|
||||
it("uses a lightweight theme-aware mask without full-window filters", () => {
|
||||
expect(dialogOverlaySource).not.toContain("backdrop-filter");
|
||||
expect(dialogOverlaySource).toContain("bg-black/25");
|
||||
@@ -53,6 +78,18 @@ describe("legacy WebView CSS fallbacks", () => {
|
||||
expect(globalsCss).not.toContain("filter: blur(4px);");
|
||||
});
|
||||
|
||||
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;");
|
||||
expect(globalsCss).toContain(".bg-primary\\/10");
|
||||
expect(globalsCss).toContain("background-color: rgba(var(--dbx-primary-rgb), 0.1) !important;");
|
||||
expect(globalsCss).toContain(".border-primary\\/30");
|
||||
expect(globalsCss).toContain("border-color: rgba(var(--dbx-primary-rgb), 0.3) !important;");
|
||||
expect(globalsCss).toContain(".hover\\:bg-primary\\/15:hover");
|
||||
expect(connectionTreeSource).toContain("showActiveConnectionsOnly");
|
||||
expect(connectionTreeSource.match(/bg-primary\/10 border-primary\/30/g)?.length).toBe(3);
|
||||
});
|
||||
|
||||
it("keeps legacy tab triggers connected to the configured corner style", () => {
|
||||
const tabsTriggerRule = globalsCss.match(/\[data-slot="tabs-trigger"\] \{([\s\S]*?)\n \}/)?.[1];
|
||||
|
||||
@@ -63,10 +100,187 @@ describe("legacy WebView CSS fallbacks", () => {
|
||||
expect(desktopIndexSource).toContain('href="/connection-dialog-legacy.css"');
|
||||
expect(connectionDialogLegacyCss).toContain("@media (min-width: 640px)");
|
||||
expect(connectionDialogLegacyCss).toContain("@media (min-width: 1024px)");
|
||||
expect(connectionDialogLegacyCss).toContain("html.dbx-legacy-webview .connection-db-picker-grid");
|
||||
expect(connectionDialogLegacyCss).toContain('html.dbx-legacy-webview [data-slot="dialog-content"].connection-dialog-content--config');
|
||||
expect(connectionDialogLegacyCss).toContain("min-width: 38rem !important;");
|
||||
expect(connectionDialogLegacyCss).toContain("width: 0 !important;");
|
||||
expect(connectionDialogLegacyCss).toContain("grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)) !important;");
|
||||
expect(connectionDialogLegacyCss).toContain('[data-slot="dialog-content"].connection-dialog-content--config');
|
||||
expect(connectionDialogLegacyCss).toMatch(/\[data-slot="dialog-content"\]\.connection-dialog-content--config\s*\{[\s\S]*?width: calc\(100vw - 2rem\) !important;[\s\S]*?min-width: 38rem !important;[\s\S]*?height: 720px !important;[\s\S]*?max-width: 880px !important;[\s\S]*?\}/);
|
||||
expect(connectionDialogLegacyCss).toContain("height: 720px !important;");
|
||||
expect(connectionDialogLegacyCss).toContain('[data-slot="dialog-content"].connection-dialog-content--config .connection-form-body');
|
||||
expect(connectionDialogLegacyCss).toContain("align-content: start !important;");
|
||||
expect(connectionDialogSource).toContain("connection-url-params-row--compact");
|
||||
expect(connectionDialogSource).toContain("connection-url-params-row--with-hint");
|
||||
expect(connectionDialogSource).toContain("connection-url-params-label");
|
||||
expect(connectionDialogLegacyCss).toContain('[data-slot="dialog-content"].connection-dialog-content--config .connection-url-params-row--compact');
|
||||
expect(connectionDialogLegacyCss).toContain("align-items: center !important;");
|
||||
expect(connectionDialogLegacyCss).toContain('[data-slot="dialog-content"].connection-dialog-content--config .connection-url-params-row--with-hint .connection-url-params-label');
|
||||
expect(connectionDialogLegacyCss).toContain("margin-top: 0.5rem !important;");
|
||||
expect(connectionDialogLegacyCss).not.toContain("minmax(8rem, 1fr)");
|
||||
expect(connectionDialogLegacyCss).toContain(".connection-db-picker-option");
|
||||
expect(connectionDialogLegacyCss).not.toContain("width >=");
|
||||
expect(connectionDialogSource).not.toContain("@media (min-width: 640px)");
|
||||
});
|
||||
|
||||
it("keeps selected tiles readable in WebViews without color-mix support", () => {
|
||||
const fallbackStart = globalsCss.indexOf("@supports not (background-color: color-mix(in srgb, black 10%, transparent))");
|
||||
const choiceFallback = globalsCss.indexOf(".dbx-choice-selected", fallbackStart);
|
||||
const tileFallback = globalsCss.indexOf(".dbx-tile-selected", fallbackStart);
|
||||
const nextSupports = globalsCss.indexOf("@supports (height: 100dvh)", fallbackStart);
|
||||
|
||||
expect(fallbackStart).toBeGreaterThan(-1);
|
||||
expect(choiceFallback).toBeGreaterThan(fallbackStart);
|
||||
expect(tileFallback).toBeGreaterThan(fallbackStart);
|
||||
expect(choiceFallback).toBeLessThan(tileFallback);
|
||||
expect(tileFallback).toBeLessThan(nextSupports);
|
||||
expect(globalsCss.slice(fallbackStart, tileFallback)).toContain("background-color: rgba(23, 23, 23, 0.08) !important;");
|
||||
expect(globalsCss.slice(fallbackStart, tileFallback)).toContain("color: rgb(23, 23, 23) !important;");
|
||||
expect(globalsCss.slice(fallbackStart, tileFallback)).toContain(".dbx-choice-selected .text-muted-foreground");
|
||||
expect(globalsCss.slice(fallbackStart, tileFallback)).toContain(".dark .dbx-choice-selected");
|
||||
expect(globalsCss.slice(fallbackStart, nextSupports)).toContain("background-color: rgba(23, 23, 23, 0.08) !important;");
|
||||
expect(globalsCss.slice(fallbackStart, nextSupports)).toContain("color: rgb(23, 23, 23) !important;");
|
||||
expect(globalsCss.slice(fallbackStart, nextSupports)).toContain(".dark .dbx-tile-selected");
|
||||
});
|
||||
|
||||
it("keeps the driver manager category navigation scoped to legacy WebViews", () => {
|
||||
const fallbackStart = driverStoreDialogSource.indexOf("html.dbx-legacy-webview .driver-store-tab");
|
||||
const fallbackEnd = driverStoreDialogSource.indexOf("@media (max-width: 900px)", fallbackStart);
|
||||
const fallback = driverStoreDialogSource.slice(fallbackStart, fallbackEnd);
|
||||
|
||||
expect(driverStoreDialogSource).toContain("data-driver-category-nav");
|
||||
expect(driverStoreDialogSource).toContain("driver-store-agent-results min-w-0 flex-1 overflow-y-auto sm:pl-4");
|
||||
expect(fallbackStart).toBeGreaterThan(-1);
|
||||
expect(fallbackEnd).toBeGreaterThan(fallbackStart);
|
||||
expect(fallback).toContain(".driver-store-tab");
|
||||
expect(fallback).toContain("margin-top: 1.25rem !important;");
|
||||
expect(fallback).toContain(".driver-store-agent-tab:not([hidden])");
|
||||
expect(fallback).toContain(".driver-store-jdbc-tab:not([hidden])");
|
||||
expect(fallback).toContain(".driver-store-storage-tab:not([hidden])");
|
||||
expect(fallback).toContain("gap: 1.25rem !important;");
|
||||
expect(fallback).not.toContain(".driver-store-tab .space-y-");
|
||||
expect(fallback).toContain("[data-driver-category-nav]");
|
||||
expect(fallback).toContain("width: 10rem !important;");
|
||||
expect(fallback).toContain("flex-direction: column !important;");
|
||||
expect(fallback).toContain("overflow-y: auto !important;");
|
||||
expect(fallback).toContain('[data-driver-category-nav] > button[aria-current="page"]');
|
||||
expect(fallback).toContain(".driver-store-agent-results");
|
||||
expect(fallback).toContain("width: 0 !important;");
|
||||
expect(fallback).toContain("flex: 1 1 0% !important;");
|
||||
});
|
||||
|
||||
it("keeps driver manager local import buttons large enough to target", () => {
|
||||
const fallbackStart = driverStoreDialogSource.indexOf("html.dbx-legacy-webview .driver-store-local-import-button");
|
||||
const fallbackEnd = driverStoreDialogSource.indexOf("@media (max-width: 900px)", fallbackStart);
|
||||
const fallback = driverStoreDialogSource.slice(fallbackStart, fallbackEnd);
|
||||
|
||||
expect(driverStoreDialogSource.match(/driver-store-local-import-button h-7 w-7 rounded-md text-xs text-muted-foreground/g)?.length).toBe(3);
|
||||
expect(driverStoreDialogSource.match(/variant="ghost"\n\s+class="driver-store-local-import-button/g)?.length).toBe(3);
|
||||
expect(fallback).toContain(".driver-store-local-import-button");
|
||||
expect(fallback).toContain("width: 2rem !important;");
|
||||
expect(fallback).toContain("height: 2rem !important;");
|
||||
expect(fallback).toContain(".driver-store-local-import-button svg");
|
||||
expect(fallback).toContain("width: 1rem !important;");
|
||||
expect(fallback).toContain("height: 1rem !important;");
|
||||
});
|
||||
|
||||
it("keeps tunnel profile selection readable in legacy WebViews", () => {
|
||||
expect(tunnelProfileManagerSource).toContain("profile.id === selectedId ? 'tunnel-profile-option--selected border-primary bg-primary/5'");
|
||||
expect(tunnelProfileManagerSource).toContain("html.dbx-legacy-webview .tunnel-profile-option--selected");
|
||||
expect(tunnelProfileManagerSource).toContain("background-color: var(--muted) !important;");
|
||||
expect(tunnelProfileManagerSource).toContain("color: var(--foreground) !important;");
|
||||
expect(tunnelProfileManagerSource).toContain("html.dbx-legacy-webview .tunnel-profile-option--selected .text-muted-foreground");
|
||||
});
|
||||
|
||||
it("keeps transport layer selection readable in legacy WebViews", () => {
|
||||
const fallbackStart = connectionDialogSource.indexOf("html.dbx-legacy-webview .connection-db-category-option--selected");
|
||||
const fallbackEnd = connectionDialogSource.indexOf(".connection-db-picker-option", fallbackStart);
|
||||
const fallback = connectionDialogSource.slice(fallbackStart, fallbackEnd);
|
||||
|
||||
expect(connectionDialogSource).toContain("connection-transport-layer-option--selected border-primary bg-primary/5");
|
||||
expect(fallback).toContain(".connection-transport-layer-option--selected");
|
||||
expect(fallback).toContain("background-color: rgba(23, 23, 23, 0.08) !important;");
|
||||
expect(fallback).toContain(".dark .connection-transport-layer-option--selected");
|
||||
});
|
||||
|
||||
it("keeps dark switches visible in legacy WebViews", () => {
|
||||
const fallbackStart = switchSource.indexOf('html.dbx-legacy-webview.dark .dbx-switch[data-state="unchecked"] .dbx-switch-thumb');
|
||||
const fallback = switchSource.slice(fallbackStart);
|
||||
|
||||
expect(fallbackStart).toBeGreaterThan(-1);
|
||||
expect(fallback).toContain('html.dbx-legacy-webview.dark .dbx-switch[data-state="unchecked"] .dbx-switch-thumb');
|
||||
expect(fallback).toContain("background-color: rgb(215, 215, 219) !important;");
|
||||
expect(fallback).toContain("html.dbx-legacy-webview.dark .dbx-switch {");
|
||||
expect(fallback).toContain("background-color: rgba(110, 110, 114, 0.44) !important;");
|
||||
expect(fallback).toContain("border-color: rgb(208, 208, 214) !important;");
|
||||
expect(fallback).toContain("background-color: rgb(19, 20, 22) !important;");
|
||||
});
|
||||
|
||||
it("keeps native number input steppers scoped to legacy WebViews", () => {
|
||||
expect(globalsCss).toContain('html.dbx-legacy-webview input[type="number"]');
|
||||
expect(globalsCss).toContain('html.dbx-legacy-webview input[type="number"]:not([class*="appearance-none"])::-webkit-inner-spin-button');
|
||||
expect(globalsCss).not.toContain('input[type="number"]::-webkit-inner-spin-button');
|
||||
expect(globalsCss).toContain("-webkit-appearance: inner-spin-button !important;");
|
||||
expect(globalsCss).not.toContain("width: 1.25rem !important;");
|
||||
expect(globalsCss).not.toContain("min-height: 1.4rem !important;");
|
||||
expect(globalsCss).not.toContain("-webkit-transform: scale(1.45);");
|
||||
expect(globalsCss).not.toContain("transform: scale(1.45);");
|
||||
expect(globalsCss).toContain('html.dbx-legacy-webview input[type="number"]:not([class*="appearance-none"]):disabled::-webkit-inner-spin-button');
|
||||
});
|
||||
|
||||
it("keeps settings field stacks spaced in legacy WebViews", () => {
|
||||
const fallbackStart = editorSettingsDialogSource.indexOf("html.dbx-legacy-webview .settings-layout");
|
||||
const fallbackEnd = editorSettingsDialogSource.indexOf("@media (max-width: 760px)", fallbackStart);
|
||||
const fallback = editorSettingsDialogSource.slice(fallbackStart, fallbackEnd);
|
||||
|
||||
expect(fallbackStart).toBeGreaterThan(-1);
|
||||
expect(fallbackEnd).toBeGreaterThan(fallbackStart);
|
||||
expect(fallback).not.toContain(".settings-layout .space-y-");
|
||||
expect(fallback).toContain('.settings-layout [data-slot="select-trigger"][data-size="default"]:not(.h-7)');
|
||||
expect(fallback).toContain("height: 2rem !important;");
|
||||
expect(fallback).toContain('.settings-layout [data-slot="select-trigger"].h-9');
|
||||
expect(fallback).toContain('.settings-layout [data-slot="select-trigger"][data-size="sm"],');
|
||||
expect(fallback).toContain('.settings-layout [data-slot="select-trigger"].h-7');
|
||||
expect(fallback).toContain("height: 1.75rem !important;");
|
||||
expect(editorSettingsDialogSource).toContain('SelectTrigger class="col-span-2" inputClass="h-8 text-xs"');
|
||||
expect(editorSettingsDialogSource).not.toContain('SelectTrigger class="col-span-2 h-8 text-xs"');
|
||||
expect(fallback).toContain(".settings-layout .settings-shortcut-row");
|
||||
expect(fallback).toContain("grid-template-columns: minmax(0, 1fr) auto !important;");
|
||||
expect(fallback).toContain(".settings-layout .settings-shortcut-actions");
|
||||
expect(fallback).toContain("justify-self: end !important;");
|
||||
expect(editorSettingsDialogSource).toContain("settings-shortcut-controls flex items-center justify-end gap-1.5");
|
||||
expect(editorSettingsDialogSource).toContain("settings-shortcut-action-button h-7 w-7");
|
||||
expect(editorSettingsDialogSource).toContain("html.dbx-legacy-webview .settings-shortcut-row:hover .settings-shortcut-action-button");
|
||||
expect(editorSettingsDialogSource).toContain("opacity: 1 !important;");
|
||||
expect(fallback).toContain(".settings-layout .settings-shortcut-controls");
|
||||
expect(fallback).toContain("flex-direction: row !important;");
|
||||
expect(fallback).toContain("justify-content: flex-end !important;");
|
||||
expect(editorSettingsDialogSource).toContain("settings-export-number-input h-9 w-28 [&::-webkit-inner-spin-button]:appearance-none");
|
||||
expect(editorSettingsDialogSource).toContain("settings-export-number-input h-9 w-32 [&::-webkit-inner-spin-button]:appearance-none");
|
||||
expect(editorSettingsDialogSource).not.toContain("\n.settings-export-number-input {");
|
||||
expect(fallback).toContain(".settings-layout .settings-export-number-input");
|
||||
expect(fallback).toContain("line-height: 1.25rem !important;");
|
||||
expect(fallback).toContain("-webkit-appearance: inner-spin-button !important;");
|
||||
expect(fallback).toContain("::-webkit-inner-spin-button");
|
||||
expect(editorSettingsDialogSource).toContain('class="settings-ai-back-button"');
|
||||
expect(fallback).toContain(".settings-ai-back-button");
|
||||
expect(fallback).toContain("margin-left: -0.625rem !important;");
|
||||
expect(editorSettingsDialogSource).toContain("settings-about-section-header flex flex-col gap-3");
|
||||
expect(editorSettingsDialogSource).toContain("settings-about-section-actions flex shrink-0 flex-wrap items-center gap-2");
|
||||
expect(changelogPanelSource).toContain("settings-about-section-header flex flex-col gap-3");
|
||||
expect(changelogPanelSource).toContain("settings-about-section-actions flex shrink-0 flex-wrap items-center gap-2");
|
||||
expect(fallback).toContain(".settings-about-section-header");
|
||||
expect(fallback).toContain("justify-content: space-between !important;");
|
||||
expect(fallback).toContain(".settings-about-section-actions");
|
||||
expect(fallback).toContain("margin-left: auto !important;");
|
||||
});
|
||||
|
||||
it("uses a runtime capability check instead of an OKLCH-only CSS proxy", () => {
|
||||
expect(legacyWebViewSource).toContain("color-mix");
|
||||
expect(legacyWebViewSource).toContain("has-selector");
|
||||
expect(legacyWebViewSource).toContain("dynamic-viewport");
|
||||
expect(legacyWebViewSource).toContain("min-function");
|
||||
expect(legacyWebViewSource).toContain("dbx-legacy-webview");
|
||||
expect(mainSource).toContain("applyLegacyWebViewClass();");
|
||||
});
|
||||
});
|
||||
|
||||
+783
-386
File diff suppressed because it is too large
Load Diff
@@ -105,6 +105,7 @@
|
||||
--popover: rgb(255 255 255);
|
||||
--popover-foreground: rgb(10 10 10);
|
||||
--primary: rgb(23 23 23);
|
||||
--dbx-primary-rgb: 23, 23, 23;
|
||||
--primary-foreground: rgb(250 250 250);
|
||||
--secondary: rgb(245 245 245);
|
||||
--secondary-foreground: rgb(23 23 23);
|
||||
@@ -166,6 +167,7 @@
|
||||
--popover: rgb(30 30 32);
|
||||
--popover-foreground: rgb(221 221 226);
|
||||
--primary: rgb(208 208 214);
|
||||
--dbx-primary-rgb: 208, 208, 214;
|
||||
--primary-foreground: rgb(19 20 22);
|
||||
--secondary: rgb(42 42 45);
|
||||
--secondary-foreground: rgb(215 215 219);
|
||||
|
||||
Generated
+1
-1
File diff suppressed because one or more lines are too long
@@ -38,7 +38,7 @@
|
||||
"apps/desktop/src/i18n/locales/docs/zh-TW.ts": "691cf502248dbd09358c6c276990945013b26c52fb682bcd56b26f378daea29c",
|
||||
"apps/desktop/src/lib/app/localeOptions.ts": "c269b1ca1d5c71dc27dd2ab5422a902c848722acabf0dbd205275624fc9482e6",
|
||||
"apps/desktop/src/lib/diagram/erDiagram.ts": "1b9522b41cd2ee15ef04dc11e0f855459287fca2d5e0f551263dfc6f8de5764e",
|
||||
"apps/desktop/src/styles/tokens.css": "5f050d28e3e278c259929e55ffc7bad63e5c3e8340ab6c04c018cbfd941c2b44",
|
||||
"apps/desktop/src/styles/tokens.css": "b8852ce50865ad98ebc992971f7a06670c5d830ca82efcee956f8d3eb7c2e9a3",
|
||||
"apps/desktop/tsconfig.json": "5ad521941835ad8529e9b81ba79c11650e5f020f0e376b7a9f17e22a9306fa76",
|
||||
"apps/desktop/vite.docs-export.config.ts": "ba99d16885a572aaccaab46fcd61a5c4ac0c3bea572ea92582fca64118f83d86"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# DBX 本地开发环境、启动与调试指南
|
||||
|
||||
本文档只记录仓库通用的准备和开发命令。版本与行为以仓库文件为准:
|
||||
|
||||
- Node.js:`.nvmrc`、`package.json#engines`
|
||||
- pnpm:`package.json#packageManager`
|
||||
- Rust:`Cargo.toml` 与 `.github/workflows/ci.yml`
|
||||
- 开发入口:`Makefile` 和 `package.json#scripts`
|
||||
|
||||
## 1. 项目结构
|
||||
|
||||
| 路径 | 用途 |
|
||||
| --- | --- |
|
||||
| `apps/desktop/src/` | Vue、TypeScript、Vite 前端 |
|
||||
| `apps/desktop/public/` | 前端静态资源 |
|
||||
| `src-tauri/` | Tauri 桌面壳层 |
|
||||
| `crates/` | Rust 工作区和数据库能力 |
|
||||
| `packages/` | CLI、MCP Server 等 Node 包 |
|
||||
| `agents/` | Java/JDBC Agent |
|
||||
| `scripts/` | 开发、构建和发布脚本 |
|
||||
|
||||
## 2. 准备环境
|
||||
|
||||
### 2.1 macOS 工具链
|
||||
|
||||
安装 Xcode Command Line Tools:
|
||||
|
||||
```bash
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
桌面端使用 Tauri 的系统 WebView。macOS 上的 WebView 版本由系统决定,升级系统或在目标系统上验证 UI 时,应同时检查亮色、暗色和主要弹窗。
|
||||
|
||||
### 2.2 Node.js 与 pnpm
|
||||
|
||||
```bash
|
||||
nvm use
|
||||
node --version
|
||||
corepack enable
|
||||
pnpm --version
|
||||
```
|
||||
|
||||
当前仓库要求 Node.js `22.13.0` 及以上,包管理器版本以 `package.json#packageManager` 为准。若本机没有 NVM,请安装满足 `.nvmrc` 的 Node.js,再启用 Corepack 或安装仓库声明的 pnpm 版本。
|
||||
|
||||
### 2.3 Rust
|
||||
|
||||
安装 rustup,并按照 CI 配置的工具链准备 Rust:
|
||||
|
||||
```bash
|
||||
rustup show
|
||||
rustc --version
|
||||
cargo --version
|
||||
rustfmt --version
|
||||
```
|
||||
|
||||
需要确认具体工具链时,查看 `.github/workflows/ci.yml` 中的 `dtolnay/rust-toolchain` 配置;不要依赖某台开发机的本地版本记录。
|
||||
|
||||
## 3. 安装依赖
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
|
||||
或使用 Makefile:
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
文档站点依赖单独安装:
|
||||
|
||||
```bash
|
||||
make docs-install
|
||||
```
|
||||
|
||||
## 4. 启动桌面端
|
||||
|
||||
标准 Tauri 开发模式:
|
||||
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
|
||||
轻量模式:
|
||||
|
||||
```bash
|
||||
make dev-fast
|
||||
```
|
||||
|
||||
如果希望隔离开发数据,可在启动前设置 `DBX_DATA_DIR`:
|
||||
|
||||
```bash
|
||||
DBX_DATA_DIR=/tmp/dbx-desktop-dev \
|
||||
RUST_BACKTRACE=1 \
|
||||
make dev
|
||||
```
|
||||
|
||||
Tauri 开发端口由 `Makefile` 的 `TAURI_DEV_PORT` 控制,默认是 `1420`。端口被占用时,可以使用其他端口并同步调整 Vite/Tauri 配置。
|
||||
|
||||
## 5. 启动 Web 版本
|
||||
|
||||
先启动 Rust Web 后端:
|
||||
|
||||
```bash
|
||||
DBX_DATA_DIR=/tmp/dbx-web-dev \
|
||||
RUST_LOG=dbx_web=debug,tower_http=info \
|
||||
RUST_BACKTRACE=1 \
|
||||
make dev-backend
|
||||
```
|
||||
|
||||
再启动前端:
|
||||
|
||||
```bash
|
||||
make dev-web
|
||||
```
|
||||
|
||||
前端脚本和后端地址以 `apps/desktop/vite.config.ts`、`scripts/dev-backend.mjs` 及相关环境变量为准。
|
||||
|
||||
## 6. 常用检查
|
||||
|
||||
前端类型检查和构建:
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
pnpm build
|
||||
```
|
||||
|
||||
仓库检查与测试:
|
||||
|
||||
```bash
|
||||
make check
|
||||
make test
|
||||
```
|
||||
|
||||
Rust 快速检查:
|
||||
|
||||
```bash
|
||||
make cargo-check-fast
|
||||
make cargo-test-fast
|
||||
```
|
||||
|
||||
只运行前端 Vitest:
|
||||
|
||||
```bash
|
||||
pnpm vitest run apps/desktop/src/components/ui/input/__tests__/Input.spec.ts
|
||||
pnpm vitest run apps/desktop/src/styles/__tests__/legacyWebviewFallback.spec.ts
|
||||
```
|
||||
|
||||
格式化和 lint:
|
||||
|
||||
```bash
|
||||
pnpm fmt
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
## 7. 数据库测试环境
|
||||
|
||||
查看可用环境:
|
||||
|
||||
```bash
|
||||
make db-list
|
||||
```
|
||||
|
||||
启动一个环境:
|
||||
|
||||
```bash
|
||||
make db DB=mysql@8.4
|
||||
```
|
||||
|
||||
验证、停止和重置:
|
||||
|
||||
```bash
|
||||
make db-verify DB=mysql@8.4
|
||||
make db-down DB=mysql@8.4
|
||||
make db-reset DB=mysql@8.4 CONFIRM=1
|
||||
```
|
||||
|
||||
这些命令依赖 Docker;具体数据库版本和 Compose 配置由 `scripts/` 与数据库环境文件维护。
|
||||
|
||||
## 8. 调试建议
|
||||
|
||||
- 修改 Vue、TypeScript 或 CSS 后优先使用 Vite HMR;跨平台问题再运行 Tauri 桌面模式。
|
||||
- 前端启动失败时查看终端中的 `[STARTUP]` 日志,并保留完整错误堆栈。
|
||||
- UI 兼容性问题应在目标 WebView 上复现,至少覆盖亮色、暗色、窄窗口和主要弹窗。
|
||||
- 提交前运行相关 Vitest、`pnpm typecheck` 和 `git diff --check`。
|
||||
- 提交前只保留可复现的仓库信息,临时环境值不要写入项目文档。
|
||||
Reference in New Issue
Block a user