From a83925d6bad0abe00616c4c342f19bead3afe700 Mon Sep 17 00:00:00 2001 From: zipg Date: Fri, 31 Jul 2026 14:38:40 +0800 Subject: [PATCH] fix(grid): make data toolbar compact mode responsive --- apps/desktop/src/components/grid/DataGrid.vue | 23 ++++++++++++------- .../src/components/layout/ContentArea.vue | 22 +++++++++++++----- .../dataGrid/dataGridToolbar.spec.ts | 22 ++++++++++++++++++ .../src/lib/dataGrid/dataGridToolbar.ts | 15 +++++++++++- packages/app-tests/queryResultToolbar.test.ts | 4 ++-- 5 files changed, 69 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index c2001fbeb8..820ed68c21 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -207,7 +207,7 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { simpleDataGridOrderByMatchesSort, simpleDataGridOrderByReferencesMissingColumn, type DataGridSortDirection, type DataGridSortMode } from "@/lib/dataGrid/dataGridSort"; -import { DATA_GRID_COMPACT_TOPBAR_WIDTH, type DataGridReloadIntent, type DataGridToolbarActionCapability, type DataGridToolbarAutoRefreshCapability, type DataGridToolbarSaveCapability } from "@/lib/dataGrid/dataGridToolbar"; +import { DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH, isDataGridToolbarCompact, type DataGridReloadIntent, type DataGridToolbarActionCapability, type DataGridToolbarAutoRefreshCapability, type DataGridToolbarSaveCapability } from "@/lib/dataGrid/dataGridToolbar"; import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities"; import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities"; import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableInfo"; @@ -458,12 +458,13 @@ const columnCommentMap = computed(() => { return map; }); const dataGridTopbarWidth = ref(0); +const dataGridViewportWidth = ref(0); const showColumnCommentsInHeader = computed(() => settingsStore.editorSettings.showColumnCommentsInHeader); const showColumnTypesInHeader = computed(() => settingsStore.editorSettings.showColumnTypesInHeader); const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.compactColumnHeaderActions); const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode); const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode); -const compactDataGridToolbar = computed(() => dataGridTopbarWidth.value > 0 && dataGridTopbarWidth.value < DATA_GRID_COMPACT_TOPBAR_WIDTH); +const compactDataGridToolbar = computed(() => isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)); const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll); const infiniteScrollMaxRows = computed(() => settingsStore.editorSettings.infiniteScrollMaxRows); const expandedCellEditor = ref<{ rowId: number; col: number } | null>(null); @@ -2068,6 +2069,7 @@ function observeGridHorizontalScrollbarScroller() { function updateDataGridTopbarWidth() { dataGridTopbarWidth.value = dataGridTopbarRef.value?.clientWidth ?? 0; + dataGridViewportWidth.value = typeof window === "undefined" ? 0 : window.innerWidth; } function observeDataGridTopbarWidth() { @@ -4603,6 +4605,11 @@ function scheduleCanvasPixelRatioRefresh() { canvasRuntime.schedulePixelRatioRefresh(); } +function refreshDataGridViewportMetrics() { + scheduleCanvasPixelRatioRefresh(); + updateDataGridTopbarWidth(); +} + function attachCanvasPixelRatioWatcher() { canvasPixelRatioMediaQueryCleanup?.(); canvasPixelRatioMediaQueryCleanup = null; @@ -5131,9 +5138,9 @@ onMounted(resumeCanvasGridWork); onActivated(resumeCanvasGridWork); onMounted(() => { if (typeof window === "undefined") return; - window.addEventListener("resize", scheduleCanvasPixelRatioRefresh); - window.visualViewport?.addEventListener("resize", scheduleCanvasPixelRatioRefresh); - window.addEventListener("dbx:ui-scale-applied", scheduleCanvasPixelRatioRefresh); + window.addEventListener("resize", refreshDataGridViewportMetrics); + window.visualViewport?.addEventListener("resize", refreshDataGridViewportMetrics); + window.addEventListener("dbx:ui-scale-applied", refreshDataGridViewportMetrics); window.addEventListener(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, onTableDataGridColumnOrderChanged); window.addEventListener("blur", clearInternalClipboardCopy); document.addEventListener("visibilitychange", clearInternalClipboardCopy); @@ -5151,9 +5158,9 @@ onUnmounted(() => { if (columnLayoutRefreshFrame) cancelAnimationFrame(columnLayoutRefreshFrame); columnLayoutRefreshFrame = 0; if (typeof window === "undefined") return; - window.removeEventListener("resize", scheduleCanvasPixelRatioRefresh); - window.visualViewport?.removeEventListener("resize", scheduleCanvasPixelRatioRefresh); - window.removeEventListener("dbx:ui-scale-applied", scheduleCanvasPixelRatioRefresh); + window.removeEventListener("resize", refreshDataGridViewportMetrics); + window.visualViewport?.removeEventListener("resize", refreshDataGridViewportMetrics); + window.removeEventListener("dbx:ui-scale-applied", refreshDataGridViewportMetrics); window.removeEventListener(TABLE_DATA_GRID_COLUMN_ORDER_CHANGED_EVENT, onTableDataGridColumnOrderChanged); window.removeEventListener("blur", clearInternalClipboardCopy); document.removeEventListener("visibilitychange", clearInternalClipboardCopy); diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 5a59163902..87d735b687 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -89,7 +89,7 @@ import * as api from "@/lib/backend/api"; import { applyMongoGridChangesToDocument, applyMongoGridChangesToDocumentBaseline, buildMongoUpdateDocument, formatMongoShellLiteral, serializeMongoDocumentId, type MongoInputValue } from "@/lib/mongo/mongoDocumentValues"; import type { SqlExecutionOverride } from "@/lib/sql/sqlExecutionTarget"; import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort"; -import { DATA_GRID_COMPACT_TOPBAR_WIDTH, type DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar"; +import { isDataGridToolbarCompact, type DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar"; import { useTabScroll } from "@/composables/useTabScroll"; import { formatElapsedSeconds } from "@/lib/common/elapsedTime"; import type { CustomSaveHandler } from "@/composables/useDataGridEditor"; @@ -188,6 +188,9 @@ onMounted(() => { setTimeout(preload, 300); } window.addEventListener("dbx-refresh-active-kv-browser", onRefreshActiveKvBrowser); + window.addEventListener("resize", updateStandaloneResultToolbarDimensions); + window.visualViewport?.addEventListener("resize", updateStandaloneResultToolbarDimensions); + window.addEventListener("dbx:ui-scale-applied", updateStandaloneResultToolbarDimensions); }); watch( @@ -208,6 +211,7 @@ const queryEditorRef = ref>(); const tableStructureEditorRef = ref<{ applyChanges: () => Promise }>(); const standaloneResultToolbarRef = ref(null); const standaloneResultToolbarWidth = ref(0); +const standaloneResultToolbarViewportWidth = ref(0); const resultTabsScrollerRef = ref(null); const dataGridViewOptionsOpen = ref(false); const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode); @@ -417,18 +421,21 @@ const hasTabularResult = computed(() => { const canShowResultOutput = computed(() => hasTabularResult.value || props.activeTab.isExecuting); const canShowExplainOutput = computed(() => !!props.activeTab.explainPlan || !!props.activeTab.explainError || !!props.activeTab.explainTableResult || !!props.activeTab.explainTableError || props.activeTab.isExplaining === true); const showStandaloneResultToolbar = computed(() => activeElasticsearchJsonResponse.value || props.activeOutputView !== "result" || !props.activeTab.result || !hasTabularResult.value); -const standaloneResultToolbarCompact = computed(() => standaloneResultToolbarWidth.value > 0 && standaloneResultToolbarWidth.value < DATA_GRID_COMPACT_TOPBAR_WIDTH); +const standaloneResultToolbarCompact = computed(() => isDataGridToolbarCompact(standaloneResultToolbarWidth.value, standaloneResultToolbarViewportWidth.value)); let standaloneResultToolbarResizeObserver: ResizeObserver | undefined; +function updateStandaloneResultToolbarDimensions() { + standaloneResultToolbarWidth.value = standaloneResultToolbarRef.value?.clientWidth ?? 0; + standaloneResultToolbarViewportWidth.value = typeof window === "undefined" ? 0 : window.innerWidth; +} + function observeStandaloneResultToolbar() { standaloneResultToolbarResizeObserver?.disconnect(); standaloneResultToolbarResizeObserver = undefined; const toolbar = standaloneResultToolbarRef.value; - standaloneResultToolbarWidth.value = toolbar?.clientWidth ?? 0; + updateStandaloneResultToolbarDimensions(); if (toolbar && typeof ResizeObserver !== "undefined") { - standaloneResultToolbarResizeObserver = new ResizeObserver(() => { - standaloneResultToolbarWidth.value = toolbar.clientWidth; - }); + standaloneResultToolbarResizeObserver = new ResizeObserver(updateStandaloneResultToolbarDimensions); standaloneResultToolbarResizeObserver.observe(toolbar); } } @@ -560,6 +567,9 @@ onUnmounted(() => { stopQueryRunningElapsedTimer(); standaloneResultToolbarResizeObserver?.disconnect(); window.removeEventListener("dbx-refresh-active-kv-browser", onRefreshActiveKvBrowser); + window.removeEventListener("resize", updateStandaloneResultToolbarDimensions); + window.visualViewport?.removeEventListener("resize", updateStandaloneResultToolbarDimensions); + window.removeEventListener("dbx:ui-scale-applied", updateStandaloneResultToolbarDimensions); }); watch( diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridToolbar.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridToolbar.spec.ts index 9dca34d561..7781d9c119 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridToolbar.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridToolbar.spec.ts @@ -1,6 +1,9 @@ import { describe, expect, it, vi } from "vitest"; import { + DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH, + dataGridToolbarCompactBreakpoint, dataGridToolbarIntervalOptions, + isDataGridToolbarCompact, selectDataGridToolbarAutoRefreshInterval, selectDataGridToolbarCopyItem, selectDataGridToolbarExportItem, @@ -28,6 +31,25 @@ function autoRefreshCapability(overrides: Partial { + it("uses a viewport-relative compact breakpoint within stable bounds", () => { + expect(dataGridToolbarCompactBreakpoint(1920)).toBe(1050); + expect(dataGridToolbarCompactBreakpoint(1280)).toBe(960); + expect(dataGridToolbarCompactBreakpoint(1080)).toBe(900); + }); + + it("keeps a scaled 1080p workspace expanded while compacting a narrower pane", () => { + expect(isDataGridToolbarCompact(1040, 1920)).toBe(true); + expect(isDataGridToolbarCompact(1040, 1280)).toBe(false); + expect(isDataGridToolbarCompact(900, 1280)).toBe(true); + expect(isDataGridToolbarCompact(0, 1280)).toBe(false); + }); + + it("preserves condition input space in embedded data grids", () => { + expect(dataGridToolbarCompactBreakpoint(1100, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(1050); + expect(isDataGridToolbarCompact(1000, 1100, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)).toBe(true); + expect(isDataGridToolbarCompact(1000, 1100)).toBe(false); + }); + it("does not invoke hidden or disabled actions", async () => { const onTrigger = vi.fn(); diff --git a/apps/desktop/src/lib/dataGrid/dataGridToolbar.ts b/apps/desktop/src/lib/dataGrid/dataGridToolbar.ts index 192cde8d85..7671b7df50 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridToolbar.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridToolbar.ts @@ -1,4 +1,17 @@ -export const DATA_GRID_COMPACT_TOPBAR_WIDTH = 1050; +const DATA_GRID_COMPACT_TOPBAR_MIN_WIDTH = 900; +const DATA_GRID_COMPACT_TOPBAR_MAX_WIDTH = 1050; +const DATA_GRID_COMPACT_TOPBAR_VIEWPORT_RATIO = 0.75; +export const DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH = DATA_GRID_COMPACT_TOPBAR_MAX_WIDTH; + +export function dataGridToolbarCompactBreakpoint(viewportWidth: number, minimumWidth = DATA_GRID_COMPACT_TOPBAR_MIN_WIDTH): number { + const normalizedViewportWidth = Number.isFinite(viewportWidth) ? Math.max(0, viewportWidth) : DATA_GRID_COMPACT_TOPBAR_MAX_WIDTH; + const normalizedMinimumWidth = Math.min(DATA_GRID_COMPACT_TOPBAR_MAX_WIDTH, Math.max(DATA_GRID_COMPACT_TOPBAR_MIN_WIDTH, minimumWidth)); + return Math.min(DATA_GRID_COMPACT_TOPBAR_MAX_WIDTH, Math.max(normalizedMinimumWidth, normalizedViewportWidth * DATA_GRID_COMPACT_TOPBAR_VIEWPORT_RATIO)); +} + +export function isDataGridToolbarCompact(toolbarWidth: number, viewportWidth: number, minimumWidth?: number): boolean { + return toolbarWidth > 0 && toolbarWidth < dataGridToolbarCompactBreakpoint(viewportWidth, minimumWidth); +} export type DataGridReloadIntent = "refresh"; diff --git a/packages/app-tests/queryResultToolbar.test.ts b/packages/app-tests/queryResultToolbar.test.ts index a453649596..08da6d50e6 100644 --- a/packages/app-tests/queryResultToolbar.test.ts +++ b/packages/app-tests/queryResultToolbar.test.ts @@ -100,9 +100,9 @@ test("standalone result views use the same compact toolbar breakpoint", () => { const dataGrid = source(dataGridPath); assert.match(contentArea, /ref="standaloneResultToolbarRef"/); - assert.match(contentArea, /standaloneResultToolbarWidth\.value < DATA_GRID_COMPACT_TOPBAR_WIDTH/); + assert.match(contentArea, /isDataGridToolbarCompact\(standaloneResultToolbarWidth\.value, standaloneResultToolbarViewportWidth\.value\)/); assert.equal((contentArea.match(/:compact="standaloneResultToolbarCompact"/g) ?? []).length, 2); - assert.match(dataGrid, /dataGridTopbarWidth\.value < DATA_GRID_COMPACT_TOPBAR_WIDTH/); + assert.match(dataGrid, /isDataGridToolbarCompact\(dataGridTopbarWidth\.value, dataGridViewportWidth\.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH\)/); }); test("embedded and standalone result toolbars share the same fixed height", () => {