").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"