diff --git a/Gemfile b/Gemfile index ff92e06fa3..a99a2d45a8 100644 --- a/Gemfile +++ b/Gemfile @@ -138,3 +138,5 @@ gem "net-ftp", "~> 0.2.0", require: false gem "net-http", "~> 0.3.2" + +gem "bugsnag", "~> 6.26" diff --git a/Gemfile.lock b/Gemfile.lock index fe1de670e6..4009a83e91 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,6 +109,8 @@ GEM popper_js (>= 1.14.3, < 2) sassc-rails (>= 2.0.0) brakeman (5.4.1) + bugsnag (6.26.0) + concurrent-ruby (~> 1.0) builder (3.2.4) capistrano (3.18.0) airbrussh (>= 1.0.0) @@ -316,12 +318,13 @@ GEM racc (~> 1.4) nokogiri (1.15.5-x86_64-linux) racc (~> 1.4) - oauth2 (1.4.11) + oauth2 (2.0.9) faraday (>= 0.17.3, < 3.0) jwt (>= 1.0, < 3.0) - multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 4) + snaky_hash (~> 2.0) + version_gem (~> 1.1) oj (3.16.3) bigdecimal (>= 3.0) omniauth (2.1.2) @@ -331,11 +334,10 @@ GEM omniauth-github (2.0.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.7.1) - omniauth-google-oauth2 (1.0.1) + omniauth-google-oauth2 (0.8.0) jwt (>= 2.0) - oauth2 (~> 1.1) - omniauth (~> 2.0) - omniauth-oauth2 (~> 1.7.1) + omniauth (>= 1.1.1) + omniauth-oauth2 (>= 1.6) omniauth-keycloak (1.5.1) faraday json-jwt (> 1.13.0) @@ -489,6 +491,9 @@ GEM simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) spawnling (2.1.5) sprockets (4.2.1) concurrent-ruby (~> 1.0) @@ -531,6 +536,7 @@ GEM concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) uri (0.13.0) + version_gem (1.1.3) view_component (2.82.0) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) @@ -559,6 +565,7 @@ DEPENDENCIES bootsnap bootstrap (~> 4.2.0) brakeman + bugsnag (~> 6.26) capistrano (~> 3.11) capistrano-bundler capistrano-locally diff --git a/app/assets/images/icons/arrow-down.svg b/app/assets/images/icons/arrow-down.svg new file mode 100644 index 0000000000..1130bff606 --- /dev/null +++ b/app/assets/images/icons/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/icons/details.svg b/app/assets/images/icons/details.svg new file mode 100644 index 0000000000..ea7ce05279 --- /dev/null +++ b/app/assets/images/icons/details.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/icons/hide.svg b/app/assets/images/icons/hide.svg new file mode 100644 index 0000000000..8cbc7b1bae --- /dev/null +++ b/app/assets/images/icons/hide.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/icons/reuses.svg b/app/assets/images/icons/reuses.svg new file mode 100644 index 0000000000..45de94ca9a --- /dev/null +++ b/app/assets/images/icons/reuses.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/icons/search.svg b/app/assets/images/icons/search.svg new file mode 100644 index 0000000000..e946dbae72 --- /dev/null +++ b/app/assets/images/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/icons/settings.svg b/app/assets/images/icons/settings.svg new file mode 100644 index 0000000000..d8956143ea --- /dev/null +++ b/app/assets/images/icons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/icons/visualize.svg b/app/assets/images/icons/visualize.svg new file mode 100644 index 0000000000..f13b442060 --- /dev/null +++ b/app/assets/images/icons/visualize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 66099b25bb..df70491a3a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -15,7 +15,6 @@ //= require bp_ajax_controller //= require bp_notes //= require bp_form_complete -//= require bp_search //= require bp_mappings //= require bp_admin //= require bp_recommender diff --git a/app/assets/javascripts/bp_admin.js b/app/assets/javascripts/bp_admin.js index 5450c92e60..e6468f9f84 100644 --- a/app/assets/javascripts/bp_admin.js +++ b/app/assets/javascripts/bp_admin.js @@ -197,6 +197,10 @@ AjaxAction.prototype.ajaxCall = function() { } }; +function determineHTTPS(url) { + return url.replace("http:", ('https:' == document.location.protocol ? 'https:' : 'http:')); +} + AjaxAction.prototype.onSuccessAction = function(data, ontology, deferredObj) { var self = this; if (!self.isLongOperation) { diff --git a/app/assets/javascripts/bp_search.js.erb b/app/assets/javascripts/bp_search.js.erb deleted file mode 100644 index 9cecf7af27..0000000000 --- a/app/assets/javascripts/bp_search.js.erb +++ /dev/null @@ -1,1046 +0,0 @@ -"use strict"; - -(() => { - // Abort if not the right page - const path = currentPathArray(); - if (path[0] !== "search") { - return; - } - - // Function to get current path array - function currentPathArray() { - // Implement the logic to get the current path array - // Replace the following line with your actual logic - return window.location.pathname.split('/'); - } - -})(); - -var showAdditionalResults = function(obj, resultSelector) { - var ontAcronym = jQuery(obj).attr("data-bp_ont"); - jQuery(resultSelector + ontAcronym).toggleClass("not_visible"); - jQuery(obj).children(".hide_link").toggleClass("not_visible"); - jQuery(obj).toggleClass("not_underlined"); -}; - -var showAdditionalOntResults = function(event) { - event.preventDefault(); - showAdditionalResults(this, "#additional_ont_results_"); -}; - -var showAdditionalClsResults = function(event) { - event.preventDefault(); - showAdditionalResults(this, "#additional_cls_results_"); -}; - - -// Declare the blacklisted class ID entities at the top level, to avoid -// repetitive execution within blacklistClsIDComponents. The order of the -// declarations here matches the order of removal. The fixed strings are -// removed once, the regex strings are removed globally from the class ID. -var blacklistFixStrArr = [], - blacklistSearchWordsArr = [], // see performSearch and aggregateResultsWithSubordinateOntologies - blacklistSearchWordsArrRegex = [], - blacklistRegexArr = [], - blacklistRegexMod = "ig"; -blacklistFixStrArr.push("https://"); -blacklistFixStrArr.push("http://"); -blacklistFixStrArr.push("bioportal.bioontology.org/ontologies/"); -blacklistFixStrArr.push("purl.bioontology.org/ontology/"); -blacklistFixStrArr.push("purl.obolibrary.org/obo/"); -blacklistFixStrArr.push("swrl.stanford.edu/ontologies/"); -blacklistFixStrArr.push("mesh.owl"); // Avoids RH-MESH subordinate to MESH -blacklistRegexArr.push(new RegExp("abnormalities", blacklistRegexMod)); -blacklistRegexArr.push(new RegExp("biological", blacklistRegexMod)); -blacklistRegexArr.push(new RegExp("biology", blacklistRegexMod)); -blacklistRegexArr.push(new RegExp("bioontology", blacklistRegexMod)); -blacklistRegexArr.push(new RegExp("clinical", blacklistRegexMod)); -blacklistRegexArr.push(new RegExp("extension", blacklistRegexMod)); -blacklistRegexArr.push(new RegExp("\.gov", blacklistRegexMod)); -blacklistRegexArr.push(new RegExp("ontology", blacklistRegexMod)); -blacklistRegexArr.push(new RegExp("ontologies", blacklistRegexMod)); -blacklistRegexArr.push(new RegExp("semanticweb", blacklistRegexMod)); - -function blacklistClsIDComponents(clsID) { - var strippedID = clsID; - // remove fixed strings first - for (var i = 0; i < blacklistFixStrArr.length; i++) { - strippedID = strippedID.replace(blacklistFixStrArr[i], ""); - }; - // cleanup with regex replacements - for (var i = 0; i < blacklistRegexArr.length; i++) { - strippedID = strippedID.replace(blacklistRegexArr[i], ""); - }; - // remove search keywords (see performSearch and aggregateResultsWithSubordinateOntologies) - for (var i = 0; i < blacklistSearchWordsArrRegex.length; i++) { - strippedID = strippedID.replace(blacklistSearchWordsArrRegex[i], ""); - }; - return strippedID; -} - -function OntologyOwnsClass(clsID, ontAcronym) { - // Does the clsID contain the ontAcronym? - // Use case insensitive match - clsID = blacklistClsIDComponents(clsID); - return clsID.toUpperCase().lastIndexOf(ontAcronym) > -1; -} - -function findOntologyOwnerOfClass(clsID, ontAcronyms) { - // Find the index of cls_id in cls_list results with the cls_id in the 'owner' - // ontology (cf. ontologies that import the class, or views). - var ontAcronym = "", - ontWeight = 0, - ontIsOwner = false, - ontOwner = { - "acronym": "", - "index": null, - "weight": 0 - }; - for (var i = 0, j = ontAcronyms.length; i < j; i++) { - ontAcronym = ontAcronyms[i]; - // Does the class ID contain the ontology acronym? If so, the result is a - // potential ontology owner. Update the ontology owner, if the ontology - // acronym matches and it has a greater 'weight' than any previous ontology owner. - // Note that OntologyOwnsClass() modifies the clsID to blacklist various strings that - // cause false or misleading matches for ontology acronyms in class ID. - if (OntologyOwnsClass(clsID, ontAcronym)) { - // This weighting that places greater value on matching an ontology acronym later in the class ID. - ontWeight = ontAcronym.length * (clsID.toUpperCase().lastIndexOf(ontAcronym) + 1); - if (ontWeight > ontOwner.weight) { - ontOwner.acronym = ontAcronym; - ontOwner.index = i; - ontOwner.weight = ontWeight; - // Cannot break here, in case another acronym has greater weight. - } - } - - } - return ontOwner; -} - - - - -jQuery(document).ready(function() { - // Wire advanced search categories - jQuery("#search_categories").chosen({ - search_contains: true, - width: "432px" - }); - /* Comment it because it is changing the button value to nothing (and it is a param for chosen, don't seems useful for button) - jQuery("#search_button").button({ - search_contains: true - });*/ - jQuery("#search_button").click(function(event) { - ajax_process_halt(); - }); - jQuery("#search_keywords").click(function(event) { - ajax_process_halt(); - }); - - jQuery("#search_spinner").hide(); - - // Put cursor in search box by default - jQuery("#search_keywords").focus(); - - jQuery("#search_select_ontologies").change(function() { - if (jQuery(this).is(":checked")) { - jQuery("#ontology_picker_options").removeClass("not_visible"); - } else { - jQuery("#ontology_picker_options").addClass("not_visible"); - jQuery("#ontology_ontologyId").val(""); - jQuery("#ontology_ontologyId").trigger("chosen:updated"); - } - }); - - jQuery("#search_results a.additional_ont_results_link").live("click", showAdditionalOntResults); - jQuery("#search_results a.additional_cls_results_link").live("click", showAdditionalClsResults); - - jQuery("#search_options").hide(); - jQuery("#advanced_options").on('click', toggleAdvancedSearchOptions); - - // Events to run whenever search results are updated (mainly counts) - jQuery(document).live("search_results_updated", function() { - // Update count - jQuery("#ontologies_count_total").html(currentOntologiesCount()); - - // Tooltip for ontology counts - updatePopupCounts(); - }); - - // Perform search - jQuery("#search_button").click(function(event) { - event.preventDefault(); - history.pushState(currentSearchParams(), document.title, "/search?" + objToQueryString(currentSearchParams())); - var state = history.state || {}; - autoSearch() - }); - - // Search on enter - jQuery("#search_keywords").bind("keyup", function(event) { - if (event.which == 13) { - jQuery("#search_button").click(); - } - }); - - - // Position of popup for details - jQuery(document).bind("reveal.facebox", function() { - if (jQuery("div.class_details_pop").is(":visible")) { - jQuery("#facebox").css("max-height", jQuery(window).height() - (jQuery("#facebox").offset().top - jQuery(window).scrollTop()) * 2 + "px"); - } - }); - - // Use pop-up with flex via an iframe for "visualize" link - jQuery("a.class_visualize").live("click", function() { - var acronym = jQuery(this).attr("data-bp_ontologyid"), - conceptid = jQuery(this).attr("data-bp_conceptid"); - jQuery("#biomixer").html('').show(); - jQuery.facebox({ - div: '#biomixer' - }); - }); - - jQuery("#search-help").on("click", bpPopWindow); - - autoSearch(); -}); - -// Automatically perform search based on input parameters -function autoSearch() { - // Check for existing parameters/queries and update UI accordingly - var params = BP_queryString(), - query = null, - ontologyIds = null, - categories = null; - - if (params.hasOwnProperty("query") || params.hasOwnProperty("q")) { - query = params.query || params.q; - jQuery("#search_keywords").val(query); - - if (params.exactmatch === "true" || params.exact_match === "true") { - if (!jQuery("#search_exact_match").is(":checked")) { - jQuery("#search_exact_match").attr("checked", true); - } - } else { - jQuery("#search_exact_match").attr("checked", false); - } - - if (params.searchproperties === "true" || params.include_properties === "true") { - if (!jQuery("#search_include_properties").is(":checked")) { - jQuery("#search_include_properties").attr("checked", true); - } - } else { - jQuery("#search_include_properties").attr("checked", false); - } - - if (params.require_definition === "true") { - if (!jQuery("#search_require_definition").is(":checked")) { - jQuery("#search_require_definition").attr("checked", true); - } - } else { - jQuery("#search_require_definition").attr("checked", false); - } - - if (params.include_views === "true") { - if (!jQuery("#search_include_views").is(":checked")) { - jQuery("#search_include_views").attr("checked", true); - } - } else { - jQuery("#search_include_views").attr("checked", false); - } - - if (params.hasOwnProperty("ontologyids") || params.hasOwnProperty("ontologies")) { - ontologyIds = params.ontologies || params.ontologyids || ""; - ontologyIds = ontologyIds.split(","); - jQuery("#ontology_ontologyId").val(ontologyIds); - jQuery("#ontology_ontologyId").trigger("chosen:updated"); - } - - if (params.hasOwnProperty("categories")) { - categories = params.categories || ""; - categories = categories.split(","); - jQuery("#search_categories").val(categories); - jQuery("#search_categories").trigger("chosen:updated"); - } - - performSearch(); - } -} - - -function currentSearchParams() { - var params = {}, ont_val = null; - // Search query - params.q = jQuery("#search_keywords").val(); - // Ontologies - ont_val = jQuery("#ontology_ontologyId").val(); - params.ontologies = (ont_val === null) ? "" : ont_val.join(","); - // Advanced options - params.include_properties = jQuery("#search_include_properties").is(":checked"); - params.include_views = jQuery("#search_include_views").is(":checked"); - params.includeObsolete = jQuery("#search_include_obsolete").is(":checked"); - // params.includeNonProduction = - // jQuery("#search_include_non_production").is(":checked"); - params.require_definition = jQuery("#search_require_definition").is(":checked"); - params.exact_match = jQuery("#search_exact_match").is(":checked"); - params.categories = jQuery("#search_categories").val() || ""; - params.lang = jQuery("#select_search_language").val() || ""; - - return params; -} - - - -function objToQueryString(obj) { - var str = [], - p = null; - for (p in obj) { - if (obj.hasOwnProperty(p)) { - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); - } - } - return str.join("&"); -} - -function performSearch() { - jQuery("#search_spinner").show(); - jQuery("#search_messages").html(""); - jQuery("#search_results").html(""); - jQuery("#result_stats").html(""); - - var ont_val = jQuery("#ontology_ontologyId").val() || null, - onts = (ont_val === null) ? "" : ont_val.join(","), - query = jQuery("#search_keywords").val(), - // Advanced options - includeProps = jQuery("#search_include_properties").is(":checked"), - includeViews = jQuery("#search_include_views").is(":checked"), - includeObsolete = jQuery("#search_include_obsolete").is(":checked"), - includeNonProduction = jQuery("#search_include_non_production").is(":checked"), - includeOnlyDefinitions = jQuery("#search_require_definition").is(":checked"), - exactMatch = jQuery("#search_exact_match").is(":checked"), - categories = jQuery("#search_categories").val() || "", - language = jQuery("#select_search_language").val() || ""; - - // Set the list of search words to be blacklisted for the ontology ownership algorithm - blacklistSearchWordsArr = query.split(/\s+/); - - jQuery.ajax({ - // bp.config is created in views/layouts/_header..., which calls - // ApplicationController::bp_config_json - url: determineHTTPS(jQuery(document).data().bp.config.rest_url) + "/search", - data: { - q: query, - lang: language, - include_properties: includeProps, - include_views: includeViews, - obsolete: includeObsolete, - include_non_production: includeNonProduction, - require_definition: includeOnlyDefinitions, - exact_match: exactMatch, - categories: categories, - ontologies: onts, - pagesize: 150, - apikey: jQuery(document).data().bp.config.apikey, - userapikey: jQuery(document).data().bp.config.userapikey, - format: "jsonp", - ncbo_slice: (("ncbo_slice" in jQuery(document).data().bp.config) ? jQuery(document).data().bp.config.ncbo_slice : '') - }, - dataType: "jsonp", - success: function(data) { - var results = [], - ontologies = {}, - groupedResults = null, - result_count = jQuery("#result_stats"), - resultsByOnt = "", - resultsOntCount = "", - resultsOntDiv = ""; - if (categories.length > 0) { - data.collection = filterCategories(data.collection, categories); - } - if (!jQuery.isEmptyObject(data)) { - groupedResults = aggregateResults(data.collection); - jQuery(groupedResults).each(function() { - results.push(formatSearchResults(this)); - }); - } - // Display error message if no results found - if (data.collection.length === 0) { - result_count.html(""); - jQuery("#search_results").html("

