diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index cc7d37b8a5..0062f64c33 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -701,6 +701,8 @@ const columnCommentMap = computed(() => { }); const dataGridTopbarWidth = ref(0); const dataGridViewportWidth = ref(0); +const dataGridTopbarOverflowCompact = ref(false); +const dataGridTopbarExpandedRequiredWidth = ref(0); const showColumnCommentsInHeader = computed(() => settingsStore.editorSettings.showColumnCommentsInHeader); const showColumnTypesInHeader = computed(() => settingsStore.editorSettings.showColumnTypesInHeader); const showTransposeFieldMetadata = computed(() => settingsStore.editorSettings.dataGridShowTransposeFieldMetadata); @@ -720,7 +722,7 @@ const columnIndexMap = computed(() => buildColumnIndexMap(indexes.value, primary const compactColumnHeaderActions = computed(() => settingsStore.editorSettings.compactColumnHeaderActions); const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridRenderMode); const dataGridSearchMode = computed(() => settingsStore.editorSettings.dataGridSearchMode); -const compactDataGridToolbar = computed(() => isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)); +const compactDataGridToolbar = computed(() => dataGridTopbarOverflowCompact.value || isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH)); const infiniteScrollEnabled = computed(() => props.paginationEnabled && settingsStore.editorSettings.infiniteScroll); const queryResultMaxRows = computed(() => effectiveQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows)); const paginationMaxRows = computed(() => (isResultsContext.value ? queryResultMaxRows.value : undefined)); @@ -2535,6 +2537,8 @@ let gridVerticalScrollbarThumbTopPercent = 0; let gridVerticalScrollbarThumbHeightPercent = 100; let gridScrollbarsRuntime: DataGridScrollbarsRuntime; let dataGridTopbarResizeObserver: ResizeObserver | null = null; +let dataGridTopbarMutationObserver: MutationObserver | null = null; +let dataGridTopbarRecheckTimer: ReturnType | null = null; let cellEditResizeObserver: ResizeObserver | null = null; // vue-virtual-scroller's @resize only fires in pageMode (it observes window), // so in container-scroll mode (transpose uses RecycleScroller without pageMode) @@ -3183,19 +3187,68 @@ function observeGridHorizontalScrollbarScroller() { } function updateDataGridTopbarWidth() { - dataGridTopbarWidth.value = dataGridTopbarRef.value?.clientWidth ?? 0; + const topbar = dataGridTopbarRef.value; + dataGridTopbarWidth.value = topbar?.clientWidth ?? 0; dataGridViewportWidth.value = typeof window === "undefined" ? 0 : window.innerWidth; + + if (!topbar) return; + + const fixedBreakpointCompact = isDataGridToolbarCompact(dataGridTopbarWidth.value, dataGridViewportWidth.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH); + if (fixedBreakpointCompact) return; + + if (dataGridTopbarOverflowCompact.value) { + if (dataGridTopbarWidth.value >= dataGridTopbarExpandedRequiredWidth.value) { + dataGridTopbarOverflowCompact.value = false; + dataGridTopbarExpandedRequiredWidth.value = 0; + } + return; + } + + // The action list gains preview/save/rollback controls in editable grids. The + // fixed breakpoint cannot see that extra width, so capture the full layout + // width before the overflow-clip wrapper can hide the trailing action. + if (topbar.scrollWidth > topbar.clientWidth + 1) { + dataGridTopbarExpandedRequiredWidth.value = topbar.scrollWidth; + dataGridTopbarOverflowCompact.value = true; + } +} + +function resetDataGridTopbarOverflowCompact() { + if (!dataGridTopbarOverflowCompact.value) return; + dataGridTopbarOverflowCompact.value = false; + dataGridTopbarExpandedRequiredWidth.value = 0; + if (dataGridTopbarRecheckTimer) clearTimeout(dataGridTopbarRecheckTimer); + // Wait for the label-width transition to finish before measuring the newly + // shortened action list. If it still overflows, updateDataGridTopbarWidth + // captures its new, smaller expanded width. + dataGridTopbarRecheckTimer = setTimeout(() => { + dataGridTopbarRecheckTimer = null; + updateDataGridTopbarWidth(); + }, 360); +} + +function clearDataGridTopbarRecheckTimer() { + if (!dataGridTopbarRecheckTimer) return; + clearTimeout(dataGridTopbarRecheckTimer); + dataGridTopbarRecheckTimer = null; } function observeDataGridTopbarWidth() { dataGridTopbarResizeObserver?.disconnect(); dataGridTopbarResizeObserver = null; + dataGridTopbarMutationObserver?.disconnect(); + dataGridTopbarMutationObserver = null; + clearDataGridTopbarRecheckTimer(); const topbar = dataGridTopbarRef.value; updateDataGridTopbarWidth(); if (topbar && typeof ResizeObserver !== "undefined") { dataGridTopbarResizeObserver = new ResizeObserver(updateDataGridTopbarWidth); dataGridTopbarResizeObserver.observe(topbar); } + if (topbar && typeof MutationObserver !== "undefined") { + dataGridTopbarMutationObserver = new MutationObserver(updateDataGridTopbarWidth); + dataGridTopbarMutationObserver.observe(topbar, { childList: true, characterData: true, subtree: true }); + } } function applyPendingGridHorizontalScrollbarDrag(clientX: number) { @@ -4843,6 +4896,21 @@ const previewToolbarCapability = computed(() => loading: isPreviewLoading.value, onTrigger: openSqlPreview, })); +watch( + () => previewToolbarCapability.value.visible, + (visible, previousVisible) => { + if (previousVisible && !visible) resetDataGridTopbarOverflowCompact(); + }, +); +// Save/rollback disappear with saveToolbarState.showActions (committing or +// discarding all pending edits), which shortens the action list while compact +// just like the preview action disappearing does. +watch( + () => saveToolbarState.value.showActions, + (showActions, previousShowActions) => { + if (previousShowActions && !showActions) resetDataGridTopbarOverflowCompact(); + }, +); const layerPreviewToolbarCapability = computed(() => { const action = previewActions.value.find((candidate) => candidate.id === "geometry-map-preview"); return { @@ -7946,6 +8014,9 @@ function pauseCanvasGridWork() { disconnectCellEditResizeObserver(); dataGridTopbarResizeObserver?.disconnect(); dataGridTopbarResizeObserver = null; + dataGridTopbarMutationObserver?.disconnect(); + dataGridTopbarMutationObserver = null; + clearDataGridTopbarRecheckTimer(); disconnectTransposeViewportObserver(); canvasPixelRatioMediaQueryCleanup?.(); canvasPixelRatioMediaQueryCleanup = null; @@ -8016,6 +8087,8 @@ onUnmounted(() => { canvasRuntime.dispose(); gridScrollbarsRuntime.dispose(); dataGridTopbarResizeObserver?.disconnect(); + dataGridTopbarMutationObserver?.disconnect(); + clearDataGridTopbarRecheckTimer(); disconnectCellEditResizeObserver(); disconnectTransposeViewportObserver(); stopGridHorizontalScrollbarDrag(); diff --git a/packages/app-tests/queryResultToolbar.test.ts b/packages/app-tests/queryResultToolbar.test.ts index ab9a2cdd26..350f8f70b3 100644 --- a/packages/app-tests/queryResultToolbar.test.ts +++ b/packages/app-tests/queryResultToolbar.test.ts @@ -169,6 +169,32 @@ test("standalone result views use the same compact toolbar breakpoint", () => { assert.match(dataGrid, /isDataGridToolbarCompact\(dataGridTopbarWidth\.value, dataGridViewportWidth\.value, DATA_GRID_CONDITION_TOOLBAR_MIN_WIDTH\)/); }); +test("embedded result toolbar compacts when editing actions overflow its available width", () => { + const dataGrid = source(dataGridPath); + + assert.match(dataGrid, /dataGridTopbarMutationObserver = new MutationObserver\(updateDataGridTopbarWidth\)/); + assert.match(dataGrid, /dataGridTopbarMutationObserver\.observe\(topbar, \{ childList: true, characterData: true, subtree: true \}\)/); + assert.match(dataGrid, /topbar\.scrollWidth > topbar\.clientWidth \+ 1/); + assert.match(dataGrid, /dataGridTopbarOverflowCompact\.value = true/); + assert.match(dataGrid, /dataGridTopbarWidth\.value >= dataGridTopbarExpandedRequiredWidth\.value/); +}); + +test("embedded result toolbar remeasures after the SQL preview action is removed", () => { + const dataGrid = source(dataGridPath); + + assert.match(dataGrid, /if \(previousVisible && !visible\) resetDataGridTopbarOverflowCompact\(\)/); + assert.match(dataGrid, /dataGridTopbarOverflowCompact\.value = false/); + assert.match(dataGrid, /dataGridTopbarRecheckTimer = setTimeout\(/); + assert.match(dataGrid, /clearDataGridTopbarRecheckTimer\(\)/); +}); + +test("embedded result toolbar remeasures after save and rollback actions are removed", () => { + const dataGrid = source(dataGridPath); + + assert.match(dataGrid, /\(\) => saveToolbarState\.value\.showActions,/); + assert.match(dataGrid, /if \(previousShowActions && !showActions\) resetDataGridTopbarOverflowCompact\(\)/); +}); + test("embedded and standalone result toolbars share the same fixed height", () => { const contentArea = source(contentAreaPath); const dataGrid = source(dataGridPath);