diff --git a/apps/desktop/src/components/sidebar/ConnectionTree.vue b/apps/desktop/src/components/sidebar/ConnectionTree.vue index 9d01ca89c8..c4f4bb4e14 100644 --- a/apps/desktop/src/components/sidebar/ConnectionTree.vue +++ b/apps/desktop/src/components/sidebar/ConnectionTree.vue @@ -6,8 +6,10 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; -import type { ObjectSourceKind, TableNameFilter, TreeNode, TreeNodeType } from "@/types/database"; +import type { ObjectSourceKind, TableInfo, TableNameFilter, TreeNode, TreeNodeType } from "@/types/database"; import { filterSidebarSearchRootsByConnectionState, filterSidebarTree, filterSidebarTreeToConnectedConnections, resolveSidebarFilterGuards } from "@/lib/sidebar/sidebarSearchTree"; +import { matchSidebarLabel } from "@/lib/sidebar/sidebarSearch"; +import { buildTableTreeNodes } from "@/lib/table/tableTree"; import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts"; import { copyNameForTreeNode, objectSourceKindForTreeNode } from "@/lib/sidebar/treeNodeClick"; import { copyToClipboard } from "@/lib/common/clipboard"; @@ -29,6 +31,8 @@ import InstallExtensionDialog from "@/components/objects/InstallExtensionDialog. import { RecycleScroller } from "vue-virtual-scroller"; import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; import LightDropdown from "@/components/ui/LightDropdown.vue"; +import LightTooltip from "@/components/ui/LightTooltip.vue"; +import { Switch } from "@/components/ui/switch"; import { cancelPendingSidebarDataOpen, runSidebarDataOpenImmediately, type SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator"; import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; import { Button } from "@/components/ui/button"; @@ -111,6 +115,7 @@ const tableSearchTimers = new Map(); const tableSearchFocusRestoreTokens = new Map(); let tableSearchFocusRestoreTokenSeq = 0; let latestTableSearchInteractionParentId: string | null = null; +let localTableSearchFocusPending = false; watch( searchQuery, @@ -157,6 +162,10 @@ watch( watch(deferredSearchQuery, (newQuery, oldQuery) => { store.sidebarSearchQuery = newQuery; + if (settingsStore.editorSettings.sidebarGlobalSearchLocal) { + if (!newQuery && oldQuery) searchRefreshedNodeIds.clear(); + return; + } const tasks: Promise[] = []; for (const root of store.treeNodes) { collectExpandedObjectSearchTargets(root, tasks, newQuery ? searchRefreshedNodeIds : undefined); @@ -357,6 +366,36 @@ function focusTableSearchInput(parentNodeId: string) { } const displayedTreeNodes = computed(() => sortConnectionListForDisplay(store.treeNodes, settingsStore.editorSettings.sidebarConnectionSortMode)); +const localTableSearchResults = ref>({}); + +const localTableSearchParentTypes = new Set(["database", "schema", "linked-server-schema", "group-tables"]); +const localTableSearchChildTypes = new Set(["table", "view", "materialized_view"]); + +function filterLocallySearchedTables(nodes: TreeNode[]): TreeNode[] { + return nodes.map((node) => { + const children = node.children ? filterLocallySearchedTables(node.children) : undefined; + const query = settingsStore.editorSettings.sidebarTableSearchLocal && localTableSearchParentTypes.has(node.type) ? store.sidebarTableSearchQueries[node.id]?.trim() : ""; + if (!query || !children) return children === node.children ? node : { ...node, children }; + + const indexed = localTableSearchResults.value[node.id]; + const matchingChildren = + indexed === null + ? children.filter((child) => localTableSearchChildTypes.has(child.type) && !!matchSidebarLabel(child.label.toLowerCase(), query.toLowerCase())) + : indexed + ? buildTableTreeNodes({ nodeId: node.id, connectionId: node.connectionId || "", database: node.database || "", schema: node.schema, catalog: node.catalog, tables: indexed.filter((entry) => !!matchSidebarLabel(entry.name.toLowerCase(), query.toLowerCase())) }) + : children.filter((child) => localTableSearchChildTypes.has(child.type) && !!matchSidebarLabel(child.label.toLowerCase(), query.toLowerCase())); + return { ...node, children: matchingChildren, isExpanded: true }; + }); +} + +async function loadLocalTableSearchResults(parentNodeId: string, refresh = false) { + try { + const entries = refresh ? await store.refreshSidebarTableSearchIndex(parentNodeId) : await store.loadSidebarTableSearchIndex(parentNodeId); + localTableSearchResults.value = { ...localTableSearchResults.value, [parentNodeId]: entries }; + } finally { + if (latestTableSearchInteractionParentId === parentNodeId) focusTableSearchInput(parentNodeId); + } +} const filteredNodes = computed(() => { let nodes = displayedTreeNodes.value; @@ -364,6 +403,8 @@ const filteredNodes = computed(() => { nodes = filterSidebarTreeToConnectedConnections(nodes, store.connectedIds); } + nodes = filterLocallySearchedTables(nodes); + const q = deferredSearchQuery.value; nodes = filterSidebarTree(nodes, q, searchCollapsedIds.value, searchableNodeTypes.value); if (q) { @@ -626,7 +667,10 @@ watch(flatNodes, (nodes) => { } } stickyScrollTop.value = 0; - void nextTick(scheduleSidebarScrollMetricsUpdate); + void nextTick(() => { + treeScrollerRef.value?.forceUpdate(true); + scheduleSidebarScrollMetricsUpdate(); + }); }); const sidebarTreeOverflowClass = computed(() => (settingsStore.editorSettings.sidebarAllowHorizontalScroll ? "overflow-x-auto sidebar-tree-horizontal-scroll" : "overflow-x-hidden")); @@ -797,11 +841,19 @@ const pasteHandlerRegistry = createSidebarPasteHandlerRegistry(); provide(sidebarTreeContextKey, { getVisibleNodes: () => selectableVisibleNodes.value, getVisibleNodeIndex: (id: string) => selectableVisibleNodeIndexById.value.get(id) ?? -1, - setTableSearchQuery: (parentNodeId, query) => { + setTableSearchQuery: (parentNodeId, query, local) => { latestTableSearchInteractionParentId = parentNodeId; store.setSidebarTableSearchQuery(parentNodeId, query); - scheduleSidebarTableSearchRefresh(parentNodeId, { restoreFocus: true }); + if (local) { + localTableSearchFocusPending = true; + void nextTick(() => { + focusTableSearchInput(parentNodeId); + localTableSearchFocusPending = false; + }); + void loadLocalTableSearchResults(parentNodeId); + } else scheduleSidebarTableSearchRefresh(parentNodeId, { restoreFocus: true }); }, + refreshTableSearchIndex: (parentNodeId) => void loadLocalTableSearchResults(parentNodeId, true), registerPasteHandler: pasteHandlerRegistry.register, }); provide(sidebarTreeRuntimeKey, sidebarTreeRuntime); @@ -1449,6 +1501,7 @@ function focusSearchAtEnd() { function onWindowKeydown(event: KeyboardEvent) { if (event.defaultPrevented) return; + if (localTableSearchFocusPending) return; if (sidebarShortcutTargetIsActive(event.target)) { if (sidebarShortcutTargetAllowsAppShortcut(event.target) && isEditConnectionShortcut(event)) { if (requestSelectedConnectionEdit()) { @@ -1641,6 +1694,9 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes }); + + + diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 79a2d91de2..997c923fd7 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -33,6 +33,7 @@ import { Archive, Square, X, + RefreshCw, } from "@lucide/vue"; import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; @@ -43,6 +44,7 @@ import ConnectionErrorIndicator from "@/components/connection/ConnectionErrorInd import ProductionContextBadge from "@/components/common/ProductionContextBadge.vue"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; import LightTooltip from "@/components/ui/LightTooltip.vue"; import type { ColumnInfo, ConnectionConfig, DatabaseType, TreeNode } from "@/types/database"; import { alignedCommentLeadingWidth, canTreeNodePin, canTreeNodeShowExpander, sidebarTreeNodeComment, trailingCommentAvailableWidth, trailingCommentGapPx, treeItemPaddingLeft, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout"; @@ -713,11 +715,21 @@ function updateTableSearchQuery(value: string | number) { if (!parentId) return; const query = String(value); if (sidebarTreeContext?.setTableSearchQuery) { - sidebarTreeContext.setTableSearchQuery(parentId, query); + sidebarTreeContext.setTableSearchQuery(parentId, query, settingsStore.editorSettings.sidebarTableSearchLocal); return; } connectionStore.setSidebarTableSearchQuery(parentId, query); - void connectionStore.refreshSidebarTableSearch(parentId); + if (!settingsStore.editorSettings.sidebarTableSearchLocal) void connectionStore.refreshSidebarTableSearch(parentId); +} + +function updateTableSearchLocal(value: boolean) { + settingsStore.updateEditorSettings({ sidebarTableSearchLocal: value }); + updateTableSearchQuery(tableSearchValue.value); +} + +function refreshTableSearchIndex() { + const parentId = tableSearchParentId.value; + if (parentId) sidebarTreeContext?.refreshTableSearchIndex?.(parentId); } function clearTableSearchQuery() { @@ -1061,8 +1073,8 @@ function onKeydown(event: KeyboardEvent) {