feat(grid): add pinned query result history

This commit is contained in:
t8y2 committed 2026-06-18 14:57:54 +08:00
1 parent fe39a38dd4
commit c155443d4b
7 files changed
+167 -30

No files matched your search

@@ -2,7 +2,7 @@
import { computed, ref, defineAsyncComponent, watch, nextTick, onMounted, onUnmounted } from "vue";
import type { CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
import { Check, Columns3, Loader2, Search, Bot, GitBranch, BarChart3, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, Toolbox, ListChecks, Database, FileUp, Download, X } from "@lucide/vue";
import { Check, Columns3, Loader2, Search, Bot, GitBranch, BarChart3, TableProperties, ChevronDown, ChevronUp, Inbox, RefreshCcw, Wrench, Toolbox, ListChecks, Database, FileUp, Download, X, Pin } from "@lucide/vue";
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
import { Button } from "@/components/ui/button";
@@ -230,9 +230,11 @@ const activeQueryError = computed(() => {
const hasQueryOutput = computed(() => !!props.activeTab.result || !!props.activeTab.explainPlan || !!props.activeTab.explainError || props.activeTab.isExecuting === true || props.activeTab.isExplaining === true);
const tabularResults = computed(() => tabularResultItems(props.activeTab.results));
const resultRuns = computed(() => resultRunItems(props.activeTab));
const activeResultRunItem = computed(() => resultRuns.value.find((run) => run.active));
const activeResultGridCacheKey = computed(() => resultGridCacheKey(props.activeTab));
const resultArchiveExporting = ref(false);
const canExportResultArchive = computed(() => props.activeTab.mode === "query" && (!!props.activeTab.result || !!props.activeTab.results?.length || !!props.activeTab.resultRuns?.length));
const resultAutoSave = computed(() => props.activeTab.resultAutoSave === true);
watch(
() => tabularResults.value.map((item) => item.index).join(","),
() => {
@@ -482,6 +484,19 @@ function removeResultRun(runId: string) {
if (removed && removedActiveRun) emit("update:activeOutputView", "result");
}
async function selectResultRun(runId: string) {
if (!(await queryStore.setActiveResultRun(props.activeTab.id, runId))) {
toast(t("tabs.missingResultRun"), 4000);
return;
}
emit("update:activeOutputView", "result");
}
function toggleResultAutoSave() {
const enabled = queryStore.toggleResultAutoSave(props.activeTab.id);
toast(t(enabled ? "tabs.autoKeepResultsEnabled" : "tabs.autoKeepResultsDisabled"), 2500);
}
function handleModRTarget(target: Element): boolean {
if (target.closest("[data-query-editor-root]")) return queryEditorRef.value?.openReplace() ?? false;
if (target.closest("[data-cell-detail-editor-root]")) return dataGridRef.value?.openCellDetailSearch() ?? false;
@@ -545,26 +560,45 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
{{ t("tabs.tableData") }}
</Button>
</div>
<Button
v-if="activeTab.mode === 'query' && activeTab.result"
variant="ghost"
size="icon"
class="h-6 w-7 shrink-0 text-muted-foreground hover:text-foreground"
:class="{ 'text-primary': resultAutoSave }"
:title="resultAutoSave ? t('tabs.autoKeepResultsEnabled') : t('tabs.autoKeepResults')"
:aria-label="resultAutoSave ? t('tabs.autoKeepResultsEnabled') : t('tabs.autoKeepResults')"
:aria-pressed="resultAutoSave"
@click="toggleResultAutoSave"
>
<Pin class="h-3.5 w-3.5" :class="{ 'fill-current': resultAutoSave }" />
</Button>
<template v-if="resultRuns.length > 0">
<span class="mx-1 h-4 w-px shrink-0 bg-border" />
<div class="flex min-w-0 max-w-[35%] items-center gap-1 overflow-x-auto overflow-y-hidden px-1" :aria-label="t('tabs.resultRuns')">
<div v-for="run in resultRuns" :key="run.id" class="inline-flex shrink-0 items-center">
<Button
size="sm"
:variant="run.active ? 'default' : 'ghost'"
class="h-6 rounded-r-none px-2 text-xs"
@click="
queryStore.setActiveResultRun(activeTab.id, run.id);
emit('update:activeOutputView', 'result');
"
>
{{ t("tabs.runN", { n: run.sequence }) }}
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="sm" class="h-6 shrink-0 gap-1 px-2 text-xs">
{{ activeResultRunItem ? t("tabs.runN", { n: activeResultRunItem.sequence }) : t("tabs.resultRuns") }}
<ChevronDown class="h-3.5 w-3.5" />
</Button>
<Button size="icon" :variant="run.active ? 'default' : 'ghost'" class="h-6 w-6 rounded-l-none border-l border-border/50 px-0" :title="t('tabs.removeRun', { n: run.sequence })" :aria-label="t('tabs.removeRun', { n: run.sequence })" @click.stop="removeResultRun(run.id)">
<X class="h-3 w-3" />
</Button>
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" class="w-48">
<DropdownMenuItem v-for="run in resultRuns" :key="run.id" class="flex items-center gap-2 pr-1" @select="selectResultRun(run.id)">
<Check v-if="run.active" class="h-3.5 w-3.5 shrink-0" />
<span v-else class="h-3.5 w-3.5 shrink-0" />
<span class="min-w-0 flex-1 truncate">{{ t("tabs.runN", { n: run.sequence }) }}</span>
<button
type="button"
class="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
:title="t('tabs.removeRun', { n: run.sequence })"
:aria-label="t('tabs.removeRun', { n: run.sequence })"
@click.stop.prevent="removeResultRun(run.id)"
>
<X class="h-3 w-3" />
</button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
<template v-if="tabularResults.length > 1">
<span class="mx-1 h-4 w-px shrink-0 bg-border" />