fix(sidebar): cancel stale table search work across modes

This commit is contained in:
jearhe authored and GitHub committed 2026-08-16 14:09:50 +08:00
1 parent e671b3da7d
commit 71136377e5
8 files changed
+763 -24

No files matched your search

@@ -40,6 +40,7 @@ import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
import { createSidebarTreeRuntime, sidebarTreeRuntimeKey, type SidebarTreeRuntimeHostInstance } from "@/lib/sidebar/sidebarTreeRuntime";
import { createSidebarPasteHandlerRegistry } from "@/lib/sidebar/sidebarPasteHandlerRegistry";
import { insertSidebarTableSearchControls, isSidebarTableSearchControlNode } from "@/lib/sidebar/sidebarTableSearchControl";
import { createSidebarTableSearchDebouncer, loadOrBuildSidebarTableSearchIndex, scheduleExclusiveSidebarTableSearchDebounce } from "@/lib/sidebar/sidebarTableSearchIndex";
import TreeItem from "./TreeItem.vue";
import SidebarTreeRuntimeHost from "./SidebarTreeRuntimeHost.vue";
import SidebarTreeItemDialogs from "./SidebarTreeItemDialogs.vue";
@@ -149,7 +150,10 @@ const searchRefreshedNodeIds = searchExpansionState.filteredNodeIds;
// touched group open and reloading it again.
const searchAutoExpandedNodeIds = searchExpansionState.autoExpandedNodeIds;
let searchTimer: number | undefined;
const tableSearchTimers = new Map<string, number>();
const remoteTableSearchDebouncer = createSidebarTableSearchDebouncer();
// Local-mode table searches debounce like remote ones: rapid typing coalesces
// into a single index load/build instead of one full build per keystroke.
const localTableSearchDebouncer = createSidebarTableSearchDebouncer();
const tableSearchFocusRestoreTokens = new Map<string, number>();
let tableSearchFocusRestoreTokenSeq = 0;
let latestTableSearchInteractionParentId: string | null = null;
@@ -244,8 +248,8 @@ watch(
if (parentNodeIds.length === 0) return;
for (const parentNodeId of parentNodeIds) {
window.clearTimeout(tableSearchTimers.get(parentNodeId));
tableSearchTimers.delete(parentNodeId);
remoteTableSearchDebouncer.cancel(parentNodeId);
localTableSearchDebouncer.cancel(parentNodeId);
tableSearchFocusRestoreTokens.delete(parentNodeId);
store.setSidebarTableSearchQuery(parentNodeId, "");
}
@@ -486,15 +490,17 @@ function clearSearchScopeFilter() {
}
function scheduleSidebarTableSearchRefresh(parentNodeId: string, options?: { focusRestore?: TableSearchFocusRestore }) {
window.clearTimeout(tableSearchTimers.get(parentNodeId));
if (isTreeSearchFiltering.value) return;
if (isTreeSearchFiltering.value) {
remoteTableSearchDebouncer.cancel(parentNodeId);
localTableSearchDebouncer.cancel(parentNodeId);
return;
}
const restoreToken = options?.focusRestore?.shouldRestoreFocus ? ++tableSearchFocusRestoreTokenSeq : 0;
if (restoreToken) {
tableSearchFocusRestoreTokens.clear();
tableSearchFocusRestoreTokens.set(parentNodeId, restoreToken);
}
const timer = window.setTimeout(() => {
tableSearchTimers.delete(parentNodeId);
scheduleExclusiveSidebarTableSearchDebounce(parentNodeId, remoteTableSearchDebouncer, localTableSearchDebouncer, () => {
void store.refreshSidebarTableSearch(parentNodeId).then(() => {
if (!restoreToken) return;
if (tableSearchFocusRestoreTokens.get(parentNodeId) !== restoreToken) return;
@@ -503,8 +509,7 @@ function scheduleSidebarTableSearchRefresh(parentNodeId: string, options?: { foc
if (!focusRestore || !isCurrentTableSearchInteraction(focusRestore)) return;
restoreTableSearchInput(focusRestore);
});
}, 250);
tableSearchTimers.set(parentNodeId, timer);
});
}
function captureTableSearchFocus(parentNodeId: string): TableSearchFocusRestore {
@@ -607,10 +612,35 @@ function filterGloballyIndexedRegexTables(nodes: TreeNode[]): TreeNode[] {
return mergeSidebarRegexIndexScopes(nodes, matchingScopes);
}
function scheduleLocalSidebarTableSearchRefresh(parentNodeId: string, focusRestore?: TableSearchFocusRestore) {
if (isTreeSearchFiltering.value) {
localTableSearchDebouncer.cancel(parentNodeId);
remoteTableSearchDebouncer.cancel(parentNodeId);
return;
}
scheduleExclusiveSidebarTableSearchDebounce(parentNodeId, localTableSearchDebouncer, remoteTableSearchDebouncer, () => {
void loadLocalTableSearchResults(parentNodeId, false, focusRestore);
});
}
async function loadLocalTableSearchResults(parentNodeId: string, refresh = false, focusRestore?: TableSearchFocusRestore) {
try {
const entries = refresh ? await store.refreshSidebarTableSearchIndex(parentNodeId) : await store.loadSidebarTableSearchIndex(parentNodeId);
localTableSearchResults.value = { ...localTableSearchResults.value, [parentNodeId]: entries };
// A missing persisted index (null) means this scope was never indexed, so
// only the currently loaded first page of children is searchable. That
// silently misses alphabetically-late tables (e.g. "T_Erp_Nc_SuPlan_List"
// for "erpncs") until an explicit index refresh happens. Build the index
// on the first search so results always cover the complete table set,
// matching the behavior of an explicit refresh. The scope key deduplicates
// concurrent full builds, and an empty (cleared) query never builds.
const query = store.sidebarTableSearchQueries[parentNodeId]?.trim() ?? "";
const entries = await loadOrBuildSidebarTableSearchIndex(
parentNodeId,
query,
() => store.loadSidebarTableSearchIndex(parentNodeId),
() => store.refreshSidebarTableSearchIndex(parentNodeId),
refresh,
);
if (query || refresh) localTableSearchResults.value = { ...localTableSearchResults.value, [parentNodeId]: entries };
} finally {
if (focusRestore) restoreTableSearchInput(focusRestore);
}
@@ -1099,7 +1129,7 @@ provide(sidebarTreeContextKey, {
restoreTableSearchInput(focusRestore);
localTableSearchFocusPending = false;
});
void loadLocalTableSearchResults(parentNodeId, false, focusRestore);
scheduleLocalSidebarTableSearchRefresh(parentNodeId, focusRestore);
} else scheduleSidebarTableSearchRefresh(parentNodeId, { focusRestore });
},
refreshTableSearchIndex: (parentNodeId) => void loadLocalTableSearchResults(parentNodeId, true),
@@ -1999,10 +2029,8 @@ onUnmounted(() => {
resetSidebarTreeDialogState();
window.removeEventListener("keydown", onWindowKeydown);
cancelPendingSidebarDataOpen();
for (const timer of tableSearchTimers.values()) {
window.clearTimeout(timer);
}
tableSearchTimers.clear();
remoteTableSearchDebouncer.cancelAll();
localTableSearchDebouncer.cancelAll();
tableSearchFocusRestoreTokens.clear();
latestTableSearchInteractionParentId = null;
latestTableSearchInteractionId = 0;