feat(result-tabs): support pinned tabs and batch closing
This commit is contained in:
17 files changed
+883
-57
No files matched your search
@@ -49,6 +49,7 @@ import "splitpanes/dist/splitpanes.css";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuPortal } from "@/components/ui/dropdown-menu";
|
||||
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import QueryEditor from "@/components/editor/QueryEditor.vue";
|
||||
@@ -255,6 +256,7 @@ onMounted(() => {
|
||||
window.addEventListener("resize", updateStandaloneResultToolbarDimensions);
|
||||
window.visualViewport?.addEventListener("resize", updateStandaloneResultToolbarDimensions);
|
||||
window.addEventListener("dbx:ui-scale-applied", updateStandaloneResultToolbarDimensions);
|
||||
revealActiveResultRunAfterRender();
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -474,13 +476,43 @@ const activeResultRunItem = computed(() => resultRuns.value.find((run) => run.ac
|
||||
const showResultRunTabs = computed(() => resultRuns.value.length > 0 && resultRunDisplayMode.value === "tabs");
|
||||
const showResultRunSelector = computed(() => resultRuns.value.length > 0 && resultRunDisplayMode.value === "list");
|
||||
const canCloseQueryResult = computed(() => props.activeTab.mode === "query" && !props.activeTab.isExecuting && !props.activeTab.activeResultRunId && (!!props.activeTab.result || !!props.activeTab.results?.length || props.activeTab.resultEvicted === true));
|
||||
|
||||
function updateResultTabsAfterRender() {
|
||||
nextTick(() => updateResultTabsScrollbar());
|
||||
}
|
||||
|
||||
function revealActiveResultRunAfterRender() {
|
||||
nextTick(() => {
|
||||
if (!showResultRunTabs.value) return;
|
||||
updateResultTabsScrollbar();
|
||||
resultTabsScrollerRef.value?.querySelector<HTMLElement>('[data-active-result-run="true"]')?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
});
|
||||
}
|
||||
|
||||
function resultRunIdsWereAppended(previous: string[], current: string[]) {
|
||||
return current.length > previous.length && previous.every((id, index) => current[index] === id);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => `${resultRunDisplayMode.value}:${resultRuns.value.map((run) => run.id).join(",")}:${props.activeTab.activeResultRunId ?? ""}`,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
updateResultTabsScrollbar();
|
||||
resultTabsScrollerRef.value?.querySelector<HTMLElement>('[data-active-result-run="true"]')?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
});
|
||||
() => ({
|
||||
tabId: props.activeTab.id,
|
||||
displayMode: resultRunDisplayMode.value,
|
||||
runIds: resultRuns.value.map((run) => run.id),
|
||||
activeRunId: props.activeTab.activeResultRunId,
|
||||
}),
|
||||
(current, previous) => {
|
||||
const switchedTab = current.tabId !== previous.tabId;
|
||||
const switchedDisplayMode = current.displayMode !== previous.displayMode;
|
||||
const activeRunChanged = current.activeRunId !== previous.activeRunId;
|
||||
|
||||
// Appending a fresh result must keep the user's horizontal position stable.
|
||||
// Reused result slots, tab/display-mode changes, and keyboard/close flows
|
||||
// still reveal the active run when it may be outside the visible strip.
|
||||
if (switchedTab || switchedDisplayMode || (activeRunChanged && !resultRunIdsWereAppended(previous.runIds, current.runIds))) {
|
||||
revealActiveResultRunAfterRender();
|
||||
return;
|
||||
}
|
||||
updateResultTabsAfterRender();
|
||||
},
|
||||
);
|
||||
const summaryItems = computed(() => executionSummaryItems(props.activeTab));
|
||||
@@ -911,6 +943,61 @@ async function removeResultRun(runId: string) {
|
||||
activeRunTab?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
||||
}
|
||||
|
||||
function toggleResultRunPinned(runId: string) {
|
||||
queryStore.toggleResultRunPinned(props.activeTab.id, runId);
|
||||
}
|
||||
|
||||
async function closeOtherResultRuns(runId: string) {
|
||||
if (!(await queryStore.closeOtherResultRuns(props.activeTab.id, runId))) return;
|
||||
await selectResultRun(runId);
|
||||
}
|
||||
|
||||
async function closeResultRunsToLeft(runId: string) {
|
||||
if (!(await queryStore.closeResultRunsToLeft(props.activeTab.id, runId))) return;
|
||||
await selectResultRun(runId);
|
||||
}
|
||||
|
||||
async function closeResultRunsToRight(runId: string) {
|
||||
if (!(await queryStore.closeResultRunsToRight(props.activeTab.id, runId))) return;
|
||||
await selectResultRun(runId);
|
||||
}
|
||||
|
||||
function resultRunContextMenuItems(run: (typeof resultRuns.value)[number]): ContextMenuItem[] {
|
||||
return [
|
||||
{
|
||||
label: t(run.pinned ? "tabs.unpinResultRun" : "tabs.pinResultRun"),
|
||||
action: () => toggleResultRunPinned(run.id),
|
||||
icon: Pin,
|
||||
iconClass: run.pinned ? "fill-current" : "",
|
||||
},
|
||||
{
|
||||
label: t("tabs.unpinAllResultRuns"),
|
||||
action: () => queryStore.unpinAllResultRuns(props.activeTab.id),
|
||||
disabled: !resultRuns.value.some((item) => item.pinned),
|
||||
icon: Pin,
|
||||
},
|
||||
{ label: "", separator: true },
|
||||
{
|
||||
label: t("tabs.closeOtherResultRuns"),
|
||||
action: () => void closeOtherResultRuns(run.id),
|
||||
disabled: resultRuns.value.length <= 1,
|
||||
icon: X,
|
||||
},
|
||||
{
|
||||
label: t("tabs.closeResultRunsToLeft"),
|
||||
action: () => void closeResultRunsToLeft(run.id),
|
||||
disabled: resultRuns.value.findIndex((item) => item.id === run.id) <= 0,
|
||||
icon: X,
|
||||
},
|
||||
{
|
||||
label: t("tabs.closeResultRunsToRight"),
|
||||
action: () => void closeResultRunsToRight(run.id),
|
||||
disabled: resultRuns.value.findIndex((item) => item.id === run.id) >= resultRuns.value.length - 1,
|
||||
icon: X,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function closeCurrentQueryResult() {
|
||||
if (!(await queryStore.closeQueryResult(props.activeTab.id))) return;
|
||||
emit("update:activeOutputView", "result");
|
||||
@@ -1163,36 +1250,38 @@ defineExpose({
|
||||
</div>
|
||||
<div ref="resultTabsScrollerRef" class="result-tab-scroll flex h-full items-center gap-1 overflow-x-auto overflow-y-hidden px-1" :style="resultTabsScrollerStyle" @scroll="updateResultTabsScrollbar" @wheel="onResultTabsWheel">
|
||||
<div role="tablist" :aria-label="t('tabs.resultRuns')" class="flex h-full shrink-0 items-center gap-1">
|
||||
<div
|
||||
v-for="(run, runIndex) in resultRuns"
|
||||
:key="run.id"
|
||||
role="presentation"
|
||||
class="group/result-run inline-flex h-7 shrink-0 items-center overflow-hidden rounded-md border transition-colors"
|
||||
:class="run.active ? 'border-border bg-background text-foreground shadow-sm' : 'border-transparent text-muted-foreground hover:border-border/70 hover:bg-background/70 hover:text-foreground'"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
data-result-run-tab
|
||||
:tabindex="run.active ? 0 : -1"
|
||||
:aria-selected="run.active"
|
||||
:data-active-result-run="run.active ? 'true' : undefined"
|
||||
class="h-full whitespace-nowrap pl-2.5 pr-1 text-xs font-medium outline-none focus-visible:bg-accent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50"
|
||||
@click="selectResultRun(run.id)"
|
||||
@keydown="onResultRunTabKeydown($event, runIndex)"
|
||||
<CustomContextMenu v-for="(run, runIndex) in resultRuns" :key="run.id" :items="() => resultRunContextMenuItems(run)" v-slot="{ onContextMenu }">
|
||||
<div
|
||||
role="presentation"
|
||||
class="group/result-run inline-flex h-7 shrink-0 select-none items-center overflow-hidden rounded-md border transition-colors"
|
||||
:class="run.active ? 'border-border bg-background text-foreground shadow-sm' : 'border-transparent text-muted-foreground hover:border-border/70 hover:bg-background/70 hover:text-foreground'"
|
||||
@contextmenu="onContextMenu"
|
||||
>
|
||||
{{ run.title || t("tabs.runN", { n: run.sequence }) }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mr-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground/70 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50"
|
||||
: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>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
data-result-run-tab
|
||||
:tabindex="run.active ? 0 : -1"
|
||||
:aria-selected="run.active"
|
||||
:data-active-result-run="run.active ? 'true' : undefined"
|
||||
class="flex h-full select-none items-center gap-1 whitespace-nowrap pl-2.5 pr-1 text-xs font-medium outline-none focus-visible:bg-accent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50"
|
||||
@click="selectResultRun(run.id)"
|
||||
@keydown="onResultRunTabKeydown($event, runIndex)"
|
||||
>
|
||||
<Pin v-if="run.pinned" class="h-3 w-3 shrink-0 fill-current text-primary" />
|
||||
{{ run.title || t("tabs.runN", { n: run.sequence }) }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="mr-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground/70 outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:bg-accent focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50"
|
||||
: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>
|
||||
</div>
|
||||
</CustomContextMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1205,20 +1294,23 @@ defineExpose({
|
||||
</Button>
|
||||
</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">{{ run.title || 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>
|
||||
<CustomContextMenu v-for="run in resultRuns" :key="run.id" :items="() => resultRunContextMenuItems(run)" v-slot="{ onContextMenu }">
|
||||
<DropdownMenuItem class="flex items-center gap-2 pr-1" @select="selectResultRun(run.id)" @contextmenu="onContextMenu">
|
||||
<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" />
|
||||
<Pin v-if="run.pinned" class="h-3 w-3 shrink-0 fill-current text-primary" />
|
||||
<span class="min-w-0 flex-1 truncate">{{ run.title || 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>
|
||||
</CustomContextMenu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user