No matches found

"); - } else { - if (jQuery("#ontology_ontologyId").val() === null) { - resultsOntCount = jQuery(""); - resultsOntCount.attr("id", "ontologies_count_total"); - resultsOntCount.text(groupedResults.length); - resultsByOnt = jQuery(""); - resultsByOnt.attr({ - "id": "ont_tooltip", - "href": "javascript:void(0)" - }); - resultsByOnt.append("Matches in "); - resultsByOnt.append(resultsOntCount); - resultsByOnt.append(" ontologies"); - resultsOntDiv = jQuery("
"); - resultsOntDiv.attr("id", "ontology_counts"); - resultsOntDiv.addClass("ontology_counts_tooltip"); - resultsByOnt.append(resultsOntDiv); - } - result_count.html(resultsByOnt); - jQuery("#search_results").html(results.join("")); - } - jQuery("a[rel*=facebox]").facebox(); - jQuery("#search_results").show(); - jQuery("#search_spinner").hide(); - }, - error: function() { - jQuery("#search_spinner").hide(); - jQuery("#search_results").hide(); - jQuery("#search_messages").html("Problem searching, please try again"); - } - }); -} - -function aggregateResults(results) { - // class URI aggregation, promotes a class that belongs to 'owning' ontology, - // e.g. /search?q=cancer returns several hits for - // 'http://purl.obolibrary.org/obo/DOID_162' - // those results should be aggregated below the DOID ontology. - // var classes = aggregateResultsByClassURI(results); - var ontologies = aggregateResultsByOntology(results); - // return aggregateResultsByOntologyWithClasses(results, classes); - // return aggregateResultsWithoutDuplicateClasses(ontologies, classes); - // return aggregateResultsWithSubordinateOntologies(ontologies, classes); - return aggregateResultsWithSubordinateOntologies(ontologies); -} - - -function aggregateResultsWithSubordinateOntologies(ontologies) { - var i, j, - resultsWithSubordinateOntologies = [], - tmpOnt = null, - tmpResult = null, - tmpClsID = null, - tmpOntOwner = null, - ontAcronym = null, - ontAcronyms = [], - clsOntOwnerTracker = {}; - // build array of ontology acronyms - for (i = 0, j = ontologies.length; i < j; i++) { - tmpOnt = ontologies[i]; - tmpResult = tmpOnt.same_ont[0]; // primary result for this ontology - ontAcronym = ontologyIdToAcronym(tmpResult.links.ontology); - ontAcronyms.push(ontAcronym); - } - // Remove any items in blacklistSearchWordsArr that match ontology acronyms. - blacklistSearchWordsArrRegex = []; - for (var i = 0; i < blacklistSearchWordsArr.length; i++) { - // Convert blacklistSearchWordsArr to regex constructs so they are removed - // with case insensitive matches in blacklistClsIDComponents - blacklistSearchWordsArrRegex.push(new RegExp(blacklistSearchWordsArr[i], blacklistRegexMod)); - - // Check for any substring matches against ontology acronyms, where the - // acronyms are assumed to be upper case strings. (Note, cannot use the - // ontAcronyms array .indexOf() method, because it doesn't search for - // substring matches). - var searchToken = blacklistSearchWordsArr[i]; - var match = false; - for (var j = ontAcronyms.length - 1; j >= 0; j--) { - if (ontAcronyms[j].indexOf(searchToken) > -1) { - match = true; - break; - } - }; - if (match) { - // Remove this blacklisted search token because it matches or partially matches an ontology acronym. - blacklistSearchWordsArr.splice(i,1); - // Don't increment i, the slice moves everything so i+1 is now at i. - } else { - i++; // check the next search token. - } - } - // build hash of primary class results with an ontology owner - for (i = 0, j = ontologies.length; i < j; i++) { - tmpOnt = ontologies[i]; - tmpOnt.sub_ont = []; // add array for any subordinate ontology results - tmpResult = tmpOnt.same_ont[0]; - tmpClsID = tmpResult["@id"]; - if (clsOntOwnerTracker.hasOwnProperty(tmpClsID)) { - continue; - } - // find the best match for the ontology owner (must iterate over all ontAcronyms) - tmpOntOwner = findOntologyOwnerOfClass(tmpClsID, ontAcronyms); - if (tmpOntOwner.index !== null) { - // This primary class result is owned by an ontology - clsOntOwnerTracker[tmpClsID] = tmpOntOwner; - } - } - // aggregate the subordinate results below the owner ontology results - for (i = 0, j = ontologies.length; i < j; i++) { - tmpOnt = ontologies[i]; - tmpResult = tmpOnt.same_ont[0]; - tmpClsID = tmpResult["@id"]; - if (clsOntOwnerTracker.hasOwnProperty(tmpClsID)) { - // get the ontology that owns this class (if any) - tmpOntOwner = clsOntOwnerTracker[tmpClsID]; - if (tmpOntOwner.index === i) { - // the current ontology is the owner of this primary result - resultsWithSubordinateOntologies.push(tmpOnt); - } else { - // There is an owner, so put this ont result set into the sub_ont array - var tmpOwnerOnt = ontologies[tmpOntOwner.index]; - tmpOwnerOnt.sub_ont.push(tmpOnt); - } - } else { - // There is no ontology that owns this primary class result, just - // display this at the top level (it's not a subordinate) - resultsWithSubordinateOntologies.push(tmpOnt); - } - } - return resultsWithSubordinateOntologies; -} - - -function aggregateResultsByOntology(results) { - // NOTE: Cannot rely on the order of hash keys (obj properties) to preserve - // the order of the results, see - // http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop - var ontologies = { - "list": [], // used to ensure we have ordered ontologies - "hash": {} - }, - res = null, - ont = null; - for (var r in results) { - res = results[r]; - ont = res.links.ontology; - if (typeof ontologies.hash[ont] === "undefined") { - ontologies.hash[ont] = initOntologyResults(); - // Manage an ordered set of ontologies (no duplicates) - ontologies.list.push(ont); - } - ontologies.hash[ont].same_ont.push(res); - } - return resultsByOntologyArray(ontologies); -} - - -function initOntologyResults() { - return { - // classes with same URI - "same_cls": [], - // other classes from the same ontology - "same_ont": [], - // subordinate ontologies - "sub_ont": [] - } -} - - -function resultsByOntologyArray(ontologies) { - var resultsByOntology = [], - ont = null; - // iterate the ordered ontologies, not the hash keys - for (var i = 0, j = ontologies.list.length; i < j; i++) { - ont = ontologies.list[i]; - resultsByOntology.push(ontologies.hash[ont]); - } - return resultsByOntology; -} - - -function aggregateResultsByClassURI(results) { - var cls_hash = {}, res = null, - cls_id = null; - for (var r in results) { - res = results[r]; - cls_id = res['@id']; - if (typeof cls_hash[cls_id] === "undefined") { - cls_hash[cls_id] = { - "clsResults": [], - "clsOntOwner": null - }; - } - cls_hash[cls_id].clsResults.push(res); - } - promoteClassesWithOntologyOwner(cls_hash); - // passed by ref, modified in-place. - return cls_hash; -} - - -function promoteClassesWithOntologyOwner(cls_hash) { - var cls_id = null, - clsData = null, - ont_owner_result = null; - // Detect and 'promote' the class with an 'owner' ontology. - for (cls_id in cls_hash) { - clsData = cls_hash[cls_id]; - // Find the class in the 'owner' ontology (cf. ontologies that import the - // class, or views). Only promote the class result if the ontology owner - // is not already in the first position. - clsData.clsOntOwner = findClassWithOntologyOwner(cls_id, clsData.clsResults); - if (clsData.clsOntOwner.index > 0) { - // pop the owner and shift it to the top of the list; note that splice and - // unshift modify in-place so there's no need to reassign into cls_hash. - ont_owner_result = clsData.clsResults.splice(clsData.clsOntOwner.index, 1)[0]; - clsData.clsResults.unshift(ont_owner_result); - clsData.clsOntOwner.index = 0; - } - } -} - - -function findClassWithOntologyOwner(cls_id, cls_list) { - // Find the index of cls_id in cls_list results with the cls_id in the 'owner' - // ontology (cf. ontologies that import the class, or views). - var clsResult = null, - ontAcronym = "", - ontOwner = { - "index": null, - "acronym": "" - }, ontIsOwner = false; - for (var i = 0, j = cls_list.length; i < j; i++) { - clsResult = cls_list[i]; - ontAcronym = ontologyIdToAcronym(clsResult.links.ontology); - // Does the cls_id contain the ont acronym? If so, the result is a - // potential ontology owner. Update the ontology owner, if the ontology - // acronym matches and it is longer than any previous ontology owner. - ontIsOwner = OntologyOwnsClass(ontAcronym, clsID); - if (ontIsOwner && (ontAcronym.length > ontOwner.acronym.length)) { - ontOwner.acronym = ontAcronym; - ontOwner.index = i; - // console.log("Detected owner: index = " + ontOwner.index + ", ont = " + ontOwner.acronym); - } - } - return ontOwner; -} - - -var sortStringFunction = function(a, b) { - // See http://www.sitepoint.com/sophisticated-sorting-in-javascript/ - var x = String(a).toLowerCase(), - y = String(b).toLowerCase(); - return x < y ? -1 : x > y ? 1 : 0; -}; - -function sortResultsByOntology(results) { - // See http://www.sitepoint.com/sophisticated-sorting-in-javascript/ - return results.sort(function(a, b) { - var ontA = String(a.links.ontology).toLowerCase(), - ontB = String(b.links.ontology).toLowerCase(); - return ontA < ontB ? -1 : ontA > ontB ? 1 : 0; - }); -} - - -function formatSearchResults(aggOntologyResults) { - var - ontResults = aggOntologyResults.same_ont, - clsResults = aggOntologyResults.same_cls, - // init primary result values - res = ontResults.shift(), - ontAcronym = ontologyIdToAcronym(res.links.ontology), - clsID = res["@id"], - clsCode = encodeURIComponent(clsID), - label_html = classLabelSpan(res), - // init search results jQuery objects - searchResultLinks = null, - searchResultDiv = null, - additionalResultsSpan = null, - additionalResultsHide = null, - additionalOntResultsAnchor = null, - additionalOntResults = "", - additionalOntResultsAttr = null, - additionalClsResults = "", - additionalClsResultsAttr = null, - additionalClsResultsAnchor = null; - - searchResultDiv = jQuery("
"); - searchResultDiv.addClass("search_result"); - searchResultDiv.attr("data-bp_ont_id", res.links.ontology); - searchResultDiv.append(classDiv(res, label_html, true)); - searchResultDiv.append(definitionDiv(res)); - - additionalResultsSpan = jQuery(""); - additionalResultsSpan.addClass("additional_results_link"); - additionalResultsSpan.addClass("search_result_link"); - - additionalResultsHide = jQuery(""); - additionalResultsHide.addClass("not_visible"); - additionalResultsHide.addClass("hide_link"); - additionalResultsHide.text("[hide]"); - - // Process additional ontology results - if (ontResults.length > 0) { - additionalOntResultsAttr = { - "href": "#additional_ont_results", - "data-bp_ont": ontAcronym, - "data-bp_cls": clsID - }; - additionalOntResultsAnchor = jQuery(""); - additionalOntResultsAnchor.addClass("additional_ont_results_link"); - additionalOntResultsAnchor.addClass("search_result_link"); - additionalOntResultsAnchor.attr(additionalOntResultsAttr); - additionalOntResultsAnchor.append(ontResults.length + " more from this ontology"); - additionalOntResultsAnchor.append(additionalResultsHide.clone()); - additionalResultsSpan.append(" - "); - additionalResultsSpan.append(additionalOntResultsAnchor); - additionalOntResults = formatAdditionalOntResults(ontResults, ontAcronym); - } - - // Process additional clsResults - if (clsResults.length > 0) { - additionalClsResultsAttr = { - "href": "#additional_cls_results", - "data-bp_ont": ontAcronym, - "data-bp_cls": clsID - }; - additionalClsResultsAnchor = jQuery(""); - additionalClsResultsAnchor.addClass("additional_cls_results_link"); - additionalClsResultsAnchor.addClass("search_result_link"); - additionalClsResultsAnchor.attr(additionalClsResultsAttr); - additionalClsResultsAnchor.append(clsResults.length + " more for this class"); - additionalClsResultsAnchor.append(additionalResultsHide.clone()); - additionalResultsSpan.append(" - "); - additionalResultsSpan.append(additionalClsResultsAnchor); - additionalClsResults = formatAdditionalClsResults(clsResults, ontAcronym); - } - - // Nest subordinate ontology results - var subOntResults = "", - subordinateOntTitle = ""; - if (aggOntologyResults.sub_ont.length > 0) { - subOntResults = jQuery("
"); - subOntResults.addClass("subordinate_ont_results"); - subordinateOntTitle = jQuery("

