fix(ui): preserve tab scroll positions
This commit is contained in:
parent
8a511d2765
commit
0431fbea49
|
|
@ -1909,6 +1909,7 @@ onUnmounted(() => {
|
|||
})
|
||||
"
|
||||
@object-schema-change="(schema) => activeTab && queryStore.updateSchema(activeTab.id, schema)"
|
||||
@object-browser-viewport-change="(tabId, viewport) => queryStore.updateObjectBrowserViewport(tabId, viewport)"
|
||||
@structure-editor-saved="
|
||||
(commentChanged) =>
|
||||
activeTab &&
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
|
|||
import { useTabScroll } from "@/composables/useTabScroll";
|
||||
import { formatElapsedSeconds } from "@/lib/common/elapsedTime";
|
||||
import type { CustomSaveHandler } from "@/composables/useDataGridEditor";
|
||||
import type { QueryTab, ConnectionConfig, TableInfoTab, TreeNode, VectorCollectionMeta } from "@/types/database";
|
||||
import type { QueryTab, ConnectionConfig, TableInfoTab, TreeNode, VectorCollectionMeta, ObjectBrowserViewport } from "@/types/database";
|
||||
import { sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
|
||||
type DataGridHandle = {
|
||||
|
|
@ -144,6 +144,7 @@ const emit = defineEmits<{
|
|||
editTableStructure: [tableName: string];
|
||||
openObjectTable: [target: { tableName: string; schema?: string; tableType?: string }];
|
||||
objectSchemaChange: [schema: string | undefined];
|
||||
objectBrowserViewportChange: [tabId: string, viewport: ObjectBrowserViewport];
|
||||
structureEditorSaved: [commentChanged: boolean];
|
||||
structureEditorClose: [];
|
||||
openSettings: [initialTab?: string, initialSection?: string];
|
||||
|
|
@ -1496,8 +1497,10 @@ defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExe
|
|||
:connection="activeConnection"
|
||||
:database="activeTab.database"
|
||||
:schema="activeTab.objectBrowser?.schema"
|
||||
:viewport="activeTab.objectBrowser?.viewport"
|
||||
@open-table="emit('openObjectTable', $event)"
|
||||
@schema-change="emit('objectSchemaChange', $event)"
|
||||
@viewport-change="emit('objectBrowserViewportChange', activeTab.id, $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { RecycleScroller } from "vue-virtual-scroller";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
import {
|
||||
|
|
@ -48,7 +48,7 @@ import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomC
|
|||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
import ProcedureExecutionDialog from "@/components/objects/ProcedureExecutionDialog.vue";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { ConnectionConfig, ForeignKeyInfo, ObjectInfo, ObjectSourceKind, ObjectStatistics } from "@/types/database";
|
||||
import type { ConnectionConfig, ForeignKeyInfo, ObjectBrowserViewMode, ObjectBrowserViewport, ObjectInfo, ObjectSourceKind, ObjectStatistics } from "@/types/database";
|
||||
import { sortTablesByFkDependency, type TableWithFk } from "@/lib/table/tableDependencySort";
|
||||
import { isSchemaAware } from "@/lib/database/databaseCapabilities";
|
||||
import { supportsSchemaDiagram, supportsTableImport, supportsTableStructureEditing, supportsTableTruncate } from "@/lib/database/databaseFeatureSupport";
|
||||
|
|
@ -94,11 +94,13 @@ const props = defineProps<{
|
|||
connection: ConnectionConfig;
|
||||
database: string;
|
||||
schema?: string;
|
||||
viewport?: ObjectBrowserViewport;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
openTable: [target: { tableName: string; schema?: string; tableType?: string }];
|
||||
schemaChange: [schema: string | undefined];
|
||||
viewportChange: [viewport: ObjectBrowserViewport];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
@ -180,6 +182,7 @@ const objectColumnWidths = ref<Record<ObjectBrowserColumnKey, number>>({
|
|||
});
|
||||
let loadId = 0;
|
||||
let stopColumnResize: (() => void) | null = null;
|
||||
let preserveObjectFilterScrollOnce = false;
|
||||
|
||||
// Export via background tracker
|
||||
const { addTask: addExportTask } = useExportTracker();
|
||||
|
|
@ -222,20 +225,107 @@ const hasUpdatedAt = computed(() => rows.value.some((row) => row.updated_at?.tri
|
|||
const hasAnyComment = computed(() => rows.value.some((row) => row.comment?.trim()));
|
||||
const isListView = computed(() => settingsStore.editorSettings.objectBrowserViewMode !== "grid");
|
||||
|
||||
// RecycleScroller exposes scrollToItem on its component instance (see
|
||||
// vue-virtual-scroller). Typed loosely to match the DataGrid usage pattern.
|
||||
const listScrollerRef = ref<{ scrollToItem?: (index: number) => void } | null>(null);
|
||||
const gridScrollerRef = ref<{ scrollToItem?: (index: number) => void } | null>(null);
|
||||
type ObjectBrowserScroller =
|
||||
| HTMLElement
|
||||
| {
|
||||
scrollToItem?: (index: number) => void;
|
||||
scrollToPosition?: (position: number) => void;
|
||||
$el?: HTMLElement;
|
||||
el?: HTMLElement | { value?: HTMLElement | null };
|
||||
};
|
||||
|
||||
// RecycleScroller exposes scroll helpers on its component instance. Keep the
|
||||
// type loose because vue-virtual-scroller does not ship complete ref typings.
|
||||
const listScrollerRef = ref<ObjectBrowserScroller | null>(null);
|
||||
const gridScrollerRef = ref<ObjectBrowserScroller | null>(null);
|
||||
let viewportFrame = 0;
|
||||
let restoreViewportFrame = 0;
|
||||
|
||||
function objectBrowserViewMode(): ObjectBrowserViewMode {
|
||||
return isListView.value ? "list" : "grid";
|
||||
}
|
||||
|
||||
function activeScroller() {
|
||||
return isListView.value ? listScrollerRef.value : gridScrollerRef.value;
|
||||
}
|
||||
|
||||
function scrollerElement(scroller: ObjectBrowserScroller | null = activeScroller()): HTMLElement | null {
|
||||
if (!scroller) return null;
|
||||
if (scroller instanceof HTMLElement) return scroller;
|
||||
if (scroller.$el instanceof HTMLElement) return scroller.$el;
|
||||
if (scroller.el instanceof HTMLElement) return scroller.el;
|
||||
if (scroller.el?.value instanceof HTMLElement) return scroller.el.value;
|
||||
return null;
|
||||
}
|
||||
|
||||
function emitViewportChange(scrollTop: number) {
|
||||
const viewport: ObjectBrowserViewport = {
|
||||
scrollTop: Math.max(0, Math.round(scrollTop)),
|
||||
viewMode: objectBrowserViewMode(),
|
||||
};
|
||||
if (props.viewport?.scrollTop === viewport.scrollTop && props.viewport.viewMode === viewport.viewMode) return;
|
||||
emit("viewportChange", viewport);
|
||||
}
|
||||
|
||||
function onObjectsScroll() {
|
||||
if (viewportFrame) return;
|
||||
viewportFrame = window.requestAnimationFrame(() => {
|
||||
viewportFrame = 0;
|
||||
const el = scrollerElement();
|
||||
if (!el) return;
|
||||
emitViewportChange(el.scrollTop);
|
||||
});
|
||||
}
|
||||
|
||||
function applyObjectBrowserScrollTop(scrollTop: number) {
|
||||
const scroller = activeScroller();
|
||||
if (scroller && !(scroller instanceof HTMLElement)) {
|
||||
scroller.scrollToPosition?.(scrollTop);
|
||||
if (scrollTop === 0) scroller.scrollToItem?.(0);
|
||||
}
|
||||
const el = scrollerElement(scroller);
|
||||
if (el) el.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
function restoreObjectBrowserViewport() {
|
||||
const viewport = props.viewport;
|
||||
if (!viewport || viewport.viewMode !== objectBrowserViewMode()) return;
|
||||
if (restoreViewportFrame) window.cancelAnimationFrame(restoreViewportFrame);
|
||||
const scrollTop = Math.max(0, viewport.scrollTop);
|
||||
nextTick(() => {
|
||||
applyObjectBrowserScrollTop(scrollTop);
|
||||
restoreViewportFrame = window.requestAnimationFrame(() => {
|
||||
applyObjectBrowserScrollTop(scrollTop);
|
||||
restoreViewportFrame = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scrollObjectsToTop() {
|
||||
// Read the active scroller inside nextTick so that after a list <-> grid
|
||||
// switch the (re)mounted scroller is the one we reset.
|
||||
emitViewportChange(0);
|
||||
nextTick(() => {
|
||||
const scroller = isListView.value ? listScrollerRef.value : gridScrollerRef.value;
|
||||
scroller?.scrollToItem?.(0);
|
||||
applyObjectBrowserScrollTop(0);
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
[listScrollerRef, gridScrollerRef, isListView],
|
||||
(_value, _oldValue, onCleanup) => {
|
||||
const el = scrollerElement();
|
||||
if (!el) return;
|
||||
el.addEventListener("scroll", onObjectsScroll, { passive: true });
|
||||
restoreObjectBrowserViewport();
|
||||
onCleanup(() => el.removeEventListener("scroll", onObjectsScroll));
|
||||
},
|
||||
{ flush: "post" },
|
||||
);
|
||||
|
||||
onActivated(() => {
|
||||
restoreObjectBrowserViewport();
|
||||
});
|
||||
|
||||
function setViewMode(mode: "list" | "grid") {
|
||||
settingsStore.updateEditorSettings({ objectBrowserViewMode: mode });
|
||||
scrollObjectsToTop();
|
||||
|
|
@ -251,7 +341,13 @@ watch([sortKey, sortDirection], () => scrollObjectsToTop());
|
|||
// Also jump to the top when the search query or object-type filter changes —
|
||||
// filtered results bear no relation to the previous scroll position.
|
||||
watch(search, () => scrollObjectsToTop());
|
||||
watch(objectFilter, () => scrollObjectsToTop());
|
||||
watch(objectFilter, () => {
|
||||
if (preserveObjectFilterScrollOnce) {
|
||||
preserveObjectFilterScrollOnce = false;
|
||||
return;
|
||||
}
|
||||
scrollObjectsToTop();
|
||||
});
|
||||
|
||||
const showCheckboxColumn = computed(() => settingsStore.editorSettings.objectBrowserShowCheckbox || selectedTableCount.value > 0);
|
||||
|
||||
|
|
@ -356,6 +452,8 @@ watch(
|
|||
onBeforeUnmount(() => {
|
||||
gridResizeObserver?.disconnect();
|
||||
gridResizeObserver = null;
|
||||
if (viewportFrame) window.cancelAnimationFrame(viewportFrame);
|
||||
if (restoreViewportFrame) window.cancelAnimationFrame(restoreViewportFrame);
|
||||
});
|
||||
|
||||
const gridRows = computed(() => {
|
||||
|
|
@ -1498,8 +1596,12 @@ async function loadObjects() {
|
|||
if (id === loadId) {
|
||||
loadingObjects.value = false;
|
||||
if (!userHasSelectedFilter.value && tableCount.value > 0) {
|
||||
// The default table filter is a presentation choice, not a user query
|
||||
// change, so preserve the tab's saved scroll offset across remounts.
|
||||
preserveObjectFilterScrollOnce = objectFilter.value !== "tables";
|
||||
objectFilter.value = "tables";
|
||||
}
|
||||
restoreObjectBrowserViewport();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/li
|
|||
import { getTableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
|
||||
import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
|
||||
import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import type { TableInfoTab, TableStructureEditorDraft, TableStructureEditorTarget } from "@/types/database";
|
||||
import type { TableInfoTab, TableStructureEditorDraft, TableStructureEditorTarget, TableStructureEditorViewport } from "@/types/database";
|
||||
import {
|
||||
buildStructureTargetLabel,
|
||||
combineDataTypeForDatabase,
|
||||
|
|
@ -62,6 +62,12 @@ const historyStore = useHistoryStore();
|
|||
const settingsStore = useSettingsStore();
|
||||
const { toast } = useToast();
|
||||
const rootRef = ref<HTMLElement>();
|
||||
type StructureScrollerRef = HTMLElement | { $el?: HTMLElement };
|
||||
const columnsScrollerRef = ref<StructureScrollerRef>();
|
||||
const indexesScrollerRef = ref<StructureScrollerRef>();
|
||||
const foreignKeysScrollerRef = ref<StructureScrollerRef>();
|
||||
const triggersScrollerRef = ref<StructureScrollerRef>();
|
||||
const ddlScrollerRef = ref<StructureScrollerRef>();
|
||||
const dynamicDataTypeOptionsCache = new Map<string, string[]>();
|
||||
|
||||
const sqlHighlighter = ref<SqlHighlighter>();
|
||||
|
|
@ -734,6 +740,7 @@ let restoringDraft = false;
|
|||
let syncingDraft = false;
|
||||
let draftHydrated = false;
|
||||
let hydratingRestoredDraft = false;
|
||||
let structureScrollFrame = 0;
|
||||
// A context-menu target may arrive before metadata rows render, so search text
|
||||
// and row scrolling are tracked separately for each request.
|
||||
let appliedInitialTargetSearchKey = "";
|
||||
|
|
@ -743,6 +750,54 @@ function cloneDraftValue<T>(value: T): T {
|
|||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
const structureScrollPositions = ref<Partial<Record<TableInfoTab, TableStructureEditorViewport>>>({});
|
||||
|
||||
function structureScrollerElement(scroller: StructureScrollerRef | undefined): HTMLElement | undefined {
|
||||
if (!scroller) return undefined;
|
||||
if (scroller instanceof HTMLElement) return scroller;
|
||||
return scroller.$el instanceof HTMLElement ? scroller.$el : undefined;
|
||||
}
|
||||
|
||||
function structureScrollerForTab(tab: TableInfoTab): HTMLElement | undefined {
|
||||
if (tab === "columns") return structureScrollerElement(columnsScrollerRef.value);
|
||||
if (tab === "indexes") return structureScrollerElement(indexesScrollerRef.value);
|
||||
if (tab === "foreignKeys") return structureScrollerElement(foreignKeysScrollerRef.value);
|
||||
if (tab === "triggers") return structureScrollerElement(triggersScrollerRef.value);
|
||||
if (tab === "ddl") return structureScrollerElement(ddlScrollerRef.value);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function restoreStructureScrollPosition(tab = activeTab.value) {
|
||||
const position = structureScrollPositions.value[tab];
|
||||
if (!position) return;
|
||||
nextTick(() => {
|
||||
const scroller = structureScrollerForTab(tab);
|
||||
if (!scroller) return;
|
||||
scroller.scrollTop = Math.max(0, position.scrollTop);
|
||||
scroller.scrollLeft = Math.max(0, position.scrollLeft);
|
||||
});
|
||||
}
|
||||
|
||||
function onStructureContentScroll(tab: TableInfoTab, event: Event) {
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
const position: TableStructureEditorViewport = {
|
||||
scrollTop: Math.max(0, Math.round(target.scrollTop)),
|
||||
scrollLeft: Math.max(0, Math.round(target.scrollLeft)),
|
||||
};
|
||||
const previous = structureScrollPositions.value[tab];
|
||||
if (previous?.scrollTop === position.scrollTop && previous.scrollLeft === position.scrollLeft) return;
|
||||
structureScrollPositions.value = {
|
||||
...structureScrollPositions.value,
|
||||
[tab]: position,
|
||||
};
|
||||
if (structureScrollFrame) return;
|
||||
structureScrollFrame = window.requestAnimationFrame(() => {
|
||||
structureScrollFrame = 0;
|
||||
syncDraftToParent();
|
||||
});
|
||||
}
|
||||
|
||||
function createCurrentDraft(initialized = true): TableStructureEditorDraft {
|
||||
return {
|
||||
activeTab: activeTab.value as TableStructureEditorDraft["activeTab"],
|
||||
|
|
@ -753,6 +808,7 @@ function createCurrentDraft(initialized = true): TableStructureEditorDraft {
|
|||
indexes: cloneDraftValue(indexes.value),
|
||||
foreignKeys: cloneDraftValue(foreignKeys.value),
|
||||
triggers: cloneDraftValue(triggers.value),
|
||||
scrollPositions: cloneDraftValue(structureScrollPositions.value),
|
||||
initialized,
|
||||
};
|
||||
}
|
||||
|
|
@ -776,8 +832,10 @@ function restoreDraft(draft: TableStructureEditorDraft) {
|
|||
indexes.value = cloneDraftValue(draft.indexes || []);
|
||||
foreignKeys.value = cloneDraftValue(draft.foreignKeys || []);
|
||||
triggers.value = cloneDraftValue(draft.triggers || []);
|
||||
structureScrollPositions.value = cloneDraftValue(draft.scrollPositions || {});
|
||||
restoringDraft = false;
|
||||
draftHydrated = !needsColumnDraftMetadataHydration();
|
||||
restoreStructureScrollPosition();
|
||||
}
|
||||
|
||||
function needsColumnDraftMetadataHydration() {
|
||||
|
|
@ -1930,6 +1988,7 @@ onActivated(() => {
|
|||
applyInitialStructureTarget();
|
||||
void hydrateRestoredDraftFromDatabase().then(() => applyInitialStructureTarget());
|
||||
}
|
||||
restoreStructureScrollPosition();
|
||||
});
|
||||
onDeactivated(unregisterStructureEditorShortcuts);
|
||||
onBeforeUnmount(() => {
|
||||
|
|
@ -1938,6 +1997,7 @@ onBeforeUnmount(() => {
|
|||
clearSqlPreviewState();
|
||||
if (columnHighlightTimer) window.clearTimeout(columnHighlightTimer);
|
||||
if (indexHighlightTimer) window.clearTimeout(indexHighlightTimer);
|
||||
if (structureScrollFrame) window.cancelAnimationFrame(structureScrollFrame);
|
||||
persistStructureDensity();
|
||||
});
|
||||
|
||||
|
|
@ -2029,6 +2089,7 @@ watch(
|
|||
watch(activeTab, () => {
|
||||
highlightedColumnId.value = null;
|
||||
highlightedIndexId.value = null;
|
||||
restoreStructureScrollPosition();
|
||||
syncDraftToParent();
|
||||
});
|
||||
|
||||
|
|
@ -2200,7 +2261,7 @@ watch(activeTab, (tab) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<TabsContent v-if="tableMetadataCapabilities.columns" value="columns" class="m-0 min-h-0 flex-1 overflow-auto p-0">
|
||||
<TabsContent ref="columnsScrollerRef" v-if="tableMetadataCapabilities.columns" value="columns" class="m-0 min-h-0 flex-1 overflow-auto p-0" @scroll.passive="onStructureContentScroll('columns', $event)">
|
||||
<table class="border-separate border-spacing-0 text-[length:var(--structure-font-size)] leading-[var(--structure-line-height)]" :style="{ minWidth: visibleColWidths.reduce((a, w) => a + w, 0) + 'px' }">
|
||||
<thead class="sticky top-0 z-10 bg-background">
|
||||
<tr>
|
||||
|
|
@ -2471,7 +2532,7 @@ watch(activeTab, (tab) => {
|
|||
</table>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent v-if="tableMetadataCapabilities.indexes" value="indexes" class="m-0 min-h-0 flex-1 overflow-auto p-0">
|
||||
<TabsContent ref="indexesScrollerRef" v-if="tableMetadataCapabilities.indexes" value="indexes" class="m-0 min-h-0 flex-1 overflow-auto p-0" @scroll.passive="onStructureContentScroll('indexes', $event)">
|
||||
<div v-if="indexesLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
|
|
@ -2575,7 +2636,7 @@ watch(activeTab, (tab) => {
|
|||
</table>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent v-if="tableMetadataCapabilities.foreignKeys" value="foreignKeys" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]">
|
||||
<TabsContent ref="foreignKeysScrollerRef" v-if="tableMetadataCapabilities.foreignKeys" value="foreignKeys" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('foreignKeys', $event)">
|
||||
<div v-if="foreignKeysLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
|
|
@ -2625,7 +2686,7 @@ watch(activeTab, (tab) => {
|
|||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent v-if="tableMetadataCapabilities.triggers" value="triggers" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]">
|
||||
<TabsContent ref="triggersScrollerRef" v-if="tableMetadataCapabilities.triggers" value="triggers" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('triggers', $event)">
|
||||
<div v-if="triggersLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
|
|
@ -2674,7 +2735,7 @@ watch(activeTab, (tab) => {
|
|||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent v-if="tableMetadataCapabilities.ddl" value="ddl" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]">
|
||||
<TabsContent ref="ddlScrollerRef" v-if="tableMetadataCapabilities.ddl" value="ddl" class="m-0 min-h-0 flex-1 overflow-auto p-[var(--structure-cell-px)]" @scroll.passive="onStructureContentScroll('ddl', $event)">
|
||||
<div v-if="ddlLoading" class="flex items-center justify-center gap-2 py-10 text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,22 @@ describe("queryStore database open state", () => {
|
|||
expect(store.isDatabaseOpen("pg-1", "analytics")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps object browser viewport per tab and clears it on schema change", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
const tabId = store.openObjectBrowser("pg-1", "app", "public");
|
||||
store.updateObjectBrowserViewport(tabId, { scrollTop: 340, viewMode: "list" });
|
||||
|
||||
const tab = store.tabs.find((item) => item.id === tabId);
|
||||
expect(tab?.objectBrowser?.viewport).toEqual({ scrollTop: 340, viewMode: "list" });
|
||||
|
||||
store.updateSchema(tabId, "archive");
|
||||
|
||||
expect(tab?.objectBrowser?.schema).toBe("archive");
|
||||
expect(tab?.objectBrowser?.viewport).toBeUndefined();
|
||||
});
|
||||
|
||||
it("closes data and structure tabs for a dropped table object", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { defineStore } from "pinia";
|
|||
import { uuid } from "@/lib/common/utils";
|
||||
import { markRaw, ref, watch, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { DatabaseType, QueryResult, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
|
||||
import type { DatabaseType, ObjectBrowserViewport, QueryResult, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
|
||||
import { orderPinnedFirst } from "@/lib/app/pinnedItems";
|
||||
import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState";
|
||||
import { buildExplainSql, parseExplainResult, parseDamengExplainText } from "@/lib/diagram/explainPlan";
|
||||
|
|
@ -1576,6 +1576,14 @@ export const useQueryStore = defineStore("query", () => {
|
|||
queueSavedSqlEditorPositionPersist(tab);
|
||||
}
|
||||
|
||||
function updateObjectBrowserViewport(id: string, viewport: ObjectBrowserViewport) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab || tab.mode !== "objects") return;
|
||||
const previous = tab.objectBrowser?.viewport;
|
||||
if (previous?.scrollTop === viewport.scrollTop && previous.viewMode === viewport.viewMode) return;
|
||||
tab.objectBrowser = { ...tab.objectBrowser, viewport };
|
||||
}
|
||||
|
||||
function renameTab(id: string, title: string) {
|
||||
const trimmed = title.trim();
|
||||
if (!trimmed) return false;
|
||||
|
|
@ -1718,7 +1726,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
if (!tab || tab.schema === schema) return;
|
||||
rollbackTabTransaction(tab);
|
||||
tab.schema = schema;
|
||||
if (tab.mode === "objects") tab.objectBrowser = { ...tab.objectBrowser, schema };
|
||||
if (tab.mode === "objects") tab.objectBrowser = { ...tab.objectBrowser, schema, viewport: undefined };
|
||||
}
|
||||
|
||||
function updateConnection(id: string, connectionId: string, database = "") {
|
||||
|
|
@ -3281,6 +3289,7 @@ export const useQueryStore = defineStore("query", () => {
|
|||
updateSql,
|
||||
updateEditorViewport,
|
||||
updateEditorSelection,
|
||||
updateObjectBrowserViewport,
|
||||
setAutoCommit,
|
||||
commitTransaction,
|
||||
rollbackTransaction,
|
||||
|
|
|
|||
|
|
@ -644,9 +644,22 @@ export interface TableStructureEditorDraft {
|
|||
indexes: import("@/lib/table/tableStructureEditorSql").EditableStructureIndex[];
|
||||
foreignKeys: import("@/lib/table/tableStructureEditorSql").EditableStructureForeignKey[];
|
||||
triggers: import("@/lib/table/tableStructureEditorSql").EditableStructureTrigger[];
|
||||
scrollPositions?: Partial<Record<TableInfoTab, TableStructureEditorViewport>>;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
export interface TableStructureEditorViewport {
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}
|
||||
|
||||
export type ObjectBrowserViewMode = "list" | "grid";
|
||||
|
||||
export interface ObjectBrowserViewport {
|
||||
scrollTop: number;
|
||||
viewMode: ObjectBrowserViewMode;
|
||||
}
|
||||
|
||||
export interface QueryTab {
|
||||
id: string;
|
||||
title: string;
|
||||
|
|
@ -715,6 +728,7 @@ export interface QueryTab {
|
|||
objectBrowser?: {
|
||||
schema?: string;
|
||||
objectType?: "tables";
|
||||
viewport?: ObjectBrowserViewport;
|
||||
};
|
||||
objectSource?: {
|
||||
schema?: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue