fix(sidebar): reveal expanded tree items
This commit is contained in:
parent
28f4aec50d
commit
ab87de4054
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ref, computed, nextTick, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Search, X, ListFilter, Check, FolderPlus } from "lucide-vue-next";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
SIDEBAR_TREE_PRERENDER_COUNT,
|
||||
SIDEBAR_TREE_SCROLL_BUFFER,
|
||||
flattenTree,
|
||||
scrollTopForExpandedTreeNode,
|
||||
shouldVirtualizeFlatTree,
|
||||
type FlatTreeNode,
|
||||
} from "@/composables/useFlatTree";
|
||||
|
|
@ -32,6 +33,7 @@ const store = useConnectionStore();
|
|||
const searchQuery = ref("");
|
||||
const deferredSearchQuery = ref("");
|
||||
const searchInputRef = ref<HTMLInputElement>();
|
||||
const treeScrollerRef = ref<InstanceType<typeof RecycleScroller> | null>(null);
|
||||
const selectedTypes = ref<string[]>([]);
|
||||
const searchCollapsedIds = ref<Set<string>>(new Set());
|
||||
let searchTimer: number | undefined;
|
||||
|
|
@ -139,6 +141,28 @@ function onSearchToggle(node: TreeNode) {
|
|||
searchCollapsedIds.value = next;
|
||||
}
|
||||
|
||||
async function onNodeToggled(node: TreeNode, wasExpanded: boolean) {
|
||||
if (wasExpanded || !node.isExpanded) return;
|
||||
|
||||
await nextTick();
|
||||
|
||||
const expandedIndex = flatNodes.value.findIndex((item) => item.id === node.id);
|
||||
const insertedRowCount = flattenTree([node]).length - 1;
|
||||
const scroller = treeScrollerRef.value?.$el as HTMLElement | undefined;
|
||||
if (!scroller || expandedIndex < 0 || insertedRowCount <= 0) return;
|
||||
|
||||
const nextScrollTop = scrollTopForExpandedTreeNode({
|
||||
expandedIndex,
|
||||
insertedRowCount,
|
||||
currentScrollTop: scroller.scrollTop,
|
||||
viewportHeight: scroller.clientHeight,
|
||||
});
|
||||
|
||||
if (nextScrollTop !== scroller.scrollTop) {
|
||||
scroller.scrollTop = nextScrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
function focusSearch(): boolean {
|
||||
const input = searchInputRef.value;
|
||||
if (!input) return false;
|
||||
|
|
@ -225,6 +249,7 @@ defineExpose({ focusSearch });
|
|||
</div>
|
||||
<RecycleScroller
|
||||
v-if="flatNodes.length > 0 && useVirtualTree"
|
||||
ref="treeScrollerRef"
|
||||
class="sidebar-tree connection-tree-scroller min-h-0 flex-1 overflow-y-auto overflow-x-auto"
|
||||
:items="flatNodes"
|
||||
:item-size="SIDEBAR_TREE_ROW_HEIGHT"
|
||||
|
|
@ -241,6 +266,7 @@ defineExpose({ focusSearch });
|
|||
:depth="item.depth"
|
||||
:drag-disabled="isFiltering"
|
||||
:pending-rename="pendingRenameGroupId === item.node.id"
|
||||
@node-toggled="onNodeToggled"
|
||||
@search-toggle="onSearchToggle"
|
||||
@rename-started="pendingRenameGroupId = null"
|
||||
/>
|
||||
|
|
@ -254,6 +280,7 @@ defineExpose({ focusSearch });
|
|||
:depth="item.depth"
|
||||
:drag-disabled="isFiltering"
|
||||
:pending-rename="pendingRenameGroupId === item.node.id"
|
||||
@node-toggled="onNodeToggled"
|
||||
@search-toggle="onSearchToggle"
|
||||
@rename-started="pendingRenameGroupId = null"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ const props = defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
"rename-started": [];
|
||||
"node-toggled": [node: TreeNode, wasExpanded: boolean];
|
||||
"search-toggle": [node: TreeNode];
|
||||
}>();
|
||||
|
||||
|
|
@ -260,11 +261,13 @@ async function toggle() {
|
|||
if (node.type === "connection-group") {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
connectionStore.toggleConnectionGroupCollapsed(node.id);
|
||||
emit("node-toggled", node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === "saved-sql-root" || node.type === "saved-sql-folder") {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
emit("node-toggled", node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -275,11 +278,13 @@ async function toggle() {
|
|||
node.type === "group-functions"
|
||||
) {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
emit("node-toggled", node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.isExpanded) {
|
||||
node.isExpanded = false;
|
||||
emit("node-toggled", node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -324,6 +329,7 @@ async function toggle() {
|
|||
} else if (node.type === "group-triggers" && node.connectionId && node.database && node.tableName) {
|
||||
await connectionStore.loadTriggers(node.connectionId, node.database, node.tableName, node.schema);
|
||||
}
|
||||
emit("node-toggled", node, wasExpanded);
|
||||
} catch (e: any) {
|
||||
if (!wasExpanded) node.isExpanded = false;
|
||||
const errMsg = e?.message || String(e);
|
||||
|
|
|
|||
|
|
@ -29,3 +29,27 @@ export function flattenTree(nodes: TreeNode[]): FlatTreeNode[] {
|
|||
export function shouldVirtualizeFlatTree(count: number): boolean {
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
export function scrollTopForExpandedTreeNode(options: {
|
||||
expandedIndex: number;
|
||||
insertedRowCount: number;
|
||||
currentScrollTop: number;
|
||||
viewportHeight: number;
|
||||
rowHeight?: number;
|
||||
}): number {
|
||||
const rowHeight = options.rowHeight ?? SIDEBAR_TREE_ROW_HEIGHT;
|
||||
if (options.expandedIndex < 0 || options.insertedRowCount <= 0 || options.viewportHeight <= 0) {
|
||||
return options.currentScrollTop;
|
||||
}
|
||||
|
||||
const visibleRowCapacity = Math.max(1, Math.floor(options.viewportHeight / rowHeight) - 1);
|
||||
const rowsToReveal = Math.min(options.insertedRowCount, visibleRowCapacity);
|
||||
const expandedContentBottom = (options.expandedIndex + 1 + rowsToReveal) * rowHeight;
|
||||
const viewportBottom = options.currentScrollTop + options.viewportHeight;
|
||||
|
||||
if (expandedContentBottom <= viewportBottom) {
|
||||
return options.currentScrollTop;
|
||||
}
|
||||
|
||||
return Math.max(0, expandedContentBottom - options.viewportHeight);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
SIDEBAR_TREE_PRERENDER_COUNT,
|
||||
SIDEBAR_TREE_SCROLL_BUFFER,
|
||||
flattenTree,
|
||||
scrollTopForExpandedTreeNode,
|
||||
shouldVirtualizeFlatTree,
|
||||
} from "../../apps/desktop/src/composables/useFlatTree.ts";
|
||||
import type { TreeNode } from "../../apps/desktop/src/types/database.ts";
|
||||
|
|
@ -57,3 +58,27 @@ test("sidebar virtual tree keeps enough buffered rows for fast scrolling", () =>
|
|||
test("sidebar virtual tree prerenders enough rows for the first frame", () => {
|
||||
assert.ok(SIDEBAR_TREE_PRERENDER_COUNT >= 40);
|
||||
});
|
||||
|
||||
test("expanded sidebar nodes scroll enough to reveal inserted rows", () => {
|
||||
assert.equal(
|
||||
scrollTopForExpandedTreeNode({
|
||||
expandedIndex: 20,
|
||||
insertedRowCount: 2,
|
||||
currentScrollTop: 15 * SIDEBAR_TREE_ROW_HEIGHT,
|
||||
viewportHeight: 6 * SIDEBAR_TREE_ROW_HEIGHT,
|
||||
}),
|
||||
17 * SIDEBAR_TREE_ROW_HEIGHT,
|
||||
);
|
||||
});
|
||||
|
||||
test("expanded sidebar nodes keep scroll position when children are already visible", () => {
|
||||
assert.equal(
|
||||
scrollTopForExpandedTreeNode({
|
||||
expandedIndex: 4,
|
||||
insertedRowCount: 2,
|
||||
currentScrollTop: 0,
|
||||
viewportHeight: 8 * SIDEBAR_TREE_ROW_HEIGHT,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue