fix(editor): let mouse hide/re-show the shared query results pane
This commit is contained in:
1 parent
b8d4388caf
commit
73c167bed3
5 files changed
+85
-10
No files matched your search
@@ -664,6 +664,21 @@ function toggleResultsPane(): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "hide results" chevron. In the shared result surface (result-only) the
|
||||||
|
* local resultsPaneOpen ref is inert — the pane is always rendered there and
|
||||||
|
* the real collapse state lives in SqlEditorWorkspace.showResultPane — so the
|
||||||
|
* chevron bubbles a toggle event up instead. Per-tab surfaces (data tabs)
|
||||||
|
* keep the local collapse behavior.
|
||||||
|
*/
|
||||||
|
function handleHideResultsPane() {
|
||||||
|
if (props.resultOnly) {
|
||||||
|
emit("toggleResultsPane");
|
||||||
|
} else {
|
||||||
|
resultsPaneOpen.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onResultsResized(payload: { panes: { size: number }[] }) {
|
function onResultsResized(payload: { panes: { size: number }[] }) {
|
||||||
const resultsPane = payload.panes[1];
|
const resultsPane = payload.panes[1];
|
||||||
if (resultsPane?.size != null && resultsPane.size >= 20 && resultsPane.size <= 85) {
|
if (resultsPane?.size != null && resultsPane.size >= 20 && resultsPane.size <= 85) {
|
||||||
@@ -1664,7 +1679,7 @@ defineExpose({
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<LightTooltip :text="t('editor.hideResultsPane')" side="bottom" :delay="0" :close-delay="0" nowrap>
|
<LightTooltip :text="t('editor.hideResultsPane')" side="bottom" :delay="0" :close-delay="0" nowrap>
|
||||||
<Button variant="ghost" size="icon" class="h-6 w-7 shrink-0 text-muted-foreground hover:text-foreground" :title="t('editor.hideResultsPane')" :aria-label="t('editor.hideResultsPane')" @click="resultsPaneOpen = false">
|
<Button variant="ghost" size="icon" class="h-6 w-7 shrink-0 text-muted-foreground hover:text-foreground" :title="t('editor.hideResultsPane')" :aria-label="t('editor.hideResultsPane')" @click="handleHideResultsPane">
|
||||||
<ChevronDown class="h-3.5 w-3.5" />
|
<ChevronDown class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</LightTooltip>
|
</LightTooltip>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onMounted, ref, useAttrs } from "vue";
|
import { computed, nextTick, onMounted, ref, useAttrs, watch } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import { Splitpanes, Pane } from "splitpanes";
|
import { Splitpanes, Pane } from "splitpanes";
|
||||||
import "splitpanes/dist/splitpanes.css";
|
import "splitpanes/dist/splitpanes.css";
|
||||||
import "./sqlEditorWorkspace.css";
|
import "./sqlEditorWorkspace.css";
|
||||||
import { useQueryStore } from "@/stores/queryStore";
|
import { useQueryStore } from "@/stores/queryStore";
|
||||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import EditorGroup from "./EditorGroup.vue";
|
import EditorGroup from "./EditorGroup.vue";
|
||||||
import QueryResultSurface from "./QueryResultSurface.vue";
|
import QueryResultSurface from "./QueryResultSurface.vue";
|
||||||
import { createContentSurfaceEventForwarders } from "@/lib/tabs/contentSurfaceEvents";
|
import { createContentSurfaceEventForwarders } from "@/lib/tabs/contentSurfaceEvents";
|
||||||
@@ -71,13 +73,7 @@ defineExpose({
|
|||||||
const group = groupForElement(element) ?? activeEditorGroup();
|
const group = groupForElement(element) ?? activeEditorGroup();
|
||||||
return group?.refreshData() ?? resultSurfaceRef.value?.refreshData() ?? false;
|
return group?.refreshData() ?? resultSurfaceRef.value?.refreshData() ?? false;
|
||||||
},
|
},
|
||||||
toggleResultsPane: () => {
|
toggleResultsPane: () => toggleSharedResultsPane(),
|
||||||
if (!showSharedResult.value) {
|
|
||||||
return activeEditorGroup()?.toggleResultsPane() ?? false;
|
|
||||||
}
|
|
||||||
showResultPane.value = !showResultPane.value;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
refreshQueryEditorCompletionCache: () => activeEditorGroup()?.refreshQueryEditorCompletionCache() ?? false,
|
refreshQueryEditorCompletionCache: () => activeEditorGroup()?.refreshQueryEditorCompletionCache() ?? false,
|
||||||
handleModRTarget: (target: Element) => {
|
handleModRTarget: (target: Element) => {
|
||||||
// DataGrid, Elasticsearch JSON, and the cell-detail editor (Teleported
|
// DataGrid, Elasticsearch JSON, and the cell-detail editor (Teleported
|
||||||
@@ -107,6 +103,7 @@ defineExpose({
|
|||||||
executeRedisCommand: (command: string) => (groupForElement(commandTargetElement(null)) ?? activeEditorGroup())?.executeRedisCommand(command) ?? Promise.resolve(false),
|
executeRedisCommand: (command: string) => (groupForElement(commandTargetElement(null)) ?? activeEditorGroup())?.executeRedisCommand(command) ?? Promise.resolve(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const queryStore = useQueryStore();
|
const queryStore = useQueryStore();
|
||||||
const activeTab = computed(() => queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId));
|
const activeTab = computed(() => queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId));
|
||||||
const showSharedResult = computed(() => activeTab.value?.mode === "query");
|
const showSharedResult = computed(() => activeTab.value?.mode === "query");
|
||||||
@@ -124,6 +121,31 @@ const showResultPane = ref(true);
|
|||||||
// (globals.css kills it for horizontal splitpanes).
|
// (globals.css kills it for horizontal splitpanes).
|
||||||
const resultPaneTargetSize = computed(() => (showSharedResult.value && showResultPane.value ? resultPaneSize.value : 0));
|
const resultPaneTargetSize = computed(() => (showSharedResult.value && showResultPane.value ? resultPaneSize.value : 0));
|
||||||
const editorPaneSize = computed(() => 100 - resultPaneTargetSize.value);
|
const editorPaneSize = computed(() => 100 - resultPaneTargetSize.value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single toggle entry point for the shared result pane. Reached from the
|
||||||
|
* keyboard shortcut (exposed toggleResultsPane) and from the shared surface's
|
||||||
|
* "hide results" chevron, which bubbles a toggleResultsPane event up through
|
||||||
|
* ContentArea → QueryResultSurface. Non-query tabs (data etc.) render a plain
|
||||||
|
* ContentArea and keep their own per-tab resultsPaneOpen collapse behavior.
|
||||||
|
*/
|
||||||
|
function toggleSharedResultsPane(): boolean {
|
||||||
|
if (!showSharedResult.value) {
|
||||||
|
return activeEditorGroup()?.toggleResultsPane() ?? false;
|
||||||
|
}
|
||||||
|
showResultPane.value = !showResultPane.value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restores the pre-split "running a query re-expands the results pane"
|
||||||
|
// behavior (see issue #6193): a collapsed shared pane comes back when the
|
||||||
|
// active tab starts executing.
|
||||||
|
watch(
|
||||||
|
() => activeTab.value?.isExecuting,
|
||||||
|
(isExecuting) => {
|
||||||
|
if (isExecuting) showResultPane.value = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
// Entrance choreography is hydration-gated: groups present at first render
|
// Entrance choreography is hydration-gated: groups present at first render
|
||||||
// never animate (no load choreography); only groups created later — by a
|
// never animate (no load choreography); only groups created later — by a
|
||||||
// split — materialize from their owner's side of the divider.
|
// split — materialize from their owner's side of the divider.
|
||||||
@@ -195,7 +217,7 @@ function handleFocusStatement(tabId: string, range: StatementRange | null): bool
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="sql-editor-workspace flex h-full min-h-0 flex-1 flex-col overflow-hidden" :class="workspaceClass">
|
<div class="sql-editor-workspace relative flex h-full min-h-0 flex-1 flex-col overflow-hidden" :class="workspaceClass">
|
||||||
<Splitpanes horizontal class="sql-editor-workspace-split flex-1 min-h-0" :class="{ 'result-pane-collapsed': resultPaneTargetSize === 0 }" @resized="onSharedResultResized">
|
<Splitpanes horizontal class="sql-editor-workspace-split flex-1 min-h-0" :class="{ 'result-pane-collapsed': resultPaneTargetSize === 0 }" @resized="onSharedResultResized">
|
||||||
<Pane class="min-h-0 min-w-0" :size="editorPaneSize" :min-size="100 - SHARED_RESULT_PANE_MAX_SIZE">
|
<Pane class="min-h-0 min-w-0" :size="editorPaneSize" :min-size="100 - SHARED_RESULT_PANE_MAX_SIZE">
|
||||||
<Splitpanes
|
<Splitpanes
|
||||||
@@ -237,6 +259,7 @@ function handleFocusStatement(tabId: string, range: StatementRange | null): bool
|
|||||||
ref="resultSurfaceRef"
|
ref="resultSurfaceRef"
|
||||||
v-bind="resultSurfaceBindings"
|
v-bind="resultSurfaceBindings"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
|
@toggle-results-pane="toggleSharedResultsPane"
|
||||||
@preview-statement="
|
@preview-statement="
|
||||||
(tabId: string, range: { from: number; to: number } | null) => {
|
(tabId: string, range: { from: number; to: number } | null) => {
|
||||||
handlePreviewStatement(tabId, range);
|
handlePreviewStatement(tabId, range);
|
||||||
@@ -252,5 +275,19 @@ function handleFocusStatement(tabId: string, range: StatementRange | null): bool
|
|||||||
</Transition>
|
</Transition>
|
||||||
</Pane>
|
</Pane>
|
||||||
</Splitpanes>
|
</Splitpanes>
|
||||||
|
<!-- The shared surface unmounts when collapsed, so the mouse re-show
|
||||||
|
affordance lives here, outside the collapsible pane. -->
|
||||||
|
<Button
|
||||||
|
v-if="showSharedResult && !showResultPane"
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
class="absolute bottom-3 right-3 z-20 h-7 gap-1.5 rounded-full border bg-background/95 px-3 text-xs shadow-lg hover:bg-accent"
|
||||||
|
:title="t('editor.showResultsPane')"
|
||||||
|
:aria-label="t('editor.showResultsPane')"
|
||||||
|
@click="showResultPane = true"
|
||||||
|
>
|
||||||
|
{{ t("editor.showResultsPane") }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -9,6 +9,7 @@ const editorSurfaceSource = readFileSync(new URL("../QueryEditorSurface.vue", im
|
|||||||
const resultSurfaceSource = readFileSync(new URL("../QueryResultSurface.vue", import.meta.url), "utf8");
|
const resultSurfaceSource = readFileSync(new URL("../QueryResultSurface.vue", import.meta.url), "utf8");
|
||||||
const contentAreaSource = readFileSync(new URL("../ContentArea.vue", import.meta.url), "utf8");
|
const contentAreaSource = readFileSync(new URL("../ContentArea.vue", import.meta.url), "utf8");
|
||||||
const surfaceContractSource = readFileSync(new URL("../querySurfaces.ts", import.meta.url), "utf8");
|
const surfaceContractSource = readFileSync(new URL("../querySurfaces.ts", import.meta.url), "utf8");
|
||||||
|
const surfaceEventsSource = readFileSync(new URL("../../../lib/tabs/contentSurfaceEvents.ts", import.meta.url), "utf8");
|
||||||
|
|
||||||
describe("SQL editor workspace single-group tracer bullet", () => {
|
describe("SQL editor workspace single-group tracer bullet", () => {
|
||||||
it("composes splitpanes editor groups and one shared result surface", () => {
|
it("composes splitpanes editor groups and one shared result surface", () => {
|
||||||
@@ -81,6 +82,26 @@ describe("SQL editor workspace single-group tracer bullet", () => {
|
|||||||
expect(resultSurfaceSource).toContain('v-bind="bindings"');
|
expect(resultSurfaceSource).toContain('v-bind="bindings"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("lets the mouse hide and re-show the shared query results pane", () => {
|
||||||
|
// Regression (#8076 refactor): the shared result surface's "hide results"
|
||||||
|
// chevron wrote the inert per-instance resultsPaneOpen ref, so query-tab
|
||||||
|
// results could no longer be collapsed by mouse — the pane was pinned
|
||||||
|
// open. The chevron must bubble a toggleResultsPane event up through the
|
||||||
|
// surface contract to the workspace, which owns the real collapse state.
|
||||||
|
expect(surfaceContractSource).toContain("toggleResultsPane: []");
|
||||||
|
expect(surfaceEventsSource).toContain('"toggleResultsPane",');
|
||||||
|
expect(contentAreaSource).toContain('emit("toggleResultsPane")');
|
||||||
|
expect(contentAreaSource).toContain('@click="handleHideResultsPane"');
|
||||||
|
expect(workspaceSource).toContain('@toggle-results-pane="toggleSharedResultsPane"');
|
||||||
|
expect(workspaceSource).toContain("showResultPane.value = !showResultPane.value");
|
||||||
|
// Collapsing unmounts the shared surface, so the mouse re-show affordance
|
||||||
|
// must live at the workspace level, not inside the collapsible pane.
|
||||||
|
expect(workspaceSource).toContain("editor.showResultsPane");
|
||||||
|
expect(workspaceSource).toContain("showSharedResult && !showResultPane");
|
||||||
|
// Running a query re-expands a collapsed pane (issue #6193 promise).
|
||||||
|
expect(workspaceSource).toContain("if (isExecuting) showResultPane.value = true;");
|
||||||
|
});
|
||||||
|
|
||||||
it("animates layout transitions for split groups and the shared result surface", () => {
|
it("animates layout transitions for split groups and the shared result surface", () => {
|
||||||
const workspaceCssSource = readFileSync(new URL("../sqlEditorWorkspace.css", import.meta.url), "utf8");
|
const workspaceCssSource = readFileSync(new URL("../sqlEditorWorkspace.css", import.meta.url), "utf8");
|
||||||
expect(workspaceSource).toContain('import "./sqlEditorWorkspace.css"');
|
expect(workspaceSource).toContain('import "./sqlEditorWorkspace.css"');
|
||||||
|
|||||||
@@ -95,4 +95,5 @@ export interface ContentAreaSurfaceEmits {
|
|||||||
openSettings: [initialTab?: string, initialSection?: string];
|
openSettings: [initialTab?: string, initialSection?: string];
|
||||||
openConnectionSettings: [connectionId: string, initialTab: "advanced"];
|
openConnectionSettings: [connectionId: string, initialTab: "advanced"];
|
||||||
toggleZenMode: [];
|
toggleZenMode: [];
|
||||||
|
toggleResultsPane: [];
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,7 @@ export const contentSurfaceEventNames = [
|
|||||||
"openSettings",
|
"openSettings",
|
||||||
"openConnectionSettings",
|
"openConnectionSettings",
|
||||||
"toggleZenMode",
|
"toggleZenMode",
|
||||||
|
"toggleResultsPane",
|
||||||
] as const satisfies Array<keyof ContentAreaSurfaceEmits>;
|
] as const satisfies Array<keyof ContentAreaSurfaceEmits>;
|
||||||
|
|
||||||
export type ContentSurfaceEventName = (typeof contentSurfaceEventNames)[number];
|
export type ContentSurfaceEventName = (typeof contentSurfaceEventNames)[number];
|
||||||
|
|||||||
Reference in new issue
Block a user