afcbfbdf1e2c8cb1a76a08ca90f1e16833296e28 chmalee Tue Feb 10 15:25:32 2026 -0800 Fix hgGateway bugs found in #34078 note 39: fix show species tree link, when disconnecting a hub on hgHubConnect, remove the hub from localStorage, fix handling of native databases that don't follow canFam4, hg38, etc format (example GRCm38B), store public hub name in the item storage so we correctly detect clicks on public hubs in recents list, call draw species tree by default on page load even if it's hidden so we don't need to render it each time we call back to the server because connecting to hubs no longer go into the tree. refs #34078 diff --git src/hg/js/hgGateway.js src/hg/js/hgGateway.js index 8b6f6c6225b..da660d9d3ca 100644 --- src/hg/js/hgGateway.js +++ src/hg/js/hgGateway.js @@ -701,30 +701,33 @@ 'Chrome.' + '

'; // Globals (within this function scope) // Set this to true to see server requests and responses in the console. var debugCartJson = false; // This is a global (within wrapper function scope) so event handlers can use it // without needing to bind functions. var scrollbarWidth = 0; // This holds everything we need to know to draw the page: taxId, db, hubs, description etc. var uiState = {}; // This is used to check whether a taxId is found in activeGenomes: var activeTaxIds = []; // gets set in init() now; // This is dbDbTree after pruning -- null if dbDbTree has no children left var prunedDbDbTree = null; + // These store the latest species tree draw results for use by the species tree toggle handler + var latestSpTree = null; + var latestStripeTops = null; // This keeps track of which gene the user has selected most recently from autocomplete. var selectedGene = null; function setupFavIcons() { // Set up onclick handlers for shortcut buttons and labels var haveIcon = false; var i, name, taxId, onClick; for (i = 0; i < favIconTaxId.length; i++) { name = favIconTaxId[i][0]; taxId = favIconTaxId[i][1]; if (activeTaxIds[taxId]) { // When user clicks on icon, set the taxId (default database); // scroll the image to that species and clear the species autocomplete input. onClick = setTaxId.bind(null, taxId, null, null, true, true); // Onclick for both the icon and its sibling label: @@ -1078,51 +1081,35 @@ // If dbDbTree is nonempty and SVG is supported, draw the tree; if SVG is not supported, // use the space to suggest that the user install a better browser. // If dbDbTree doesn't exist, leave the "Represented Species" section hidden. var svg, spTree, stripeTops; if (dbDbTree) { if (document.createElementNS) { // Draw the phylogenetic tree and do layout adjustments svg = document.getElementById('speciesTree'); spTree = speciesTree.draw(svg, dbDbTree, null, { hgHubConnectUrl: 'hgHubConnect?hgsid=' + window.hgsid, containerWidth: $('#speciesPicker').width() }); setSpeciesPickerSizes(spTree.width, spTree.height); stripeTops = rainbow.draw(svg, dbDbTree, spTree.yTree, spTree.height, spTree.leafTops); + latestSpTree = spTree; + latestStripeTops = stripeTops; } else { $('#speciesTreeContainer').html(getBetterBrowserMessage); } - let showSpeciesTree = document.getElementById("speciesTreeLink"); - showSpeciesTree.addEventListener("click", function() { - let speciesTreeSection = document.getElementById("speciesGraphic"); - if (!$(speciesTreeSection).is(":visible")) { - $('#representedSpeciesTitle').show(); - $('#speciesGraphic').show(); - if (dbDbTree && document.createElementNS) { - // These need to be done after things are visible because heights are 0 when hidden. - highlightLabelForDb(uiState.db, uiState.taxId); - initRainbowSlider(spTree.height, rainbow.colors, stripeTops); - } - showSpeciesTree.textContent = "Hide species tree"; - } else { - $('#representedSpeciesTitle').hide(); - $('#speciesGraphic').hide(); - showSpeciesTree.textContent = "Show species tree"; - } - }); } } function addHubsToList(hubList) { // Render connected hubs into the connected hubs section // Only show the section if there are non-curated hubs to display var $title = $('#connectedHubsTitle'); var $section = $('#connectedHubsSection'); var $panel = $('#connectedHubsList'); if (!hubList || hubList.length === 0) { // No hubs, hide the section $title.hide(); $section.hide(); return; @@ -1427,43 +1414,56 @@ alert(callerName + ': empty response from server'); } else if (jsonData.error) { console.error(jsonData.error); alert(callerName + ': error from server: ' + jsonData.error); } else { if (debugCartJson) { console.log('from server:\n', jsonData); } return true; } return false; } function updateStateAndPage(jsonData) { // Update uiState with new values and update the page. - var hubsChanged = !_.isEqual(jsonData.hubs, uiState.hubs); + var hubsChanged = !_.isEqual(jsonData.hubs || [], uiState.hubs || []); // In rare cases, there can be a genome (e.g. Baboon) with multiple species/taxIds // (e.g. Papio anubis for papAnu1 vs. Papio hamadryas for papHam1). Changing the // db can result in changing the taxId too. In that case, update the highlighted // species in the tree image. if (jsonData.taxId !== uiState.taxId) { highlightLabel('textEl_' + jsonData.taxId, false); } + // Before overwriting uiState, clean up localStorage recents for any removed hubs + if (hubsChanged) { + var newHubUrls = {}; + (jsonData.hubs || []).forEach(function(hub) { + newHubUrls[hub.hubUrl] = true; + }); + (uiState.hubs || []).forEach(function(hub) { + if (!newHubUrls[hub.hubUrl]) { + removeRecentGenomesByHubUrl(hub.hubUrl); + } + }); + } _.assign(uiState, jsonData); updateFindPositionSection(uiState); - if (hubsChanged) { drawSpeciesPicker(prunedDbDbTree); + if (hubsChanged) { addHubsToList(uiState.hubs); + displayRecentGenomesInPanel(); } } function handleRefreshState(jsonData) { if (checkJsonData(jsonData, 'handleRefreshState')) { updateStateAndPage(jsonData); } } function isGenomeAtFrontOfRecents(db) { // Check if a genome with this db is already at the front of the recents list. // This is used to detect if autocompleteCat.js just added it (to avoid double-adding). // We compare without hub prefixes since autocompleteCat saves without prefix but // server responses may include prefixes like "hub_123_GCA_xxx". var stored = window.localStorage.getItem("recentGenomes"); @@ -1486,30 +1486,35 @@ if (!isGenomeAtFrontOfRecents(dbForRecents)) { // Construct a descriptive label that includes the db/accession for identification var label = uiState.genomeLabel || uiState.genome; if (label && dbForRecents && label.indexOf(dbForRecents) < 0) { // Add the db/accession to the label if not already present label = label + ' (' + dbForRecents + ')'; } var recentItem = { db: dbForRecents, genome: uiState.genome, label: label, taxId: uiState.taxId }; if (uiState.hubUrl) { recentItem.hubUrl = uiState.hubUrl; + // Store hubName so we can reconstruct the full hub db name later + var parsedHubName = hubNameFromDb(uiState.db); + if (parsedHubName) { + recentItem.hubName = parsedHubName; + } if (uiState.hubUrl.startsWith("/gbdb")) { recentItem.category = "UCSC Curated"; } else { recentItem.category = "Assembly Hub"; } } else { recentItem.category = "UCSC Curated"; } addRecentGenome(recentItem); } displayRecentGenomesInPanel(); } else { console.log('handleSetDb ignoring: ' + trackHubSkipHubName(jsonData.db) + ' !== ' + trackHubSkipHubName(uiState.db)); } @@ -1594,65 +1599,73 @@ (isAutocomplete && db !== uiState.db)) { uiState.hubUrl = hubUrl; uiState.taxId = taxId; uiState.db = trackHubSkipHubName(db); // Use cart variables to connect to the selected hub and switch to db // (hubConnectLoadHubs, called by cartNew) cmd = { cgiVar: { hubUrl: hubUrl, genome: trackHubSkipHubName(db) }, setHubDb: { hubUrl: hubUrl, taxId: '' + taxId } }; cart.send(cmd, handleSetDb); cart.flush(); clearPositionInput(); } + if (hubName) { highlightLabel('textEl_' + hubName, isAutocomplete); + } if (! isAutocomplete) { clearSpeciesInput(); } } function setDbFromAutocomplete(item) { // The user has selected a result from the species-search autocomplete. // It might be a taxId and/or db from dbDb, or it might be a hub db, // or a taxon-only result (like "Human") that shows an assembly dropdown. var taxId = item.taxId || -1; var db = item.db; var org = item.org || item.value || item.label; var cmd; var genome = item.genome || ''; - // Check if db is a valid assembly name (not an organism name) - // Valid db names are typically lowercase alphanumeric like "hg38", "mm10" - // Organism names are capitalized like "Human", "Mouse" - var isValidDb = db && /^[a-z]/.test(db) && db !== org; + // Check if db is a valid assembly name (not an organism name). + // If db is the same as org, it's likely a taxon-only result, not an assembly. + var isValidDb = db && db !== org; - // Detect GenArk by category OR by genome name pattern (GCA_/GCF_) - var isGenArk = (typeof item.category !== "undefined" && item.category.startsWith("UCSC GenArk")) || - (item.hubUrl && (genome.startsWith('GCA_') || genome.startsWith('GCF_'))); + // Detect GenArk: must have hubUrl, and either "UCSC Curated" category (GenArk hubs + // under /gbdb) or a GCA_/GCF_ genome pattern. The hubUrl check distinguishes GenArk + // from native databases which also use "UCSC Curated" category. + var isGenArk = item.hubUrl && + ((typeof item.category !== "undefined" && item.category.startsWith("UCSC Curated")) || + (genome.startsWith('GCA_') || genome.startsWith('GCF_'))); if (isGenArk) { // For items from localStorage recents, db is the accession; for fresh autocomplete, genome is db = item.db || item.genome; setHubDb(item.hubUrl, taxId, db, "GenArk", item.scientificName || org, true); } else if (item.hubUrl && item.hubName) { // Public hub - the autocomplete sends the hub database from hubPublic.dbList, // without the hub prefix -- restore the prefix here. db = item.hubName + '_' + item.db; setHubDb(item.hubUrl, taxId, db, item.hubName, org, true); + } else if (item.hubUrl) { + // Connected hub from localStorage recents without hubName. + // db is already the bare assembly name; server resolves via hubUrl. + setHubDb(item.hubUrl, taxId, db, null, org, true); } else if (taxId > 0) { // Native db with valid taxId - pass db only if it's a valid assembly name setTaxId(taxId, isValidDb ? db : null, org, true, false); } else if (isValidDb) { // Native db without taxId - use setDb command directly cmd = { setDb: { db: db, position: "lastDbPos" } }; cart.send(cmd, handleSetDb); cart.flush(); uiState.db = db; clearPositionInput(); clearSpeciesInput(); } // Refresh the recent genomes panel (autocompleteCat.js handles saving to localStorage) displayRecentGenomesInPanel(); @@ -1924,30 +1937,52 @@ onSelect: setDbFromAutocomplete, onServerReply: processSpeciesResults, showRecentGenomes: true, enterSelectsIdentical: false }); $('#selectAssembly').on("change", onChangeDbMenu); $('#positionDisplay').on("click", onClickCopyPosition); $('#copyPosition').on("click", onClickCopyPosition); $('.jwGoButtonContainer').on("click", goToHgTracks); $(window).on("resize", setRightColumnWidth.bind(null, scrollbarWidth)); displaySurvey(); replaceHgsidInLinks(); // Display recent genomes in the left panel on page load displayRecentGenomesInPanel(); + // Set up species tree toggle link (attach once to avoid duplicate listeners) + var speciesTreeLink = document.getElementById("speciesTreeLink"); + if (speciesTreeLink) { + speciesTreeLink.addEventListener("click", function() { + var speciesTreeSection = document.getElementById("speciesGraphic"); + if (!$(speciesTreeSection).is(":visible")) { + $('#representedSpeciesTitle').show(); + $('#speciesGraphic').show(); + if (prunedDbDbTree && document.createElementNS && latestSpTree) { + // These need to be done after things are visible because heights are 0 when hidden. + highlightLabelForDb(uiState.db, uiState.taxId); + initRainbowSlider(latestSpTree.height, rainbow.colors, latestStripeTops); + } + speciesTreeLink.textContent = "Hide species tree"; + } else { + $('#representedSpeciesTitle').hide(); + $('#speciesGraphic').hide(); + speciesTreeLink.textContent = "Show species tree"; + } + }); + } + // Set up info icon tooltips var speciesSearchInfo = document.getElementById('speciesSearchInfo'); if (speciesSearchInfo) { addMouseover(speciesSearchInfo, "Searches are case-insensitive and match by prefix. You can search by:" + "" + "For multi-word searches, all words are required by default."); } var recentGenomesInfo = document.getElementById('recentGenomesInfo');