From ee85033068ed4a868ee57ddf396777935794fb5e Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 14 Jul 2026 18:10:24 +0800 Subject: [PATCH] fix(sidebar): centralize tree action handling --- .../src/components/sidebar/ConnectionTree.vue | 507 +++++- .../sidebar/SidebarAsyncDialogError.vue | 14 + .../sidebar/SidebarAsyncDialogLoading.vue | 11 + .../sidebar/SidebarTreeItemDialogs.vue | 503 ++++++ .../src/components/sidebar/TreeItem.vue | 1606 ++++++++--------- .../components/sidebar/sidebarAsyncDialogs.ts | 24 + .../sidebar/sidebarTreeDialogState.ts | 139 ++ .../src/components/ui/CustomContextMenu.vue | 9 +- .../composables/__tests__/useFlatTree.spec.ts | 131 ++ apps/desktop/src/composables/useFlatTree.ts | 120 ++ .../src/lib/__tests__/app/pinnedItems.spec.ts | 51 +- .../sidebar/sidebarActionTarget.spec.ts | 62 + .../sidebarDataOpenCoordinator.spec.ts | 95 + apps/desktop/src/lib/app/pinnedItems.ts | 84 +- .../src/lib/sidebar/sidebarActionTarget.ts | 45 + .../src/lib/sidebar/sidebarDangerDialog.ts | 22 + .../lib/sidebar/sidebarDataOpenCoordinator.ts | 60 + .../lib/sidebar/sidebarDatabaseOpenState.ts | 4 +- .../desktop/src/lib/tabs/dataTabActivation.ts | 4 +- apps/desktop/src/stores/connectionStore.ts | 30 +- .../app-tests/codemirrorSqlDialect.test.ts | 6 +- packages/app-tests/dataTabActivation.test.ts | 4 + .../app-tests/sidebarContextMenuHost.test.ts | 38 + .../sidebarDatabaseOpenState.test.ts | 9 + .../app-tests/sidebarDialogRouting.test.ts | 55 + packages/app-tests/sidebarLargeTree.test.ts | 54 + .../app-tests/sidebarTreeAffordances.test.ts | 24 + .../app-tests/sidebarTreeItemCleanup.test.ts | 26 + 28 files changed, 2705 insertions(+), 1032 deletions(-) create mode 100644 apps/desktop/src/components/sidebar/SidebarAsyncDialogError.vue create mode 100644 apps/desktop/src/components/sidebar/SidebarAsyncDialogLoading.vue create mode 100644 apps/desktop/src/components/sidebar/SidebarTreeItemDialogs.vue create mode 100644 apps/desktop/src/components/sidebar/sidebarAsyncDialogs.ts create mode 100644 apps/desktop/src/components/sidebar/sidebarTreeDialogState.ts create mode 100644 apps/desktop/src/composables/__tests__/useFlatTree.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/sidebar/sidebarActionTarget.spec.ts create mode 100644 apps/desktop/src/lib/__tests__/sidebar/sidebarDataOpenCoordinator.spec.ts create mode 100644 apps/desktop/src/lib/sidebar/sidebarActionTarget.ts create mode 100644 apps/desktop/src/lib/sidebar/sidebarDangerDialog.ts create mode 100644 apps/desktop/src/lib/sidebar/sidebarDataOpenCoordinator.ts create mode 100644 packages/app-tests/sidebarContextMenuHost.test.ts create mode 100644 packages/app-tests/sidebarDialogRouting.test.ts create mode 100644 packages/app-tests/sidebarLargeTree.test.ts create mode 100644 packages/app-tests/sidebarTreeAffordances.test.ts create mode 100644 packages/app-tests/sidebarTreeItemCleanup.test.ts diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 4573c759f..a85870ffd 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -6,10 +6,10 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; -import type { TreeNode, TreeNodeType } from "@/types/database"; +import type { ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database"; import { filterSidebarSearchRootsByConnectionState, filterSidebarTree } from "@/lib/sidebar/sidebarSearchTree"; import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts"; -import { copyNameForTreeNode } from "@/lib/sidebar/treeNodeClick"; +import { copyNameForTreeNode, objectSourceKindForTreeNode } from "@/lib/sidebar/treeNodeClick"; import { copyToClipboard } from "@/lib/common/clipboard"; import { connectionPasteTargetGroupId, selectedConnectionClipboardNodes, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection"; import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch"; @@ -17,14 +17,24 @@ import { usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport"; import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import { activeTabSidebarTarget, findSidebarNodeForActiveTab, findSidebarNodeForTarget, findNodePathForTarget, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection, type ActiveTabSidebarTarget, type SidebarNodeScrollAlign } from "@/lib/sidebar/sidebarActiveTabTarget"; import { findLoadedTableTargetForCandidate, queryContextTargetFromCandidate, queryCursorTableCandidate, type QueryCursorTableCandidate } from "@/lib/sql/queryCursorTableTarget"; -import { SIDEBAR_TREE_ROW_HEIGHT, SIDEBAR_TREE_PRERENDER_COUNT, SIDEBAR_TREE_SCROLL_BUFFER, flattenTree, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree"; +import { createFlatTreeIndex, SIDEBAR_TREE_ROW_HEIGHT, SIDEBAR_TREE_PRERENDER_COUNT, SIDEBAR_TREE_SCROLL_BUFFER, flattenTree, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree"; import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext"; import { createSidebarPasteHandlerRegistry } from "@/lib/sidebar/sidebarPasteHandlerRegistry"; import { insertSidebarTableSearchControls, isSidebarTableSearchControlNode } from "@/lib/sidebar/sidebarTableSearchControl"; import TreeItem from "./TreeItem.vue"; +import SidebarTreeItemDialogs from "./SidebarTreeItemDialogs.vue"; +import InstallExtensionDialog from "@/components/objects/InstallExtensionDialog.vue"; import { RecycleScroller } from "vue-virtual-scroller"; import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; import LightDropdown from "@/components/ui/LightDropdown.vue"; +import { cancelPendingSidebarDataOpen, runSidebarDataOpenImmediately, type SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator"; +import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; +import { codeMirrorSqlDialect } from "@/lib/database/jdbcDialect"; +import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter"; +import { createSidebarActionTarget, findSidebarActionTarget, type SidebarActionTarget } from "@/lib/sidebar/sidebarActionTarget"; +import type { SidebarDangerDialogRequest } from "@/lib/sidebar/sidebarDangerDialog"; +import { resetSidebarTreeDialogState } from "./sidebarTreeDialogState"; +import { SidebarDangerConfirmDialog, SidebarDdlViewDialog, SidebarObjectSourceDialog, SidebarProcedureExecutionDialog, SidebarVisibleDatabasesDialog, SidebarVisibleSchemasDialog } from "./sidebarAsyncDialogs"; const { t } = useI18n(); const store = useConnectionStore(); @@ -39,6 +49,37 @@ const pointerInsideTree = ref(false); const treeScrollerRef = ref | null>(null); const plainTreeScrollerRef = ref(null); const sidebarScrollbarTrackRef = ref(null); +const sidebarContextMenuRef = ref<{ close: () => void } | null>(null); +const sidebarContextMenuItems = ref([]); +const sidebarContextMenuTarget = ref(null); +const sidebarDangerDialogRequest = ref(null); +const sidebarDangerDialogOpen = ref(false); +const sidebarDangerDialogConfirming = ref(false); +const sidebarTreeItemDialogController = ref | null>(null); +const sidebarInstallExtensionTarget = ref(null); +const sidebarInstallExtensionDialogRef = ref | null>(null); +const sidebarDdlTarget = ref(null); +const sidebarDdlOpen = ref(false); +const sidebarObjectSourceTarget = ref<{ node: TreeNode; initialEditing: boolean } | null>(null); +const sidebarObjectSourceOpen = ref(false); +const sidebarProcedureTarget = ref(null); +const sidebarProcedureOpen = ref(false); +const sidebarVisibleDatabasesTarget = ref(null); +const sidebarVisibleDatabasesOpen = ref(false); +const sidebarVisibleSchemasTarget = ref(null); +const sidebarVisibleSchemasOpen = ref(false); +let sidebarActionGeneration = 0; +const sidebarDdlDatabaseType = computed(() => { + const connectionId = sidebarDdlTarget.value?.connectionId; + return connectionId ? effectiveDatabaseTypeForConnection(store.getConfig(connectionId)) : undefined; +}); +const sidebarObjectSourceType = computed(() => (sidebarObjectSourceTarget.value ? objectSourceKindForTreeNode(sidebarObjectSourceTarget.value.node.type) : null)); +const sidebarObjectSourceDatabaseType = computed(() => { + const connectionId = sidebarObjectSourceTarget.value?.node.connectionId; + return connectionId ? effectiveDatabaseTypeForConnection(store.getConfig(connectionId)) : undefined; +}); +const sidebarObjectSourceDialect = computed(() => codeMirrorSqlDialect(sidebarObjectSourceDatabaseType.value)); +const sidebarObjectSourceFormatDialect = computed(() => sqlFormatDialectForDbType(sidebarObjectSourceDatabaseType.value)); type SearchScope = "connection" | "database" | "schema" | "table" | "view"; const selectedSearchScopes = ref([]); const searchCollapsedIds = ref>(new Set()); @@ -288,13 +329,19 @@ const flatNodes = computed(() => activeQueries: store.sidebarTableSearchQueries, }), ); -const visibleNodes = computed(() => flatNodes.value.map((item) => item.node)); -const selectableVisibleNodes = computed(() => visibleNodes.value.filter((node) => !isSidebarTableSearchControlNode(node))); -const selectableVisibleNodeIndexById = computed(() => { - const next = new Map(); - selectableVisibleNodes.value.forEach((node, index) => next.set(node.id, index)); - return next; -}); +// Build all lookup tables in one linear pass whenever the visible tree changes. +// Selection, scrolling and sticky headers then avoid repeated full-array scans. +const flatTreeIndex = computed(() => + createFlatTreeIndex(flatNodes.value, { + isSelectable: (node) => !isSidebarTableSearchControlNode(node), + isBoundary: (type) => type === "connection" || type === "connection-group", + isDatabaseContainer: (type) => DATABASE_LEVEL_TYPES.has(type), + isSchemaContainer: (type) => SCHEMA_LEVEL_TYPES.has(type), + }), +); +const visibleNodes = computed(() => flatTreeIndex.value.visibleNodes); +const selectableVisibleNodes = computed(() => flatTreeIndex.value.selectableVisibleNodes); +const selectableVisibleNodeIndexById = computed(() => flatTreeIndex.value.selectableVisibleNodeIndexById); const useVirtualTree = computed(() => shouldVirtualizeFlatTree(flatNodes.value.length)); const activeTab = computed(() => queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId)); @@ -382,51 +429,19 @@ const stickyNode = computed(() => { if (len === 0) return null; const topIndex = Math.min(Math.floor(stickyScrollTop.value / SIDEBAR_TREE_ROW_HEIGHT), len - 1); - // flatNodes is a DFS preorder spanning ALL connections, so walking up from a - // leaf visits `... -> schema -> database -> connection -> `. - // Stop at the connection boundary so the sticky row never leaks across into a - // different connection's subtree (e.g. MySQL's last database sticking while - // scrolling Dameng). Within one connection: track both candidates and prefer - // database-level; only fall back to schema when the whole path has no - // database-level container (Dameng/Oracle-style trees). - let schemaCandidate: FlatTreeNode | null = null; - let schemaCandidateTop = 0; - for (let i = topIndex; i >= 0; i--) { - const item = nodes[i]; - if (item.type === "connection" || item.type === "connection-group") break; - if (DATABASE_LEVEL_TYPES.has(item.type)) { - const rowTop = i * SIDEBAR_TREE_ROW_HEIGHT; - return stickyScrollTop.value > rowTop ? item : null; - } - if (item.type === "schema" && !schemaCandidate) { - schemaCandidate = item; - schemaCandidateTop = i * SIDEBAR_TREE_ROW_HEIGHT; - } - } - if (!schemaCandidate) return null; - return stickyScrollTop.value > schemaCandidateTop ? schemaCandidate : null; + const containerIndex = flatTreeIndex.value.stickyContainerIndexByIndex[topIndex] ?? -1; + if (containerIndex < 0) return null; + return stickyScrollTop.value > containerIndex * SIDEBAR_TREE_ROW_HEIGHT ? nodes[containerIndex] : null; }); const stickyHeaderStyle = computed(() => { const node = stickyNode.value; if (!node) return {}; - const nodes = flatNodes.value; - const currentIndex = nodes.findIndex((item) => item.id === node.id); + const currentIndex = flatTreeIndex.value.flatNodeIndexById.get(node.id) ?? -1; if (currentIndex < 0) return {}; - // Look forward for the next sibling container at the SAME level as the sticky - // node so the push-up only fires when a peer scrolls in (database-to-database, - // or schema-to-schema for Dameng/Oracle), never schema-into-database. Stop at - // the connection boundary so we never reach into the next connection's rows. - const nextTypes = SCHEMA_LEVEL_TYPES.has(node.type) ? SCHEMA_LEVEL_TYPES : DATABASE_LEVEL_TYPES; - let nextDatabaseIndex = -1; - for (let i = currentIndex + 1; i < nodes.length; i++) { - const item = nodes[i]; - if (item.type === "connection" || item.type === "connection-group") break; - if (nextTypes.has(item.type)) { - nextDatabaseIndex = i; - break; - } - } + // The next peer index is precomputed with the flat-tree snapshot so scrolling + // never scans the remaining tree. Connection boundaries reset the lookup. + const nextDatabaseIndex = SCHEMA_LEVEL_TYPES.has(node.type) ? flatTreeIndex.value.nextSchemaContainerIndexByIndex[currentIndex] : flatTreeIndex.value.nextDatabaseContainerIndexByIndex[currentIndex]; if (nextDatabaseIndex < 0) return {}; const distanceToNext = nextDatabaseIndex * SIDEBAR_TREE_ROW_HEIGHT - stickyScrollTop.value; if (distanceToNext >= SIDEBAR_TREE_ROW_HEIGHT) return {}; @@ -438,6 +453,11 @@ const stickyHeaderStyle = computed(() => { // Reset tracking when the tree rebuilds (connect/disconnect/collapse) so a // stale scrollTop doesn't keep the overlay mounted after a structural change. watch(flatNodes, () => { + // Menu actions originate from a rendered row instance. Close the singleton + // before a structural update can recycle that row onto another node. + sidebarContextMenuRef.value?.close(); + sidebarContextMenuItems.value = []; + sidebarContextMenuTarget.value = null; stickyScrollTop.value = 0; void nextTick(scheduleSidebarScrollMetricsUpdate); }); @@ -567,7 +587,7 @@ function topOcclusionHeightForSidebarNode(nodeId: string): number { async function scrollToSidebarNode(nodeId: string, options?: { align?: SidebarNodeScrollAlign }) { await nextTick(); - const index = flatNodes.value.findIndex((item) => item.id === nodeId); + const index = flatTreeIndex.value.flatNodeIndexById.get(nodeId) ?? -1; const scroller = currentTreeScroller(); if (!scroller || index < 0) return; @@ -845,10 +865,177 @@ function onSearchToggle(node: TreeNode) { searchCollapsedIds.value = next; } +function openSidebarContextMenu(event: MouseEvent, node: TreeNode, items: ContextMenuItem[], openContextMenu: (event: MouseEvent, itemsOverride?: ContextMenuItem[]) => void) { + sidebarContextMenuTarget.value = createSidebarActionTarget(node); + sidebarContextMenuItems.value = items; + // Pass the current row's resolved menu atomically. Waiting for the items prop + // to flush would let the singleton menu briefly reuse the previous row menu. + openContextMenu(event, items); +} + +function openSidebarDangerDialog(request: SidebarDangerDialogRequest) { + sidebarDangerDialogRequest.value = request; + sidebarDangerDialogConfirming.value = false; + sidebarDangerDialogOpen.value = true; +} + +async function confirmSidebarDangerDialog() { + const request = sidebarDangerDialogRequest.value; + if (!request || sidebarDangerDialogConfirming.value) return; + if (request.closeOnConfirm !== false) sidebarDangerDialogOpen.value = false; + sidebarDangerDialogConfirming.value = true; + try { + await request.confirm(); + sidebarDangerDialogOpen.value = false; + } finally { + sidebarDangerDialogConfirming.value = false; + } +} + +function updateSidebarDangerDialogOption(event: Event) { + const option = sidebarDangerDialogRequest.value?.option; + if (!option) return; + option.checked = (event.target as HTMLInputElement).checked; + void option.onChange?.(option.checked); +} + +function updateSidebarTreeItemDialogController(controller: Record | null) { + sidebarTreeItemDialogController.value = controller; +} + +async function openSidebarInstallExtension(node: TreeNode) { + sidebarInstallExtensionTarget.value = createSidebarActionTarget(node); + await nextTick(); + sidebarInstallExtensionDialogRef.value?.show(); +} + +function beginSidebarAction(): number { + sidebarActionGeneration += 1; + sidebarDdlOpen.value = false; + sidebarObjectSourceOpen.value = false; + sidebarProcedureOpen.value = false; + sidebarVisibleDatabasesOpen.value = false; + sidebarVisibleSchemasOpen.value = false; + sidebarDdlTarget.value = null; + sidebarObjectSourceTarget.value = null; + sidebarProcedureTarget.value = null; + sidebarVisibleDatabasesTarget.value = null; + sidebarVisibleSchemasTarget.value = null; + return sidebarActionGeneration; +} + +function tableDdlObjectTypeForSidebarNode(type: TreeNodeType): ObjectSourceKind | undefined { + if (type === "view") return "VIEW"; + if (type === "materialized_view") return "MATERIALIZED_VIEW"; + return undefined; +} + +function openSidebarDdl(node: TreeNode) { + if (!node.connectionId || !node.database) return; + beginSidebarAction(); + sidebarDdlTarget.value = createSidebarActionTarget(node); + sidebarDdlOpen.value = true; +} + +function openSidebarObjectSource(node: TreeNode, initialEditing: boolean) { + if (!node.connectionId || !node.database || !objectSourceKindForTreeNode(node.type)) return; + const target = createSidebarActionTarget(node); + const requestGeneration = beginSidebarAction(); + void store + .ensureConnected(target.connectionId!) + .then(() => { + if (requestGeneration !== sidebarActionGeneration) return; + store.activeConnectionId = target.connectionId!; + sidebarObjectSourceTarget.value = { node: target, initialEditing }; + sidebarObjectSourceOpen.value = true; + }) + .catch((error: any) => { + if (requestGeneration === sidebarActionGeneration) toast(error?.message || String(error), 5000); + }); +} + +function openSidebarProcedure(node: TreeNode) { + if (node.type !== "procedure" || !node.connectionId || !node.database) return; + beginSidebarAction(); + sidebarProcedureTarget.value = createSidebarActionTarget(node); + sidebarProcedureOpen.value = true; +} + +function openSidebarData(node: TreeNode, requireSelection: boolean, runner: (node: TreeNode, request: SidebarDataOpenRequest) => Promise) { + const target = createSidebarActionTarget(node); + runSidebarDataOpenImmediately((request) => { + if (requireSelection && store.selectedTreeNodeId !== target.id) return; + return runner(target, request); + }); +} + +function openSidebarVisibleDatabases(node: TreeNode) { + if (node.type !== "connection" || !node.connectionId) return; + beginSidebarAction(); + sidebarVisibleDatabasesTarget.value = createSidebarActionTarget(node); + sidebarVisibleDatabasesOpen.value = true; +} + +function openSidebarVisibleSchemas(node: TreeNode) { + if ((node.type !== "connection" && node.type !== "database") || !node.connectionId) return; + const database = node.type === "database" ? node.database : store.getConfig(node.connectionId)?.database; + if (database == null) return; + beginSidebarAction(); + sidebarVisibleSchemasTarget.value = createSidebarActionTarget({ ...node, database }); + sidebarVisibleSchemasOpen.value = true; +} + +function openSidebarProcedureSql(sql: string) { + const target = sidebarProcedureTarget.value; + if (!target?.connectionId || !target.database || !sql) return; + const tabId = queryStore.createTab(target.connectionId, target.database, `Execute - ${target.label}`, "query", target.schema); + queryStore.updateSql(tabId, sql); +} + +async function executeSidebarProcedureSql(sql: string) { + const target = sidebarProcedureTarget.value; + if (!target?.connectionId || !target.database || !sql) return; + const tabId = queryStore.createTab(target.connectionId, target.database, `Execute - ${target.label}`, "query", target.schema); + queryStore.updateSql(tabId, sql); + await queryStore.executeTabSql(tabId, sql); +} + +async function refreshSidebarActionTarget() { + const target = sidebarObjectSourceTarget.value?.node || sidebarDdlTarget.value; + if (!target) return; + const currentTarget = findSidebarActionTarget(store.treeNodes, target); + if (!currentTarget) return; + try { + await store.refreshTreeNode(currentTarget); + } catch (error: any) { + toast(error?.message || String(error), 5000); + } +} + +watch(sidebarDdlOpen, (open) => { + if (!open) sidebarDdlTarget.value = null; +}); + +watch(sidebarObjectSourceOpen, (open) => { + if (!open) sidebarObjectSourceTarget.value = null; +}); + +watch(sidebarProcedureOpen, (open) => { + if (!open) sidebarProcedureTarget.value = null; +}); + +watch(sidebarVisibleDatabasesOpen, (open) => { + if (!open) sidebarVisibleDatabasesTarget.value = null; +}); + +watch(sidebarVisibleSchemasOpen, (open) => { + if (!open) sidebarVisibleSchemasTarget.value = null; +}); + function collapseAllTreeNodes() { store.collapseAllTreeNodes(); if (isSearching.value) { - searchCollapsedIds.value = new Set(flatNodes.value.filter((item) => item.node.children?.length).map((item) => item.id)); + searchCollapsedIds.value = new Set(flatTreeIndex.value.expandableNodeIds); } } @@ -866,7 +1053,7 @@ async function selectActiveTabSidebarNode(options: { scroll: boolean }) { await nextTick(); - const index = flatNodes.value.findIndex((item) => item.id === match.id); + const index = flatTreeIndex.value.flatNodeIndexById.get(match.id) ?? -1; const scroller = currentTreeScroller(); if (!scroller || index < 0) return; @@ -985,7 +1172,7 @@ function isEditConnectionShortcut(event: KeyboardEvent): boolean { function requestSelectedConnectionEdit(): boolean { const selectedNodeId = store.selectedTreeNodeId; - const currentNode = selectedNodeId ? visibleNodes.value.find((node) => node.id === selectedNodeId) : null; + const currentNode = selectedNodeId ? flatTreeIndex.value.nodeById.get(selectedNodeId) : null; if (!currentNode) return false; const editTarget = selectedConnectionEditTarget(currentNode, selectedSidebarNodesInVisibleOrder()); if (!editTarget) return false; @@ -1025,7 +1212,7 @@ function requestSelectedSidebarPaste(): boolean { const clipboard = store.treeClipboard; const selectedNodeId = store.selectedTreeNodeId; if (clipboard?.kind === "connection-copy") { - const selectedNode = selectedNodeId ? visibleNodes.value.find((node) => node.id === selectedNodeId) : null; + const selectedNode = selectedNodeId ? flatTreeIndex.value.nodeById.get(selectedNodeId) : null; const targetGroupId = connectionPasteTargetGroupId(selectedNode, (connectionId) => store.groupIdForConnection(connectionId)); void store .pasteConnectionClipboard(targetGroupId) @@ -1045,7 +1232,19 @@ onMounted(() => { }); onUnmounted(() => { + sidebarActionGeneration += 1; + sidebarContextMenuTarget.value = null; + sidebarContextMenuItems.value = []; + sidebarDdlTarget.value = null; + sidebarObjectSourceTarget.value = null; + sidebarProcedureTarget.value = null; + sidebarVisibleDatabasesTarget.value = null; + sidebarVisibleSchemasTarget.value = null; + sidebarTreeItemDialogController.value = null; + sidebarDangerDialogRequest.value = null; + resetSidebarTreeDialogState(); window.removeEventListener("keydown", onWindowKeydown); + cancelPendingSidebarDataOpen(); for (const timer of tableSearchTimers.values()) { window.clearTimeout(timer); } @@ -1107,60 +1306,172 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes }); /> -
- - - -
- +
+ - -
- - + + + + + + + + + + + + + + +
{{ t("sidebar.noConnections") }}
diff --git a/apps/desktop/src/components/sidebar/SidebarAsyncDialogError.vue b/apps/desktop/src/components/sidebar/SidebarAsyncDialogError.vue new file mode 100644 index 000000000..c1c2d3a5c --- /dev/null +++ b/apps/desktop/src/components/sidebar/SidebarAsyncDialogError.vue @@ -0,0 +1,14 @@ + + + diff --git a/apps/desktop/src/components/sidebar/SidebarAsyncDialogLoading.vue b/apps/desktop/src/components/sidebar/SidebarAsyncDialogLoading.vue new file mode 100644 index 000000000..d50dcdbb6 --- /dev/null +++ b/apps/desktop/src/components/sidebar/SidebarAsyncDialogLoading.vue @@ -0,0 +1,11 @@ + + + diff --git a/apps/desktop/src/components/sidebar/SidebarTreeItemDialogs.vue b/apps/desktop/src/components/sidebar/SidebarTreeItemDialogs.vue new file mode 100644 index 000000000..8f770c73e --- /dev/null +++ b/apps/desktop/src/components/sidebar/SidebarTreeItemDialogs.vue @@ -0,0 +1,503 @@ + + + diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 417e48026..d22df9bd4 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -1,5 +1,5 @@