"); - subordinateOntTitle.addClass("subordinate_ont_results_title"); - subordinateOntTitle.addClass("search_result_link"); - subordinateOntTitle.attr("data-bp_ont", ontAcronym); - subordinateOntTitle.text("Reuses in other ontologies"); - subOntResults.append(subordinateOntTitle); - jQuery(aggOntologyResults.sub_ont).each(function() { - subOntResults.append(formatSearchResults(this)); - }); - } - - searchResultLinks = jQuery("
"); - searchResultLinks.addClass("search_result_links"); - searchResultLinks.append(resultLinksSpan(res)); - searchResultLinks.append(additionalResultsSpan); - - searchResultDiv.append(searchResultLinks); - searchResultDiv.append(additionalOntResults); - searchResultDiv.append(additionalClsResults); - searchResultDiv.append(subOntResults); - return searchResultDiv.prop("outerHTML"); -} - - - -function formatAdditionalClsResults(clsResults, ontAcronym) { - var additionalClsTitle = null, - clsResultsFormatted = null, - searchResultDiv = null, - classLabelDiv = null, - classDetailsDiv = null; - additionalClsTitle = jQuery("

"); - additionalClsTitle.addClass("additional_cls_results_title"); - additionalClsTitle.text("Same Class URI - Other Ontologies"); - clsResultsFormatted = jQuery("
"); - clsResultsFormatted.attr("id", "additional_cls_results_" + ontAcronym); - clsResultsFormatted.addClass("additional_cls_results"); - clsResultsFormatted.addClass("not_visible"); - clsResultsFormatted.append(additionalClsTitle); - jQuery(clsResults).each(function() { - searchResultDiv = jQuery("
"); - searchResultDiv.addClass("search_result_links"); - searchResultDiv.append(resultLinksSpan(this)); - // class prefLabel with ontology name - classLabelDiv = classDiv(this, classLabelSpan(this), true); - classDetailsDiv = jQuery("
"); - classDetailsDiv.addClass("search_result_additional"); - classDetailsDiv.append(classLabelDiv); - classDetailsDiv.append(definitionDiv(this, "additional_def_container")); - classDetailsDiv.append(searchResultDiv); - clsResultsFormatted.append(classDetailsDiv); - }); - return clsResultsFormatted; -} - -function formatAdditionalOntResults(ontResults, ontAcronym) { - var additionalOntTitle = null, - ontResultsFormatted = null, - searchResultDiv = null, - classLabelDiv = null, - classDetailsDiv = null; - additionalOntTitle = jQuery(""); - additionalOntTitle.addClass("additional_ont_results_title"); - additionalOntTitle.addClass("search_result_link"); - additionalOntTitle.attr("data-bp_ont", ontAcronym); - additionalOntTitle.text("Same Ontology - Other Classes"); - ontResultsFormatted = jQuery("
"); - ontResultsFormatted.attr("id", "additional_ont_results_" + ontAcronym); - ontResultsFormatted.addClass("not_visible"); - // ontResultsFormatted.addClass( "additional_ont_results" ); - // ontResultsFormatted.append( additionalOntTitle ); - jQuery(ontResults).each(function() { - searchResultDiv = jQuery("
"); - searchResultDiv.addClass("search_result_links"); - searchResultDiv.append(resultLinksSpan(this)); - // class prefLabel without ontology name - classLabelDiv = classDiv(this, classLabelSpan(this), false); - classDetailsDiv = jQuery("
"); - classDetailsDiv.addClass("search_result_additional"); - classDetailsDiv.append(classLabelDiv); - classDetailsDiv.append(definitionDiv(this, "additional_def_container")); - classDetailsDiv.append(searchResultDiv); - ontResultsFormatted.append(classDetailsDiv); - }); - return ontResultsFormatted; -} - -function updatePopupCounts() { - var ontologies = [], - result = null, - resultsCount = 0; - jQuery("#search_results div.search_result").each(function() { - result = jQuery(this); - // Add one to the additional results to get total count (1 is for the - // primary result) - resultsCount = result.children("div.additional_ont_results").find("div.search_result_additional").length + 1; - ontologies.push(result.attr("data-bp_ont_name") + " " + resultsCount + "
"); - }); - // Sort using case insensitive sorting - ontologies.sort(sortStringFunction); - jQuery("#ontology_counts").html(ontologies.join("")); -} - - -function classLabelSpan(cls) { - // Wrap the class prefLabel in a span, indicating that the class is obsolete - // if necessary. - let prefLabel = cls.prefLabel - - if(Array.isArray(prefLabel)){ - let query = jQuery("#search_keywords").val() - // Filter labels containing the query or return the first label - let filteredLabels = prefLabel.filter(label => label.includes(query)) - // If there are matching labels, use the first one; otherwise, use the first label - prefLabel = filteredLabels.length > 0 ? filteredLabels[0] : prefLabel[0] - } - - var MAX_LENGTH = 60, - labelText = prefLabel, - labelSpan = null; - - if (labelText > MAX_LENGTH) { - labelText = prefLabel.substring(0, MAX_LENGTH) + "..."; - } - labelSpan = jQuery("").text(labelText); - if (cls.obsolete === true) { - labelSpan.addClass('obsolete_class'); - labelSpan.attr('title', 'obsolete class'); - } else { - labelSpan.addClass('prefLabel'); - } - return labelSpan; - // returns a jQuery object; use .prop('outerHTML') to get markup. -} - -function filterCategories(results, filterCats) { - var newResults = [], - result = null, - acronym = null; - jQuery(results).each(function() { - result = this; - acronym = ontologyIdToAcronym(result.links.ontology); - jQuery(filterCats).each(function() { - if (categoriesMap[this].indexOf(acronym) > -1) { - newResults.push(result); - } - }); - }); - return newResults; -} - -function shortenDefinition(def) { - var defLimit = 210, - defWords = null; - if (typeof def !== "undefined" && def !== null && def.length > 0) { - // Make sure definitions isn't an array - def = (typeof def === "string") ? def : def.join(". "); - // Strip out xml elements and/or html - def = jQuery("
").html(def).text(); - if (def.length > defLimit) { - defWords = def.slice(0, defLimit).split(" "); - // Remove the last word in case we got one partway through - defWords.pop(); - def = defWords.join(" ") + " ..."; - } - } - jQuery(document).trigger("search_results_updated"); - return def || ""; -} - -function advancedOptionsSelected() { - var selected = null, - check = null, - i = null, - j = null; - if (document.URL.indexOf("opt=advanced") >= 0) { - return true; - } - check = [ - - function() { - return jQuery("#search_include_properties").is(":checked"); - }, - function() { - return jQuery("#search_include_views").is(":checked"); - }, - function() { - return jQuery("#search_include_non_production").is(":checked"); - }, - function() { - return jQuery("#search_include_obsolete").is(":checked"); - }, - function() { - return jQuery("#search_only_definitions").is(":checked"); - }, - function() { - return jQuery("#search_exact_match").is(":checked"); - }, - function() { - return jQuery("#search_categories").val() !== null && (jQuery("#search_categories").val() || []).length > 0; - }, - function() { - return jQuery("#ontology_ontologyId").val() !== null && (jQuery("#ontology_ontologyId").val() || []).length > 0; - } - ]; - for (i = 0, j = check.length; i < j; i++) { - selected = check[i](); - if (selected) { - return true; - } - }; - return false; -} - -function ontologyIdToAcronym(id) { - return id.split("/").slice(-1)[0]; -} - -function getOntologyName(cls) { - var ont = jQuery(document).data().bp.ontologies[cls.links.ontology]; - if (typeof ont === 'undefined') { - return ""; - } - return " - " + ont.name + " (" + ont.acronym + ")"; -} - -function currentResultsCount() { - return jQuery(".search_result").length + jQuery(".search_result_additional").length; -} - -function currentOntologiesCount() { - return jQuery(".search_result").length; -} - -function classDiv(res, clsLabel, displayOntologyName) { - var clsID = null, - clsCode = null, - clsURI = null, - ontAcronym = null, - ontName = null, - clsAttr = null, - clsAnchor = null, - clsIdDiv = null; - ontAcronym = ontologyIdToAcronym(res.links.ontology); - clsID = res["@id"]; - clsCode = encodeURIComponent(clsID); - clsURI = "/ontologies/" + ontAcronym + "?p=classes&conceptid=" + clsCode; - ontName = displayOntologyName ? getOntologyName(res) : ""; - clsAttr = { - "title": res.prefLabel, - "data-bp_conceptid": clsID, - "data-exact_match": res.exactMatch, - "href": clsURI - }; - clsAnchor = jQuery(""); - clsAnchor.attr(clsAttr); - clsAnchor.append(clsLabel); - clsAnchor.append(ontName); - clsIdDiv = jQuery("
"); - clsIdDiv.addClass("concept_uri"); - clsIdDiv.text(res["@id"]); - return jQuery("
").addClass("class_link").append(clsAnchor).append(clsIdDiv); -} - - -function resultLinksSpan(res) { - var ontAcronym = null, - clsID = null, - clsCode = null, - detailsAttr = null, - detailsAnchor = null, - vizAttr = null, - vizAnchor = null, - resLinks = null; - ontAcronym = ontologyIdToAcronym(res.links.ontology); - clsID = res["@id"]; - clsCode = encodeURIComponent(clsID); - // construct link for class 'details' in facebox - detailsAttr = { - "href": "/ajax/class_details?modal=false&ontology=" + ontAcronym + "&conceptid=" + clsCode + "&styled=false", - "rel": "facebox[.class_details_pop]" - }; - detailsAnchor = jQuery(""); - detailsAnchor.attr(detailsAttr); - detailsAnchor.addClass("class_details"); - detailsAnchor.addClass("search_result_link"); - detailsAnchor.text("details"); - // construct link for class 'visualizer' in facebox - vizAttr = { - "href": "javascript:void(0);", - "data-bp_conceptid": clsID, - "data-bp_ontologyid": ontAcronym - }; - vizAnchor = jQuery(""); - vizAnchor.attr(vizAttr); - vizAnchor.addClass("class_visualize"); - vizAnchor.addClass("search_result_link"); - vizAnchor.text("visualize"); - resLinks = jQuery(""); - resLinks.addClass("additional"); - resLinks.append(detailsAnchor); - resLinks.append(" - "); - resLinks.append(vizAnchor); - return resLinks; -} - - -function definitionDiv(res, defClass) { - defClass = typeof defClass === "undefined" ? "def_container" : defClass; - return jQuery("
").addClass(defClass).text(shortenDefinition(res.definition)); -} - -function determineHTTPS(url) { - return url.replace("http:", ('https:' == document.location.protocol ? 'https:' : 'http:')); -} - -function toggleAdvancedSearchOptions() { - var elem = jQuery("#advanced_options"); - var searchOptions = jQuery("#search_options"); - - if (elem.text() == elem.data("text-swap")) { - elem.text(elem.data("text-original")); - searchOptions.hide(); - } else { - elem.data("text-original", elem.text()); - elem.text(elem.data("text-swap")); - searchOptions.show(); - } -} - - diff --git a/app/assets/stylesheets/components/index.scss b/app/assets/stylesheets/components/index.scss index 8a6ff12ccd..11229aadd2 100644 --- a/app/assets/stylesheets/components/index.scss +++ b/app/assets/stylesheets/components/index.scss @@ -27,3 +27,4 @@ @import "alert"; @import "progress_pages"; @import "select"; +@import "search_result"; \ No newline at end of file diff --git a/app/assets/stylesheets/components/search_result.scss b/app/assets/stylesheets/components/search_result.scss new file mode 100644 index 0000000000..9dfa051cb7 --- /dev/null +++ b/app/assets/stylesheets/components/search_result.scss @@ -0,0 +1,113 @@ +.search-result-component.sub-component{ + margin-bottom: 10px; +} + + +.search-result-component .title{ + color: var(--primary-color) !important; + font-size: 20px; + font-weight: 500; +} + +.search-result-component.sub-component .title{ + color: var(--primary-color) !important; + font-size: 18px; + font-weight: 500; +} + +.search-result-component .uri{ + color: #888888; + font-size: 14px; + margin: 3px 0; +} + +.search-result-component.sub-component .uri{ + color: #888888; + font-size: 12px; + margin: 3px 0; +} + +.search-result-component .actions{ + display: flex; + margin-top: 10px; +} + +.search-result-component.sub-component .actions{ + display: flex; + margin-top: 7px; +} + +.search-result-component .actions .button{ + display: flex; + justify-content: center; + align-items: center; + border-radius: 4px; + background-color: var(--light-color); + padding: 5px 13px; + margin-right: 10px; +} + +.search-result-component .actions .button:hover{ + cursor: pointer; +} + +.search-result-component .actions .button svg path{ + fill: var(--primary-color); +} + +.search-result-component .actions .button .text{ + color: var(--primary-color); + margin-left: 8px; +} + +.search-result-component.sub-component .actions .button .text{ + font-size: 12px; +} + +.search-result-component.sub-component .actions .button svg{ + width: 12px; +} + +.search-result-component .actions .button.icon-right .text{ + margin-right: 8px; + margin-left: 0; +} + +.more-from-ontology{ + display: flex; + margin-top: 10px; +} + +.more-from-ontology .vertical-line{ + width: 1px; + background-color: var(--primary-color); + border-radius: 100px; + margin-right: 30px; +} + +.search-result-sub-components{ + margin: 20px 0; +} + +.search-result-sub-components .reuses-title{ + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.search-result-sub-components .reuses-title div{ + margin-left: 10px; + font-size: 16px; + font-weight: 600; + color: #888888; +} + +.more-from-ontology.reuses{ + background-color: #F8F8F8; +} +.more-from-ontology.reuses .vertical-line{ + background-color: #888888; +} + + + diff --git a/app/assets/stylesheets/search.scss b/app/assets/stylesheets/search.scss index ab621fb364..584ee3fad0 100644 --- a/app/assets/stylesheets/search.scss +++ b/app/assets/stylesheets/search.scss @@ -1,185 +1,90 @@ -form.button-to { - float: left; +.search-page-container { + display: flex; + justify-content: center; } - -#ontology_picker_head { - font-size: 10pt !important; -} - -.not_visible { - position: fixed !important; - top: -999999px !important; -} - -#search_results_container #search_results_info { - display: none !important; -} - -#search_results_filter { - display: none; -} - -#search_spinner { - display: inline-block; - padding: 8px 4px; +.search-page-subcontainer { + width: 1248px; + padding: 20px 50px; } - -#search_options #ontology_ontologyId { - display: none; -} - -.class_details_pop { - overflow: auto; - width: 750px !important; - display: block !important; +.search-page-input-container{ + width: 100%; } -table#search_results td { - vertical-align: top; - padding: 12px 8px 12px 12px; +.search-page-input{ + position: relative; + padding-bottom: 80px; } - -div.search_result { - margin-bottom: 1.5em; -} - -div.class_link a { +.search-page-input input{ + position: absolute; + border-radius: 100px; + box-shadow: rgba(100, 100, 111, 0.1) 0 7px 29px 0; + border: none; + outline: none; font-size: 18px; + padding: 15px 25px; + width: 100%; } - -span.class_def { - display: block; - margin: 3px 0 2px; - font-size: 12px; -} - -.additional_ont_results { - padding: 2em; - margin: 2em; - background-color: rgb(230,230,230); -} - -.additional_cls_results { - padding: 2em; - margin: 2em; - background-color: rgb(230,230,230); -} - -.subordinate_ont_results { - padding: 1em; - padding-left: 2em; - padding-top: 2em; - margin: 1em; - margin-left: 2em; - margin-top: 2em; - background-color: rgb(240,240,240); -} - -.subordinate_ont_results_title { - color: rgb(100,100,100); - background: rgb(200,200,200); - padding: 0.5em; +.search-page-input input:focus{ + box-shadow: rgba(100, 100, 111, 0.2) 0 7px 29px 0; } - -div.search_result_additional { - padding-left: 30px; - margin: 1em 0 1.2em; +.search-page-advanced{ + display: flex; + margin-bottom: 15px; } - -div.search_result_additional .class_link a { - font-size: 15px !important; +.search-page-advanced .left{ + width: 600px; + margin-right: 40px; } - -div.additional_results_link { - margin-top: 5px; +.search-page-advanced .filter-container{ + margin-bottom: 15px; } - -.search_result_links a { - font-size: 11px !important; - color: green; +.search-page-advanced .filter-container .title{ + margin-bottom: 5px; + color: #888888; + font-size: 14px; } -.hide_link { - text-decoration: underline; - padding-left: 7px; -} -.not_underlined { - text-decoration: none; -} -#search_results { - display: none; -} -#search_results_container { - margin-top: .5em; - clear: both; +.search-page-options{ + } - -div#search_categories_chzn { - width: 432px !important; +.search-page-button{ + position: absolute; + top: 14px; + left: 1104px; + border: none; + background: none; } - -div#search_categories_chzn .chzn-choices input { - font-style: oblique; +.search-page-button svg path{ + fill: var(--primary-color) } - -div#search_categories_chzn.chzn-container-active input { - font-style: normal !important; +.search-page-button:hover{ + cursor: pointer; } - -div#search_categories_chzn .chzn-drop { - width: 432px !important; +.search-page-options{ + display: flex; + justify-content: space-between; } - -#search_messages { - font-style: oblique; - color: gray; - padding-bottom: 7px; -} - -#result_stats a { - color: gray; +.search-page-advanced-button{ + display: flex; } - -#ontology_counts { - display: none; +.search-page-advanced-button :hover{ + cursor: pointer; } - -.popup_counts { - float: right; - display: none; +.search-page-advanced-button .text{ + margin-left: 10px; + color: var(--primary-color); } -.ontology_counts_tooltip { - background-color: #EEEEEE; - border: 1px solid black; - color: black; - font-size: 12px; - padding: 10px 15px; - text-align: left; - z-index: 999; - box-shadow: 3px 3px 7px gray; +.search-page-advanced-button .icon svg path{ + fill: var(--primary-color); } - -.definition { - cursor: help; +.search-page-number-of-results{ + color: #888888; } -.concept_uri { - font-size: 9pt; - color: gray; +.search-page-result-element{ + margin-top: 40px; } - -#search_categories_chosen .chosen-container .chosen-container-multi { - width: 432px; -} - -/* Prevents placeholder text in search input from being truncated. -/* https://github.com/harvesthq/chosen/issues/2029#issuecomment-187442769 */ -#search_options #ontology_ontologyId_chosen .search-field:only-child, -#search_options #ontology_ontologyId_chosen .search-field:only-child input { - width: 100% !important; -} - diff --git a/app/components/chips_component.rb b/app/components/chips_component.rb index 0e9628bec9..9a2a4f0099 100644 --- a/app/components/chips_component.rb +++ b/app/components/chips_component.rb @@ -1,10 +1,10 @@ class ChipsComponent < ViewComponent::Base renders_one :count - def initialize(id:nil, name:, label: nil, value:, checked: false) + def initialize(id:nil, name:, label: nil, value: nil, checked: false) @id = id || name @name = name - @value = value + @value = value || 'true' @checked = checked @label = label || @value end diff --git a/app/components/display/search_result_component.rb b/app/components/display/search_result_component.rb new file mode 100644 index 0000000000..27e7382c54 --- /dev/null +++ b/app/components/display/search_result_component.rb @@ -0,0 +1,58 @@ + +class Display::SearchResultComponent < ViewComponent::Base + include ModalHelper + renders_many :subresults, Display::SearchResultComponent + renders_many :reuses, Display::SearchResultComponent + def initialize(number: 0,title: nil, ontology_acronym: nil ,uri: nil, definition: nil, link: nil, is_sub_component: false) + @title = title + @uri = uri + @definition = definition + @link = link + @is_sub_component = is_sub_component + @ontology_acronym = ontology_acronym + @number = number.to_s + end + + def sub_component_class + @is_sub_component ? 'sub-component' : '' + end + + def sub_ontologies_id + string = @number+'_sub_ontologies' + end + + def reuses_id + string = @number+'_reuses' + end + + def details_button + link_to_modal(nil, "/ajax/class_details?modal=true&ontology=#{@ontology_acronym}&conceptid=#{@uri}&styled=false", data: { show_modal_title_value: @title, show_modal_size_value: 'modal-xl' }) do + content_tag(:div, class: 'button') do + concat inline_svg_tag('icons/details.svg') + concat content_tag(:div, class: 'text') { 'Details' } + end + end + end + + def visualize_button + link_to_modal(nil, "/ajax/biomixer/?ontology=#{@ontology_acronym}&conceptid=#{@uri}", data: { show_modal_title_value: @title, show_modal_size_value: 'modal-xl' }) do + content_tag(:div, class: 'button') do + concat inline_svg_tag('icons/visualize.svg') + concat content_tag(:div, class: 'text') { 'Visualize' } + end + end + end + + def reveal_ontologies_button(text,id) + content_tag(:div, class: 'button icon-right', 'data-action': "click->reveal-component#toggle", 'data-id': id) do + concat(content_tag(:div, class: 'text') do + text + end) + concat(inline_svg_tag("icons/arrow-down.svg")) + end + end + + + + +end \ No newline at end of file diff --git a/app/components/display/search_result_component/search_result_component.html.haml b/app/components/display/search_result_component/search_result_component.html.haml new file mode 100644 index 0000000000..d7026d3075 --- /dev/null +++ b/app/components/display/search_result_component/search_result_component.html.haml @@ -0,0 +1,35 @@ +.search-result-component{class: sub_component_class, 'data-controller': 'reveal-component'} + %a.title{href: @link} + = @title + - if @uri + .uri + = @uri + - if @definition + .text + = @definition + .actions + = details_button + = visualize_button + - if subresults? + = reveal_ontologies_button("#{subresults.size} more from this ontology", sub_ontologies_id) + - if reuses? + = reveal_ontologies_button("Reuses in #{reuses.size} ontologies", reuses_id) + - if subresults? + .more-from-ontology.d-none{id: sub_ontologies_id} + .vertical-line + .search-result-sub-components + - subresults.each do |result| + .search-result-sub-component + = result + - if reuses? + .more-from-ontology.reuses.d-none{id: reuses_id} + .vertical-line + .search-result-sub-components + .reuses-title + = inline_svg_tag 'icons/reuses.svg' + %div Reuses in other ontologies + - reuses.each do |reuse| + .search-result-sub-component + = reuse + + \ No newline at end of file diff --git a/app/components/layout/reveal_component/reveal_component_controller.js b/app/components/layout/reveal_component/reveal_component_controller.js index 52badb5999..e30df8d76a 100644 --- a/app/components/layout/reveal_component/reveal_component_controller.js +++ b/app/components/layout/reveal_component/reveal_component_controller.js @@ -1,24 +1,35 @@ -import Reveal from 'stimulus-reveal-controller' +import { Controller } from "@hotwired/stimulus" -export default class extends Reveal { +export default class extends Controller{ static values = { - condition: String + condition: String, + hiddenClass : {type: String, default: "d-none"} } - connect() { - super.connect() - } + static targets = ["hideButton", "showButton", 'item' ] toggle(event) { if (!this.conditionValue) { - super.toggle() + this.#toggle(event) } else if (this.#shown() && !this.#conditionChecked(event)) { - super.toggle() + this.#toggle(event) } else if (!this.#shown() && this.#conditionChecked(event)) { - super.toggle() + this.#toggle(event) } } + show(event){ + this.#getItems(event).classList.remove(this.hiddenClassValue) + this.hideButtonTarget.classList.remove(this.hiddenClassValue) + this.showButtonTarget.classList.add(this.hiddenClassValue) + } + hide(event){ + this.#getItems(event).classList.add(this.hiddenClassValue) + this.hideButtonTarget.classList.add(this.hiddenClassValue) + this.showButtonTarget.classList.remove(this.hiddenClassValue) + } + + #conditionChecked(event) { return this.conditionValue === event.target.value } @@ -27,4 +38,24 @@ export default class extends Reveal { return !this.itemTargets[0].classList.contains(this.class); } + #toggle(event) { + this.#getItems(event).forEach((s) => { + s.classList.toggle(this.hiddenClassValue); + }); + } + + #ItemById(event){ + let button = event.target.closest("[data-id]"); + return document.getElementById(button.dataset.id); + } + #getItems(event){ + let items + if(this.hasItemTarget){ + items = this.itemTarget + } else { + items = [this.#ItemById(event)] + } + return items + } + } \ No newline at end of file diff --git a/app/controllers/concepts_controller.rb b/app/controllers/concepts_controller.rb index db1370004f..bcb1f34537 100644 --- a/app/controllers/concepts_controller.rb +++ b/app/controllers/concepts_controller.rb @@ -167,7 +167,8 @@ def details @concept = @ontology.explore.single_class({full: true}, CGI.unescape(params[:conceptid])) concept_not_found(CGI.unescape(params[:conceptid])) if @concept.nil? - + @container_id = params[:modal] ? 'application_modal_content' : 'concept_details' + if params[:styled].eql?("true") render :partial => "details", :layout => "partial" else diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb new file mode 100644 index 0000000000..443a07fdd7 --- /dev/null +++ b/app/controllers/concerns/search_aggregator.rb @@ -0,0 +1,230 @@ +module SearchAggregator + extend ActiveSupport::Concern + BLACKLIST_FIX_STR = [ + "https://", + "http://", + "bioportal.bioontology.org/ontologies/", + "purl.bioontology.org/ontology/", + "purl.obolibrary.org/obo/", + "swrl.stanford.edu/ontologies/", + "mesh.owl" # Avoids RH-MESH subordinate to MESH + ] + + BLACKLIST_REGEX = [ + /abnormalities/i, + /biological/i, + /biology/i, + /bioontology/i, + /clinical/i, + /extension/i, + /\.gov/i, + /ontology/i, + /ontologies/i, + /semanticweb/i + ] + + def aggregate_results(query, results) + ontologies = aggregate_by_ontology(results) + grouped_results = add_subordinate_ontologies(query, ontologies) + all_ontologies = LinkedData::Client::Models::Ontology.all(include: 'acronym,name', include_views: true, display_links: false, display_context: false) + + grouped_results.map do |group| + format_search_result(group, all_ontologies) + end + end + + def format_search_result(result, ontologies) + same_ont = result[:same_ont] + same_cls = result[:sub_ont] + result = same_ont.shift + ontology = result.links['ontology'].split('/').last + { + root: search_result_elem(result, ontology, ontology_name_acronym(ontologies, ontology)), + descendants: same_ont.map { |x| search_result_elem(x, ontology, '') }, + reuses: same_cls.map do |x| + format_search_result(x, ontologies) + end + } + end + + private + + def search_result_elem(class_object, ontology_acronym, title) + label = concept_label(class_object.prefLabel) + { + uri: class_object.id.to_s, + title: title.empty? ? label : "#{label} - #{title}", + ontology_acronym: ontology_acronym, + link: "/ontologies/#{ontology_acronym}?p=classes&conceptid=#{class_object.id}", + definition: Array(class_object.definition).join(' ') + } + end + + def concept_label(pref_labels_list, obsolete = false, max_length = 60) + # select closest to query + selected = pref_labels_list.select do |pref_lab| + pref_lab.include?(@search_query) || @search_query.include?(pref_lab) + end.first + + selected ||= (pref_labels_list&.first || '') + + selected = selected[0..max_length] if selected.size > max_length + selected = "#{selected}".html_safe if obsolete + selected + end + + def ontology_name_acronym(ontologies, selected_acronym) + ontology = ontologies.select { |x| x.acronym.eql?(selected_acronym.split('/').last) }.first + binding.pry if ontology.nil? + "#{ontology.name} (#{ontology.acronym})" + end + + def aggregate_by_ontology(results) + ontologies = {} + + results.each do |res| + ont = res.links['ontology'] + unless ontologies[ont] + ontologies[ont] = { + # classes with same URI + same_cls: [], + # other classes from the same ontology + same_ont: [], + # subordinate ontologies + sub_ont: [] + } + end + ontologies[ont][:same_ont] << res + end + ontologies.values + end + + def add_subordinate_ontologies(query, ontologies) + # get for each concept his main ontology parent + concepts_ontology_owner = extract_concepts_owners(ontologies, query) + + # aggregate the subordinate results below the owner ontology results + subordinate_ontologies = [] + ontologies.each_with_index do |ont, i| + cls_id = ont[:same_ont].first["@id"] + + if concepts_ontology_owner.has_key?(cls_id) + # get the ontology that owns this class (if any) + ont_owner = concepts_ontology_owner[cls_id] + if ont_owner[:index].eql?(i) + # the current ontology is the owner of this primary result + subordinate_ontologies.push(ont) + else + # There is an owner, so put this ont result set into the sub_ont array of the owner + real_owner = ontologies[ont_owner[:index]] + real_owner[:sub_ont].push(ont) + end + else + # There is no ontology that owns this primary class result, just + # display this at the top level (it's not a subordinate) + subordinate_ontologies.push(ont) + end + end + subordinate_ontologies + end + + def extract_concepts_owners(ontologies, query) + cls_ont_owner_tracker = {} + ontologies.each do |ont| + ont[:sub_ont] = [] # array for any subordinate ontology results regrouping the concept reuses + + cls_id = ont[:same_ont].first["@id"] + next if cls_ont_owner_tracker.has_key?(cls_id) + + # find the best match for the ontology owner (must iterate over all acronyms) + ont_owner = ontology_owner_of_class(cls_id, ontologies, query) + + # This primary class result is owned by an ontology + cls_ont_owner_tracker[cls_id] = ont_owner if ont_owner[:index] + end + cls_ont_owner_tracker + end + + def extract_back_list_words(acronyms, query) + blacklist_words = [] + query.split(/\s+/).each_with_index do |search_word, i| + # Convert blacklist_search_words_arr to regex constructs so they are removed + # with case-insensitive matches in blacklist_cls_id_components + blacklist_words.push(Regexp.new(search_word, Regexp::IGNORECASE)) + + # Check for any substring matches against ontology acronyms, where the + # acronyms are assumed to be upper case strings. + # Note: We cannot use the ont_acronyms array .index method because it doesn't search for substring matches. + search_token = search_word + match = false + + acronyms.each do |acronym| + match = acronym.include?(search_token) + break if match + end + + # Remove this blacklisted search token because it matches or partially matches an ontology acronym. + blacklist_words.delete_at(i) if match + end + blacklist_words + end + + def ontology_owner_of_class(cls_id, ontologies, query) + acronyms = ontologies.map { |ont| ont[:same_ont].first.links['ontology'].split('/').last } + + # Remove any items in blacklistSearchWordsArr that match ontology acronyms. + # TODO make sure this is really useful + blacklist_words = extract_back_list_words(acronyms, query) + + ont_owner = { + acronym: "", + index: nil, + weight: 0 + } + + acronyms.each_with_index do |acronym, i| + if ontology_own_class?(cls_id, acronym, blacklist_words) + weight = acronym.size * (cls_id.upcase.rindex(acronym) + 1) + if weight > ont_owner[:weight] + ont_owner = { + acronym: acronym, + index: i, + weight: weight + } + # Cannot break here, in case another acronym has greater weight. + end + end + end + + ont_owner + end + + def ontology_own_class?(cls_id, acronym, blacklist_words) + cls_id = blacklist_cls_id_components(cls_id.dup, blacklist_words) + + cls_id.upcase.include?(acronym) rescue binding.pry + end + + def blacklist_cls_id_components(cls_id, blacklist_words) + + stripped_id = cls_id + + # Remove fixed strings first + BLACKLIST_FIX_STR.each do |fixed_str| + stripped_id.gsub!(fixed_str, "") + end + + # Cleanup with regex replacements + BLACKLIST_REGEX.each do |regex| + stripped_id.gsub!(regex, "") + end + + # Remove search keywords (see perform_search and aggregate_results_with_subordinate_ontologies) + blacklist_words.each do |search_word_regex| + stripped_id.gsub!(search_word_regex, "") + end + + stripped_id + end +end + diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 5f8914e698..f6469e2808 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -398,7 +398,6 @@ def widgets end end - def show_additional_metadata @metadata = submission_metadata @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:id]).first diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 64e9ef2743..e6241d05c0 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,14 +1,24 @@ require 'uri' class SearchController < ApplicationController - + include SearchAggregator skip_before_action :verify_authenticity_token layout :determine_layout def index - @search_query = params[:query].nil? ? params[:q] : params[:query] - @search_query ||= "" + @search_query = params[:query] || params[:q] || '' + @advanced_options_open = false + @search_results = [] + + return if @search_query.empty? + + params[:pagesize] = "150" + params[:ontologies] = params[:ontologies_list]&.join(",") + results = LinkedData::Client::Models::Class.search(@search_query, params).collection + + @advanced_options_open = !search_params_empty? + @search_results = aggregate_results(@search_query, results) end def json_search @@ -37,12 +47,12 @@ def json_search target_value = result.prefLabel.select{|x| x.include?( params[:q].delete('*'))}.first || result.prefLabel.first case params[:target] - when "name" - target_value = result.prefLabel - when "shortid" - target_value = result.id - when "uri" - target_value = result.id + when "name" + target_value = result.prefLabel + when "shortid" + target_value = result.id + when "uri" + target_value = result.id end acronym = result.links["ontology"].split('/').last @@ -110,21 +120,18 @@ def check_params_ontologies(params) end end - def format_record_type(record_type, obsolete = false) - case record_type - when "apreferredname" - record_text = "Preferred Name" - when "bconceptid" - record_text = "Class ID" - when "csynonym" - record_text = "Synonym" - when "dproperty" - record_text = "Property" - else - record_text = "" - end - record_text = "Obsolete Class" if obsolete - record_text + def search_params + [ + :ontologies, :categories, + :include_properties, :obsolete, :include_views, + :exact_match, :require_definition + ] + end + + def search_params_empty? + (params[:lang].nil? || params[:lang].eql?('all')) && + search_params.all?{|key| params[key].nil? || params[key].empty?} end end + diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3ecd43c533..507078a0eb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -60,6 +60,7 @@ def isOwner?(id) end end end + def encode_param(string) diff --git a/app/helpers/multi_languages_helper.rb b/app/helpers/multi_languages_helper.rb index f1c0f83642..81d6058188 100644 --- a/app/helpers/multi_languages_helper.rb +++ b/app/helpers/multi_languages_helper.rb @@ -56,11 +56,12 @@ def search_languages # top ten spoken languages portal_languages.keys + %w[zh es hi ar bn pt ru ur id] end - def search_language_selector(id: 'search_language', name: 'search_language') + def search_language_selector(id: 'search_language', name: 'search_language', selected: nil) render Input::LanguageSelectorComponent.new(id: id, name: name, enable_all: true, languages: search_languages, 'data-select-input-searchable-value': false, - title: search_language_help_text) + title: search_language_help_text, + selected: selected&.to_sym) end diff --git a/app/javascript/component_controllers/index.js b/app/javascript/component_controllers/index.js index 5eff9ec43d..129e09a6f6 100644 --- a/app/javascript/component_controllers/index.js +++ b/app/javascript/component_controllers/index.js @@ -28,5 +28,6 @@ application.register("search-input", Search_input_component_controller) application.register("tabs-container", Tabs_container_component_controller) application.register("circle-progress-bar", CircleProgressBarComponentController) application.register("alert-component", alert_component_controller) + application.register("progress-pages", Progress_pages_component_controller) application.register("reveal-component", Reveal_component_controller) diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js index 90a3788a2e..f0624491d2 100644 --- a/app/javascript/controllers/application.js +++ b/app/javascript/controllers/application.js @@ -17,6 +17,5 @@ application.register('read-more', ReadMore) import Timeago from 'stimulus-timeago' application.register('timeago', Timeago) export { application } -import Reveal from 'stimulus-reveal-controller' -application.register('reveal', Reveal) + diff --git a/app/views/concepts/_biomixer.html.erb b/app/views/concepts/_biomixer.html.erb deleted file mode 100644 index d4a6e22add..0000000000 --- a/app/views/concepts/_biomixer.html.erb +++ /dev/null @@ -1,71 +0,0 @@ -<%require 'cgi'%> -<% user_api_key = (session[:user].try(:apikey) || $BIOMIXER_APIKEY).to_s %> -<% rest_domain = $REST_URL.sub(/https?:\/\//, "") %> -<% src_url = "#{$BIOMIXER_URL}/?mode=embed&embed_mode=paths_to_root&ontology_acronym=#{@ontology.acronym}&full_concept_id=#{CGI.escape(@concept.fullId)}&userapikey=#{user_api_key}&restURLPrefix=#{rest_domain}" %> -<% original_src = src_url%> - - - -
- -
diff --git a/app/views/concepts/_biomixer.html.haml b/app/views/concepts/_biomixer.html.haml new file mode 100644 index 0000000000..9c6fe13328 --- /dev/null +++ b/app/views/concepts/_biomixer.html.haml @@ -0,0 +1,47 @@ += render_in_modal do + - require 'cgi' + - user_api_key = (session[:user].try(:apikey) || $BIOMIXER_APIKEY).to_s + - rest_domain = $REST_URL.sub(/https?:\/\//, "") + - src_url = "#{$BIOMIXER_URL}/?mode=embed&embed_mode=paths_to_root&ontology_acronym=#{CGI.escape(@ontology.acronym)}&full_concept_id=#{CGI.escape(@concept.fullId)}&userapikey=#{user_api_key}&restURLPrefix=#{rest_domain}" + - original_src = src_url + + :javascript + jQuery(document).data().bp.biomixer_fullscreen = {}; + jQuery(document).data().bp.biomixer_fullscreen.enabled = false; + jQuery(document).data().bp.biomixer_fullscreen.bd_container_h = jQuery("#bd_content .cls-info-container").css("height"); + jQuery(document).data().bp.biomixer_fullscreen.bd_contents_h = jQuery("#bd .bd_content .cls-info-container #contents").css("max-height"); + jQuery(document).data().bp.biomixer_fullscreen.bio_container_h = jQuery("#biomixer_container").css("height"); + jQuery(document).data().bp.biomixer_fullscreen.bd_container_w = jQuery("#bd_content .cls-info-container").css("width"); + jQuery(document).data().bp.biomixer_fullscreen.bio_container_w = jQuery("#biomixer_container").css("width"); + + jQuery(document).data().bp.biomixer_fullscreen = { + maximize: 'hide', + minimize: 'show' + }; + + jQuery(document).data().bp.biomixer_fullscreen.toggle = function(toggle) { + jQuery("#bd_content .sidebar")[toggle](); + jQuery("#bd_content .gutter")[toggle](); + jQuery("#bd_content .cls-info-container .tabs")[toggle](); + jQuery(document).data().bp.biomixer_fullscreen.heights(toggle === 'show'); + } + + jQuery(document).data().bp.biomixer_fullscreen.heights = function(original) { + var height; + if (original) { + jQuery("#bd_content .cls-info-container").css("height", jQuery(document).data().bp.biomixer_fullscreen.bd_container_h); + jQuery("#bd .bd_content .cls-info-container #contents").css("max-height", jQuery(document).data().bp.biomixer_fullscreen.bd_contents_h); + jQuery("#biomixer_container").css("height", jQuery(document).data().bp.biomixer_fullscreen.bio_container_h); + jQuery("#bd_content .cls-info-container").css("width", jQuery(document).data().bp.biomixer_fullscreen.bd_container_w); + jQuery("#biomixer_container").css("padding", 0).css("width", jQuery(document).data().bp.biomixer_fullscreen.bio_container_w); + jQuery("#bd_content").trigger('resize'); + } else { + height = jQuery(window).height() - jQuery("#bd_content .cls-info-container").offset().top - 50; + jQuery("#bd_content .cls-info-container").css("width", "100%").css("height", height); + jQuery("#bd .bd_content .cls-info-container #contents").css("max-height", height); + jQuery("#biomixer_container").css("padding", 0).css("width", "100%").css("height", height); + } + } + + #biomixer_container(style="padding-left: .5%; width: 99.5%; height: 900px;") + %iframe#biomixer_iframe(src=original_src data-src=src_url style="min-height: 700px;" height="100%" width="100%" frameborder="0") diff --git a/app/views/concepts/_details.html.haml b/app/views/concepts/_details.html.haml index deb946f824..4b2f986259 100644 --- a/app/views/concepts/_details.html.haml +++ b/app/views/concepts/_details.html.haml @@ -1,4 +1,4 @@ -= turbo_frame_tag 'concept_details' do += turbo_frame_tag @container_id do - schemes_keys = %w[hasTopConcept topConceptOf] - label_xl_set = %w[skos-xl#prefLabel skos-xl#altLabel skos-xl#hiddenLabel] diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index 91664b0a8e..e51ca2ba13 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -1,77 +1,75 @@ -- @title = t("search.title") +.search-page-container + .search-page-subcontainer{'data-controller': 'reveal-component'} + = form_tag(search_path, method: :get, 'data-turbo': true) do + .search-page-input-container{'data-controller': 'reveal'} + .search-page-input + %input{type:"text", placeholder:"Enter a term, e.g. Melanoma", name: "q", value: @search_query} + %button.search-page-button{type:'submit'} + = inline_svg_tag 'icons/search.svg' -%div.container.mt-5 - %h1.display-4 - = t("search.class_search") + .search-page-advanced{'data-reveal-component-target': 'item', class: "#{@advanced_options_open ? '' : 'd-none'}"} + .left + .filter-container + .title + Search language + .field + = search_language_selector(name: 'lang', selected: params[:lang]) + + .filter-container + .title + = t("search.ontologies") + .field + - get_ontologies_data + = render Input::SelectComponent.new(id: 'search-ontologies', name: 'ontologies_list[]', value: @onts_for_select, multiple: "multiple", selected: params[:ontologies_list]) + + .right + .filter-container + .title + Include in search + .d-flex + = render(ChipsComponent.new(name: 'include_properties', label: 'Property values', checked: params[:include_properties])) + = render(ChipsComponent.new(name: 'obsolete', label: 'Obsolete classes', checked: params[:obsolete])) + = render(ChipsComponent.new(name: 'include_views', label: 'Ontology views', checked: params[:include_views])) - = form_tag("/search", method: "post") do - %div.form-group - = text_field_tag("search_keywords", nil, class: "form-control", aria: {describedby: "classSearchHelpBlock"}) - %small#classSearchHelpBlock.form-text.text-muted - = t("search.index.search_keywords_placeholder") - = link_to(t('help'), Rails.configuration.settings.links[:help_search], id: "search-help", - aria: {label: t('search.view_search_documentation')}, class: "float-right") - %div.form-group - = link_to(t('search.show_advanced_options'), "javascript:void(0)", id: "advanced_options", data: {text_swap: t('search.hide_advanced_options')}, class: "form-text") - - -# Advanced search options - %div#search_options - %div.form-group.row - %div.col-sm-2.mb-4 Search language - %div.col-sm-10.mb-4 - %div.w-25 - = search_language_selector - %div.col-sm-2= t("search.include_in_search") + ":" - %div.col-sm-10 - %div.form-check - = check_box(:search, :include_properties, class: "form-check-input") - = label(:search, :include_properties, t('search.property_values'), class: "form-check-label definition", title: t(".property_definition")) - %div.form-check - = check_box(:search, :include_obsolete, class: "form-check-input") - = label(:search, :include_obsolete,t('search.obsolete_classes'), class: "form-check-label definition", title: t(".obsolete_definition")) - %div.form-check - = check_box(:search, :include_views, class: "form-check-input") - = label(:search, :include_views, t('search.ontology_views'), class: "form-check-label") + .filter-container + .title + Show only + .d-flex + = render(ChipsComponent.new(name: 'exact_match', label: 'Exact Matches', checked: params[:exact_match])) + = render(ChipsComponent.new(name: 'require_definition', label: 'Classes with definitions', checked: params[:require_definition])) - %div.col-sm-2= t("search.narrow_search_to") + ":" - %div.col-sm-10 - %div.form-check - = check_box(:search, :exact_match, class: "form-check-input") - = label(:search, :exact_match, t("exact_matches"), class: "form-check-label") - %div.form-check - = check_box(:search, :require_definition, class: "form-check-input") - = label(:search, :require_definition, t("search.classes_with_definitions"), class: "form-check-label") - %div.form-group - %h6{style: "font-size: 10pt !important"}= t("search.categories") - = select(:search, :categories, options_for_select(categories_for_select), {}, style: "width: 432px", multiple: "true", data: {placeholder: t("search.index.categories_placeholder")}) - %div.form-group.mb-5{style: "width:432px"} - = render :partial => "shared/ontology_picker", locals: {sel_text: t("search.ontologies")} - - = button_tag(t('search.title'), id: "search_button", class: "btn btn-primary") - = content_tag(:span, id: "search_spinner") do - %img{src: asset_path('spinners/spinner_000000_16px.gif'), style: "vertical-align: middle;"} - - -# Search results - %div.row.mt-4#search_results_container - %div.col - #result_stats - #search_messages - #search_results - -%div#biomixer{style: "display: none;"} - -:javascript - // Hash of ontology id => name, acronym for lookup use via JS - - jQuery(document).ready(function() { + .search-page-options + - if @search_results + .search-page-number-of-results + = "Match in #{@search_results.length} ontologies" + .search-page-advanced-button.show-options{class: "#{@advanced_options_open ? 'd-none' : ''}",'data': {'action': 'click->reveal-component#show', 'reveal-component-target': 'showButton'}} + .icon + =inline_svg_tag 'icons/settings.svg' + .text + Show advanced options + .search-page-advanced-button.hide-options{class: "#{@advanced_options_open ? '' : 'd-none'}", 'data': {'action': 'click->reveal-component#hide', 'reveal-component-target': 'hideButton'}} + .icon + =inline_svg_tag 'icons/hide.svg' + .text + hide advanced options + - if @search_results + .search-page-results-container + - number = 0 + - @search_results.each do |result| + .search-page-result-element + - number = number + 1 + - descendants = result[:descendants] + - reuses = result[:reuses] + - result[:root][:number] = number + = render Display::SearchResultComponent.new(result[:root]) do |c| + - descendants.each { |d| c.subresult(d.merge(is_sub_component: true))} + - reuses.each do |r| + - c.reuse(r[:root].merge(is_sub_component: true)) do |b| + - r[:descendants].each { |dd| b.subresult(dd.merge(is_sub_component: true))} - jQuery(document).data().bp.ontologies = #{Hash[LinkedData::Client::Models::Ontology.all(include_views: true).map {|o| [o.id, {name: o.name, acronym: o.acronym}]}].to_json.html_safe} - - if (jQuery("#search_keywords").val() !== "") { - performSearch(); - } - }); - - - + - if @search_results.empty? && !@search_query.empty? + .browse-empty-illustration + %img{:src => "#{asset_path("empty-box.svg")}"} + %p No result was found + diff --git a/config/bioportal_config_env.rb.sample b/config/bioportal_config_env.rb.sample index 4cc57816bc..5a27a28365 100644 --- a/config/bioportal_config_env.rb.sample +++ b/config/bioportal_config_env.rb.sample @@ -105,6 +105,9 @@ $ANNOUNCE_LIST = ENV['SUPPORT_EMAIL'] $SUPPORT_EMAIL = ENV['SUPPORT_EMAIL'] # Email used to send notifications $NOTIFICATION_EMAIL = ENV['SUPPORT_EMAIL'] + +# Bugsnag is a tool for exceptions tracking https://www.bugsnag.com/ +$BUGSNAG_APIKEY # reCAPTCHA # In order to use reCAPTCHA on the account creation and feedback submission pages: # 1. Obtain a reCAPTCHA v2 key from: https://www.google.com/recaptcha/admin diff --git a/config/initializers/bugsnag.rb b/config/initializers/bugsnag.rb new file mode 100644 index 0000000000..3d770d8eff --- /dev/null +++ b/config/initializers/bugsnag.rb @@ -0,0 +1,3 @@ +Bugsnag.configure do |config| + config.api_key = $BUGSNAG_APIKEY +end diff --git a/config/locales/de.yml b/config/locales/de.yml index 4524bf312a..618e7a7f15 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -368,7 +368,6 @@ de: reproduce_results: Reproduzieren Sie diese Ergebnisse mit der score: Ergebnis search: - categories: Kategorien class_search: Klasse suchen classes_with_definitions: Klassen mit Definitionen hide_advanced_options: Erweiterte Optionen ausblenden diff --git a/config/locales/en.yml b/config/locales/en.yml index 3daa18ad8b..2ca520abe0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -373,7 +373,6 @@ en: score: Score search: - categories: Categories class_search: Class Search classes_with_definitions: Classes with definitions hide_advanced_options: Hide advanced pptions diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 78d888ff9d..68d23840df 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -373,7 +373,6 @@ fr: score: Score search: - categories: Catégories class_search: Recherche de classe classes_with_definitions: Classes avec définitions hide_advanced_options: Masquer les options avancées diff --git a/config/locales/it.yml b/config/locales/it.yml index 1075422193..48f524456a 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -361,7 +361,6 @@ it: reproduce_results: Riprodurre questi risultati utilizzando il metodo score: Punteggio search: - categories: Categorie class_search: Ricerca di classe classes_with_definitions: Classi con definizioni hide_advanced_options: Nascondere le opzioni avanzate diff --git a/config/routes.rb b/config/routes.rb index c5bb9654e8..06f04b619f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -189,6 +189,8 @@ get 'jambalaya/:ontology/:id' => 'visual#jam', :as => :jam + # Search + get 'search', to: 'search#index' ########################################################################################################### # Install the default route as the lowest priority. get '/:controller(/:action(/:id))' diff --git a/package.json b/package.json index f33c80df4a..ac2ab6e4bc 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "stimulus-flatpickr": "^3.0.0-0", "stimulus-rails-nested-form": "^4.0.0", "stimulus-read-more": "^4.1.0", - "stimulus-reveal-controller": "^4.1.0", "stimulus-timeago": "^4.1.0", "tippy.js": "^6.3.7", "tom-select": "^2.2.2", diff --git a/test/components/previews/display/search_result_component_preview.rb b/test/components/previews/display/search_result_component_preview.rb new file mode 100644 index 0000000000..3f5dabe440 --- /dev/null +++ b/test/components/previews/display/search_result_component_preview.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Display::SearchResultComponentPreview < ViewComponent::Preview + + + def default() + render Display::SearchResultComponent.new(title:'height - INRAE Thesaurus (INRAETHES)' , uri: 'http://opendata.inrae.fr/thesaurusINRAE/c_17053', text: 'Height of plant from ground to top of spike, excluding awns.') + end + end + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8d0d998727..ed8f899a79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -769,11 +769,6 @@ stimulus-read-more@^4.1.0: resolved "https://registry.yarnpkg.com/stimulus-read-more/-/stimulus-read-more-4.1.0.tgz#f34efb2dcb33fd091936d84c569937bc100506c8" integrity sha512-SJyCJqZrhDSKpfrepnhStBaxtyv6Jnvr+b84GDg3l+/BzL5HaFLYmc6QkSNCeR6y0x+Zw7lwKuzv+XzyAm1KzQ== -stimulus-reveal-controller@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/stimulus-reveal-controller/-/stimulus-reveal-controller-4.1.0.tgz#bf0fb4c2706f22d41544b5b02e2fbd794f608575" - integrity sha512-cPpTLV/+IQgiE+J3iBMjf3kD3H9ZOeoRJjyhvcsjyPE82mdcsuWxlzpI1pwSJPN66qSud4hVkhNH5w4xadyOfA== - stimulus-timeago@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/stimulus-timeago/-/stimulus-timeago-4.1.0.tgz#5e4b712d9eadd7f0e2b3b142f35f334dba4b3857"