Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 141 additions & 49 deletions apps/desktop/src/components/layout/ContentArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -255,6 +256,7 @@ onMounted(() => {
window.addEventListener("resize", updateStandaloneResultToolbarDimensions);
window.visualViewport?.addEventListener("resize", updateStandaloneResultToolbarDimensions);
window.addEventListener("dbx:ui-scale-applied", updateStandaloneResultToolbarDimensions);
revealActiveResultRunAfterRender();
});

watch(
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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>
Expand All @@ -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>
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,12 @@ export default {
resultRuns: "Result runs",
resultSets: "Result sets",
removeRun: "Remove run {n}",
pinResultRun: "Pin result tab",
unpinResultRun: "Unpin result tab",
unpinAllResultRuns: "Unpin all result tabs",
closeOtherResultRuns: "Close other result tabs",
closeResultRunsToLeft: "Close result tabs to the left",
closeResultRunsToRight: "Close result tabs to the right",
closeResult: "Close result",
autoKeepResults: "Auto-keep query results",
autoKeepResultsEnabled: "Auto-keep results enabled",
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/i18n/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,12 @@ export default withEnglishFallback({
resultRuns: "Ejecuciones de resultado",
resultSets: "Conjuntos de resultados",
removeRun: "Eliminar ejecución {n}",
pinResultRun: "Fijar pestaña de resultado",
unpinResultRun: "Desfijar pestaña de resultado",
unpinAllResultRuns: "Desfijar todas las pestañas de resultado",
closeOtherResultRuns: "Cerrar otras pestañas de resultado",
closeResultRunsToLeft: "Cerrar pestañas de resultado a la izquierda",
closeResultRunsToRight: "Cerrar pestañas de resultado a la derecha",
closeResult: "Cerrar resultado",
autoKeepResults: "Conservar resultados automáticamente",
autoKeepResultsEnabled: "Conservación automática activada",
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/i18n/locales/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1370,6 +1370,12 @@ export default withEnglishFallback({
resultRuns: "Esecuzioni risultati",
resultSets: "Set di risultati",
removeRun: "Rimuovi esecuzione {n}",
pinResultRun: "Fissa scheda risultato",
unpinResultRun: "Sblocca scheda risultato",
unpinAllResultRuns: "Sblocca tutte le schede risultato",
closeOtherResultRuns: "Chiudi le altre schede risultato",
closeResultRunsToLeft: "Chiudi le schede risultato a sinistra",
closeResultRunsToRight: "Chiudi le schede risultato a destra",
closeResult: "Chiudi risultato",
autoKeepResults: "Mantieni automaticamente i risultati",
autoKeepResultsEnabled: "Mantieni risultati abilitato",
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/i18n/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,12 @@ export default withEnglishFallback({
resultRuns: "実行履歴",
resultSets: "結果セット",
removeRun: "実行 {n} を削除",
pinResultRun: "結果タブを固定",
unpinResultRun: "結果タブの固定を解除",
unpinAllResultRuns: "すべての結果タブの固定を解除",
closeOtherResultRuns: "他の結果タブを閉じる",
closeResultRunsToLeft: "左側の結果タブを閉じる",
closeResultRunsToRight: "右側の結果タブを閉じる",
closeResult: "結果を閉じる",
autoKeepResults: "クエリ結果を自動保持",
autoKeepResultsEnabled: "結果の自動保持をオンにしました",
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,12 @@ export default withEnglishFallback({
resultRuns: "결과 실행",
resultSets: "결과 집합",
removeRun: "실행 {n} 제거",
pinResultRun: "결과 탭 고정",
unpinResultRun: "결과 탭 고정 해제",
unpinAllResultRuns: "모든 결과 탭 고정 해제",
closeOtherResultRuns: "다른 결과 탭 닫기",
closeResultRunsToLeft: "왼쪽 결과 탭 닫기",
closeResultRunsToRight: "오른쪽 결과 탭 닫기",
closeResult: "결과 닫기",
autoKeepResults: "쿼리 결과 자동 보관",
autoKeepResultsEnabled: "결과 자동 보관 활성화됨",
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/i18n/locales/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,12 @@ export default withEnglishFallback({
resultRuns: "Execuções de resultado",
resultSets: "Conjuntos de resultados",
removeRun: "Remover execução {n}",
pinResultRun: "Fixar aba de resultado",
unpinResultRun: "Desafixar aba de resultado",
unpinAllResultRuns: "Desafixar todas as abas de resultado",
closeOtherResultRuns: "Fechar outras abas de resultado",
closeResultRunsToLeft: "Fechar abas de resultado à esquerda",
closeResultRunsToRight: "Fechar abas de resultado à direita",
closeResult: "Fechar resultado",
autoKeepResults: "Manter resultados automaticamente",
autoKeepResultsEnabled: "Manutenção automática de resultados ativada",
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,12 @@ export default withEnglishFallback({
resultRuns: "执行结果",
resultSets: "语句结果",
removeRun: "删除执行 {n}",
pinResultRun: "固定结果标签",
unpinResultRun: "取消固定结果标签",
unpinAllResultRuns: "取消固定所有结果标签",
closeOtherResultRuns: "关闭其他结果标签",
closeResultRunsToLeft: "关闭左侧结果标签",
closeResultRunsToRight: "关闭右侧结果标签",
closeResult: "关闭结果",
autoKeepResults: "自动保留查询结果",
autoKeepResultsEnabled: "已开启自动保留结果",
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/i18n/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,12 @@ export default withEnglishFallback({
resultRuns: "執行結果",
resultSets: "語句結果",
removeRun: "移除執行 {n}",
pinResultRun: "釘選結果分頁",
unpinResultRun: "取消釘選結果分頁",
unpinAllResultRuns: "取消釘選所有結果分頁",
closeOtherResultRuns: "關閉其他結果分頁",
closeResultRunsToLeft: "關閉左側結果分頁",
closeResultRunsToRight: "關閉右側結果分頁",
closeResult: "關閉結果",
autoKeepResults: "自動保留查詢結果",
autoKeepResultsEnabled: "自動保留結果已啟用",
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/app/openTabsPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface SavedQueryResultRun {
sequence: number;
sql: string;
createdAt: number;
pinned?: boolean;
activeResultIndex?: number;
resultCacheKey?: string;
resultEvicted?: boolean;
Expand Down Expand Up @@ -138,6 +139,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
sequence: run.sequence,
sql: run.sql,
createdAt: run.createdAt,
...(run.pinned ? { pinned: true } : {}),
activeResultIndex: run.activeResultIndex,
...(run.resultCacheKey !== undefined ? { resultCacheKey: run.resultCacheKey } : {}),
...(run.resultEvicted ? { resultEvicted: true } : {}),
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/lib/tabs/tabPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,12 +344,13 @@ export function activeResultRun(tab: Pick<QueryTab, "resultRuns" | "activeResult
return tab.resultRuns?.find((run) => run.id === tab.activeResultRunId);
}

export function resultRunItems(tab: Pick<QueryTab, "resultRuns" | "activeResultRunId">): { id: string; title: string; sequence: number; active: boolean }[] {
export function resultRunItems(tab: Pick<QueryTab, "resultRuns" | "activeResultRunId">): { id: string; title: string; sequence: number; active: boolean; pinned: boolean }[] {
return (tab.resultRuns ?? []).map((run) => ({
id: run.id,
title: run.title,
sequence: run.sequence,
active: run.id === tab.activeResultRunId,
pinned: run.pinned === true,
}));
}

Expand Down
Loading
Loading