161ce497109c05416bfe719cb391667d64c580b2 chmalee Tue Mar 17 15:15:18 2026 -0700 Merge hgSearch results by trackName before looping over results, which prevents having multiple sections per hgFindSpec as some tracks have multiple hgFindSpecs that can match for a given term. Also fix category display in a similar way so that the same track does not show up in the tree multiple times which breaks checkbox selection, refs #34334 and #37078 diff --git src/hg/js/hgSearch.js src/hg/js/hgSearch.js index 895fa7de179..965f15cabfa 100644 --- src/hg/js/hgSearch.js +++ src/hg/js/hgSearch.js @@ -128,34 +128,36 @@ function tracksToTree(trackList) { /* Go through the list of all tracks for this assembly, filling * out the necessary information for jstree to be able to work. * Only include categories that have results. * The groups object will get filled out along the way. */ trackGroups = uiState.trackGroups; var ret = []; var parentsHash = {}; var groups = {}; visibleTrackGroup.children = []; visibleTrackGroup.numMatches = 0; visibleTrackGroup.searchTime = -1; hiddenTrackGroup.children = []; hiddenTrackGroup.numMatches = 0; hiddenTrackGroup.searchTime = -1; + var seenTracks = {}; _.each(trackList, function(track) { - if (!(track.id in uiState.resultHash)) { + if (!(track.id in uiState.resultHash) || track.id in seenTracks) { return; } + seenTracks[track.id] = true; var newCateg = {}; _.assign(newCateg, track); newCateg.text = track.longLabel; newCateg.text = track.label; var group = track.group; newCateg.state = {checked: true, opened: true}; newCateg.text = track.label; newCateg.li_attr = {title: track.description}; newCateg.numMatches = uiState.resultHash[newCateg.id].matches.length; newCateg.searchTime = uiState.resultHash[newCateg.id].searchTime; addCountAndTimeToLabel(newCateg); if (track.visibility > 0) { if (!groups.visible) { groups.visible = visibleTrackGroup; } @@ -537,62 +539,63 @@ $("#searchBarSearchString").val(""); } if (uiState && uiState.positionMatches && uiState.positionMatches.length > 0) { // clear the old search results if there were any: parentDiv.empty(); // create the elements that will hold results: var newList = document.createElement("ul"); var noUlStyle = document.createAttribute("class"); noUlStyle.value = "ulNoStyle"; newList.setAttributeNode(noUlStyle); parentDiv.append(newList); clearOldFacetCounts(); var categoryCount = 0; - // Loop through categories of match (public hubs, help docs, a single track, ... - _.each(uiState.positionMatches, function(categ) { - let title = categ.name; + // Loop through merged results (resultHash combines matches from + // multiple hgFindSpecs for the same track into one entry) + _.each(Object.keys(uiState.resultHash), function(title) { + let categ = uiState.resultHash[title]; let searchDesc = categ.description; let matches = categ.matches; - let numMatches = matches.length; - let newListObj = document.createElement("li"); + let idKey = title + 'Results'; let idAttr = document.createAttribute("id"); - idAttr.value = title + 'Results'; + idAttr.value = idKey; + let newListObj = document.createElement("li"); newListObj.setAttributeNode(idAttr); let noLiStyle = document.createAttribute("class"); noLiStyle.value = "liNoStyle"; newListObj.setAttributeNode(noLiStyle); let inp = document.createElement("input"); inp.type = "hidden"; - inp.id = idAttr.value + categoryCount; + inp.id = idKey + categoryCount; inp.value = "0"; newListObj.appendChild(inp); let ctrlImg = document.createElement("img"); ctrlImg.height = "18"; ctrlImg.width = "18"; - ctrlImg.id = idAttr.value + categoryCount + "_button"; + ctrlImg.id = idKey + categoryCount + "_button"; ctrlImg.src = "../images/remove_sm.gif"; newListObj.appendChild(ctrlImg); let descText = document.createTextNode(" " + searchDesc + ":"); newListObj.appendChild(descText); // Now loop through each actual hit on this table and unpack onto list let subList = document.createElement("ul"); // only print the first 10 at first printMatches(subList, matches.slice(0,10), title, searchDesc, false); if (matches.length > 10) { - let idStr = idAttr.value + "_" + categoryCount; + let idStr = idKey + "_" + categoryCount; let showMoreLi = document.createElement("li"); showMoreLi.id = idStr; showMoreLi.classList.add("liNoStyle","searchResult"); let showMoreInp = document.createElement("input"); showMoreInp.type = "hidden"; showMoreInp.value = '0'; showMoreInp.id = showMoreLi.id + "showMore"; showMoreLi.appendChild(showMoreInp); let showMoreImg = document.createElement("img"); showMoreImg.height = "18"; showMoreImg.width = "18"; showMoreImg.id = showMoreLi.id + "_showMoreButton"; showMoreImg.src = "../images/add_sm.gif"; showMoreLi.appendChild(showMoreImg); let showMoreDiv = document.createElement("div"); @@ -603,33 +606,33 @@ let newText = ""; if (matches.length > 500) { newText = " Show 490 (out of " + (matches.length) + " total) more matches for " + searchDesc; } else { newText = " Show " + (matches.length - 10) + " more matches for " + searchDesc; } showMoreA.textContent = newText; showMoreDiv.appendChild(showMoreA); showMoreLi.appendChild(showMoreDiv); subList.appendChild(showMoreLi); } newListObj.append(subList); newList.append(newListObj); // make result list collapsible: - $('#'+idAttr.value+categoryCount+"_button").click(collapseNode); - $('#'+idAttr.value+"_" +categoryCount+"_showMoreButton").click(showMoreResults); - $('#'+idAttr.value + "_" + categoryCount + "_showMoreLink").click(showMoreResults); + $('#'+idKey+categoryCount+"_button").click(collapseNode); + $('#'+idKey+"_" +categoryCount+"_showMoreButton").click(showMoreResults); + $('#'+idKey + "_" + categoryCount + "_showMoreLink").click(showMoreResults); categoryCount += 1; }); } else if (uiState && typeof uiState.search !== "undefined") { // No results from match var msg = "<p>No results</p>"; parentDiv.empty(); parentDiv.html(msg); clearOldFacetCounts(); } else { parentDiv.empty(); } } function handleGenomeChange(jsonData) { // Handle the response from getUiState after the user changes the genome. @@ -731,34 +734,40 @@ return true; } return false; } function updateStateAndPage(jsonData, doSaveHistory) { // Update uiState with new values and update the page. _.assign(uiState, jsonData); db = uiState.db; if (typeof jsonData === "undefined" || jsonData === null) { // now that the show more text is a link, a popstate event gets fired because the url changes // we can safely return because there is no state to change return; } if (typeof jsonData.positionMatches !== "undefined") { - // clear the old resultHash + // clear the old resultHash, merging matches from multiple hgFindSpecs + // for the same track into one entry uiState.resultHash = {}; _.each(uiState.positionMatches, function(match) { + if (match.name in uiState.resultHash) { + uiState.resultHash[match.name].matches = + uiState.resultHash[match.name].matches.concat(match.matches); + } else { uiState.resultHash[match.name] = match; + } }); } else { // no results for this search uiState.resultHash = {}; uiState.positionMatches = []; } updateFilters(uiState); updateSearchResults(uiState); updateCurrentGenomeLabel(); urlVars = {"db": db, "search": uiState.search, "showSearchResults": ""}; // changing the url allows the history to be associated to a specific url var urlParts = changeUrl(urlVars); $("#searchCategories").jstree(true).refresh(false,true); saveLinkClicks(); if (doSaveHistory) @@ -955,32 +964,38 @@ } window.location.replace(newUrl); } var urlParts = {}; if (debugCartJson) { console.log('from server:\n', cartJson); } if (typeof cartJson.search !== "undefined") { urlParts = changeUrl({"search": cartJson.search}); } else { urlParts = changeUrl({"db": db}); cartJson.search = urlParts.urlVars.search; } _.assign(uiState,cartJson); if (typeof cartJson.categs !== "undefined") { + uiState.resultHash = {}; _.each(uiState.positionMatches, function(match) { + if (match.name in uiState.resultHash) { + uiState.resultHash[match.name].matches = + uiState.resultHash[match.name].matches.concat(match.matches); + } else { uiState.resultHash[match.name] = match; + } }); filtersToJstree(); makeCategoryTree(); } else { cart.send({ getUiState: {db: db} }, handleRefreshState); cart.flush(); } $("#searchCategories").bind('ready.jstree', function(e, data) { // wait for the category jstree to finish loading before showing the results $("#searchBarSearchString").val(uiState.search); updateSearchResults(uiState); // when a category is checked/unchecked we show/hide that result // from the result list $("#searchCategories").on('check_node.jstree uncheck_node.jstree', function(e, data) {