diff --git a/app/assets/stylesheets/annotator.scss b/app/assets/stylesheets/annotator.scss index fb051b5f3..8cb42cc98 100644 --- a/app/assets/stylesheets/annotator.scss +++ b/app/assets/stylesheets/annotator.scss @@ -45,7 +45,7 @@ } .annotator-page-text-area .insert-sample-text-button{ display: flex; - justify-content: end; + justify-content: flex-end; padding: 10px 20px 20px 20px; } .annotator-page-text-area .insert-sample-text-button .button{ diff --git a/app/assets/stylesheets/components/progress_pages.scss b/app/assets/stylesheets/components/progress_pages.scss index 6e3f6cff5..42b055fa4 100644 --- a/app/assets/stylesheets/components/progress_pages.scss +++ b/app/assets/stylesheets/components/progress_pages.scss @@ -37,7 +37,7 @@ .progress-pages-container .progress-item:last-of-type { & > div { - align-items: end; + align-items: flex-end; left: -23px; } } diff --git a/app/assets/stylesheets/components/search_input.scss b/app/assets/stylesheets/components/search_input.scss index 8d01cbb9a..c92436daa 100644 --- a/app/assets/stylesheets/components/search_input.scss +++ b/app/assets/stylesheets/components/search_input.scss @@ -11,6 +11,7 @@ .search-content{ display: flex; + flex-wrap: wrap; color: #777777 !important; justify-content: space-between; padding: 20px 20px; @@ -26,10 +27,14 @@ .search-content div img{ width: 12px; } + .search-content div p{ font-weight: 300; - margin-left: 10px; margin-bottom: 0; + word-break: break-word; +} +.search-content div small { + word-break: break-word; } .search-dropdown-active{ @@ -38,6 +43,7 @@ .search-element{ margin-bottom: 0; + max-width: 280px; } .searched-elements{ @@ -49,24 +55,40 @@ .home-search-button{ position: absolute; - top: 42px; - right: 20px; + top: 38px; + right: 12px; +} + +.home-search-button svg{ width: 15px; height: 15px; cursor: pointer; } -.home-search-button path{ +.home-search-button svg path{ fill: var(--primary-color); } .home-search-button.search-input-nav-icon{ position: absolute; - top: 11px; - right: 8px; + top: 4px; + right: 6px; + z-index: 99; +} +.home-search-button.search-input-nav-icon svg{ width: 11px; height: 11px; cursor: pointer; - z-index: 99; } -.home-search-button.search-input-nav-icon path{ + +.home-search-button.search-input-nav-icon svg path{ fill: white; } +.search-component-loader{ + padding-top: 4px; + padding-right: 2px; + color: var(--primary-color); +} +.home-search-button.search-input-nav-icon .search-component-loader{ + margin-top: 0; + padding-right: 0; + color: white; +} diff --git a/app/assets/stylesheets/recommender.scss b/app/assets/stylesheets/recommender.scss index b89ec8349..07ba6952f 100644 --- a/app/assets/stylesheets/recommender.scss +++ b/app/assets/stylesheets/recommender.scss @@ -52,7 +52,7 @@ } .recommender-page-text-area .insert-sample-text-button{ display: flex; - justify-content: end; + justify-content: flex-end; padding: 20px; } .recommender-page-text-area .insert-sample-text-button .button{ @@ -176,7 +176,7 @@ } .recommender-result-highlighted{ display: flex; - justify-content: end; + justify-content: flex-end; } .recommender-page-text-area-results a{ font-weight: 600; diff --git a/app/assets/stylesheets/search.scss b/app/assets/stylesheets/search.scss index e19e08ebf..6135a122c 100644 --- a/app/assets/stylesheets/search.scss +++ b/app/assets/stylesheets/search.scss @@ -11,8 +11,11 @@ } .search-page-input{ - display: flex; - margin-bottom: 25px; + position: relative; + padding-bottom: 80px; + .search-container{ + top:65px; + } } .search-page-input input{ border-radius: 8px; diff --git a/app/components/agent_search_input_component/agent_search_input_component.html.haml b/app/components/agent_search_input_component/agent_search_input_component.html.haml index 13a1101ed..89f0fdd30 100644 --- a/app/components/agent_search_input_component/agent_search_input_component.html.haml +++ b/app/components/agent_search_input_component/agent_search_input_component.html.haml @@ -6,6 +6,6 @@ - s.template do %a{href: "LINK", class: "search-content", 'data-turbo-frame': '_self'} %p.search-element.home-searched-ontology - NAME(ACRONYM) + NAME (IDENTIFIERS) %p.home-result-type TYPE \ No newline at end of file diff --git a/app/components/search_input_component/search_input_component.html.haml b/app/components/search_input_component/search_input_component.html.haml index d77759858..fb0d79f28 100644 --- a/app/components/search_input_component/search_input_component.html.haml +++ b/app/components/search_input_component/search_input_component.html.haml @@ -6,11 +6,18 @@ 'data-search-input-scroll-down-value': @scroll_down.to_s, 'data-search-input-selected-item-value': 0 } + - if @search_icon_type - %a{href: '/search', 'data-search-input-target': 'button'} - = inline_svg_tag 'arrow-right.svg', class: "home-search-button #{nav_icon_class}" + %div{class: "home-search-button #{nav_icon_class}"} + %a{href: '/search', 'data-search-input-target': 'button'} + .search-component-arrow + = inline_svg_tag 'arrow-right.svg' + .search-component-loader.d-none{'data-search-input-target': 'loader'} + = render LoaderComponent.new(small: true) - else + %a.d-none{'data-search-input-target': 'loader'} %a.d-none{'data-search-input-target': 'button'} + = render Input::InputFieldComponent.new(name: @name, placeholder: @placeholder, data: {'action': 'input->search-input#search blur->search-input#blur keydown.down->search-input#arrow_down keydown.up->search-input#arrow_up keydown.enter->search-input#enter_key', 'search-input-target': 'input'}) diff --git a/app/components/search_input_component/search_input_component_controller.js b/app/components/search_input_component/search_input_component_controller.js index f3322977c..5c8d07cad 100644 --- a/app/components/search_input_component/search_input_component_controller.js +++ b/app/components/search_input_component/search_input_component_controller.js @@ -1,9 +1,10 @@ import {Controller} from "@hotwired/stimulus" import useAjax from "../../javascript/mixins/useAjax"; +import debounce from 'debounce' // Connects to data-controller="search-input" export default class extends Controller { - static targets = ["input", "dropDown", "actionLink", "template", "button"] + static targets = ["input", "dropDown", "actionLink", "template", "button", "loader"] static values = { items: Array, ajaxUrl: String, @@ -19,11 +20,18 @@ export default class extends Controller { this.dropDown = this.dropDownTarget this.actionLinks = this.actionLinkTargets this.items = this.itemsValue + this.search = debounce(this.search.bind(this), 100); } search() { this.selectedItemValue = 0 - this.#searchInput() + this.loaderTarget.classList.remove("d-none") + this.buttonTarget.classList.add("d-none") + this.searchInput() + } + + searchInput() { + this.#fetchItems() } prevent(event){ @@ -74,7 +82,7 @@ export default class extends Controller { } else { useAjax({ type: "GET", - url: this.ajaxUrlValue + this.#inputValue(), + url: this.ajaxUrlValue + encodeURIComponent(this.#inputValue()), dataType: "json", success: (data) => { this.items = data.map(x => { return {...x, link: (this.itemLinkBaseValue + x[this.idKeyValue])}} ) @@ -89,6 +97,8 @@ export default class extends Controller { } #renderLines() { + this.loaderTarget.classList.add("d-none") + this.buttonTarget.classList.remove("d-none") const inputValue = this.#inputValue(); let results_list = [] if (inputValue.length > 0) { @@ -112,9 +122,8 @@ export default class extends Controller { let text = Object.values(item).reduce((acc, value) => acc + value, "") - // Check if the item contains the substring - if (text.toLowerCase().includes(inputValue.toLowerCase())) { + if (!this.cacheValue || text.toLowerCase().includes(inputValue.toLowerCase())) { results_list.push(item); breaker = breaker + 1 } @@ -149,13 +158,9 @@ export default class extends Controller { value = value.toString().split('/').slice(-1) } const regex = new RegExp('\\b' + key + '\\b', 'gi'); - string = string.replace(regex, value.toString()) + string = string.replace(regex, value ? value.toString() : "") }) return new DOMParser().parseFromString(string, "text/html").body.firstElementChild } - - #searchInput() { - this.#fetchItems() - } } diff --git a/app/controllers/concerns/search_content.rb b/app/controllers/concerns/search_content.rb new file mode 100644 index 000000000..861d6470a --- /dev/null +++ b/app/controllers/concerns/search_content.rb @@ -0,0 +1,172 @@ +module SearchContent + extend ActiveSupport::Concern + def search_ontologies(query: '*', groups: [], categories: [], languages: [], private_only: false, formats: [], + is_of_type: [], formality_level: [], + show_views: false, status: 'alpha,beta,production', + page: 1, page_size: 10) + + visibility = private_only ? "private" : 'public' + qf = [ + "ontology_acronymSuggestEdge^25 ontology_nameSuggestEdge^15 descriptionSuggestEdge^10 ", # start of the word first + "ontology_acronym_text^15 ontology_name_text^10 description_text^5 " + ] + submissions = LinkedData::Client::HTTP.get('search/ontologies', + { query: query.blank? ? "*" : query, + groups: groups, + hasDomain: categories, + hasOntologyLanguage: formats, + status: status, + page: page, pagesize: page_size, + visibility: visibility, + isOfType: is_of_type, + hasFormat: formality_level, + show_views: show_views, + qf: qf, # custom ranking + languages: languages } + .reject { |k, v| v.blank? }) + + submissions = submissions.collection + + submissions.map do |os| + transformed_os = OpenStruct.new + ontology = OpenStruct.new + metrics = OpenStruct.new + os.each_pair do |key, value| + if key.to_s.start_with?("ontology_") + ontology[key.to_s.sub("ontology_", "").gsub(/_.*\z/, "")] = value + elsif key.to_s.start_with?("metrics_") + metrics[key.to_s.sub("metrics_", "").gsub(/_.*\z/, "")] = value + elsif key != :links && key != :context + new_key = key.to_s.gsub(/_.*\z/, "") + transformed_os[new_key] = value + end + end + transformed_os[:ontology] = ontology unless ontology.to_h.empty? + transformed_os[:metrics] = metrics unless metrics.to_h.empty? + transformed_os + end + end + + def search_ontologies_content(query:, page: 1, page_size: 10, filter_by_ontologies: []) + acronyms = filter_by_ontologies + original_query = query + query = query.gsub(':', '\:').gsub('/', '\/') if page.eql?(1) + + qf = [ + "ontology_t^100 resource_id^50", + "http___www.w3.org_2004_02_skos_core_prefLabel_txt^30", + "http___www.w3.org_2004_02_skos_core_prefLabel_t^30", + "http___www.w3.org_2000_01_rdf-schema_label_txt^30", + "http___www.w3.org_2000_01_rdf-schema_label_t^30" + ] + ontologies = LinkedData::Client::Models::Ontology.all(include: 'acronym,name', also_include_views: true) + selected_onto = [] + + q = query.split(' ').first || '' + selected_onto += ontologies.select { |x| x.acronym.downcase.eql?(q.downcase) } + + selected_onto.uniq! + [selected_onto.first].compact.each do |o| + acr = o.acronym + acronyms << acr + query.gsub!(/\b#{acr}\b/, "") + query.gsub!(/\b#{acr.downcase}\b/, "") + query.gsub!('-', " ") + end + + query = query.strip + if query.blank? + query = "*" + elsif query.split(' ').size > 1 + query = "#{query}*" + else + query = "*#{query}*" + end + + results = LinkedData::Client::HTTP.get('search/ontologies/content', { q: query, qf: qf.join(' '), page: page, pagesize: page_size, ontologies: acronyms.first }) + search_content_result_to_json(original_query , query, results, ontologies, selected_onto) + end + + private + + def search_content_result_to_json(query, changed_query, results, ontologies, selected_onto = []) + json = [] + selected_onto = selected_onto.empty? ? ontologies.select { |x| x.acronym.downcase.include?(query.downcase) } : selected_onto + + json += selected_onto.map do |x| + { + id: ontology_path(id: x.acronym, p: 'summary'), + name: x.name, + acronym: x.acronym, + type: 'Ontology', + label: nil + } + end + + changed_query.gsub!('*', '') + + json += results.collection.map do |x| + next nil unless x.ontology_t + + label = nil + [ + "http___www.w3.org_2000_01_rdf-schema_label_t", + "http___www.w3.org_2000_01_rdf-schema_label_txt", + "http___www.w3.org_2004_02_skos_core_prefLabel_t", + "http___www.w3.org_2004_02_skos_core_prefLabel_txt", + ].each do |v| + v = Array(x[v]) + selected_label = v&.select { |p| p.downcase[changed_query.strip.downcase] || changed_query.downcase[p.strip.downcase] }&.first + label = selected_label if selected_label + label ||= v&.first + end + + + type = id_type(x.type_t, x.type_txt) + { + id: link_by_type(x.resource_id, x.ontology_t, type), + name: x.resource_id, + acronym: x.ontology_t, + type: type || '', + label: label + } + end.compact + + json + end + + def supported_types + %w[Concept Class Ontology ConceptScheme Collection NamedIndividual AnnotationProperty ObjectProperty DatatypeProperty] + end + + def id_type(type_t, type_txt) + + type = (Array(type_t) + Array(type_txt)).map { |x| helpers.link_last_part(x) } + .select{|x| supported_types.include?(x)} + + type = Array(type).reject { |x| x.eql?("NamedIndividual") } if (Array(type).size > 1) + + type.first + end + + def link_by_type(id, ontology, type) + case type + when 'Concept', 'Class' + ontology_path(id: ontology, p: 'classes', conceptid: id) + when 'Ontology' + ontology_path(id: ontology, p: 'summary') + when 'ConceptScheme' + ontology_path(id: ontology, p: 'schemes', schemeid: id) + when 'Collection' + ontology_path(id: ontology, p: 'collections', collectionid: id) + when 'NamedIndividual' + ontology_path(id: ontology, p: 'instances', instanceid: id) + when 'AnnotationProperty', 'ObjectProperty', 'DatatypeProperty' + ontology_path(id: ontology, p: 'properties', instanceid: id) + else + #"/content_finder?acronym=#{x.ontology_t}&uri=#{helpers.escape(x.resource_id)}&output_format=xml" + ontology_path(id: ontology, p: 'summary') + end + end +end + diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index f61ce08a6..622d6dafe 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,7 +1,8 @@ require 'uri' class SearchController < ApplicationController - include SearchAggregator + include SearchAggregator, SearchContent + skip_before_action :verify_authenticity_token layout :determine_layout @@ -99,6 +100,19 @@ def json_search render plain: response, content_type: content_type end + def json_ontology_content_search + query = params[:search] || '*' + page = (params[:page] || 1).to_i + acronyms = params[:acronyms] || [] + page_size = (params[:page_size] || 10).to_i + + results = search_ontologies_content(query: query, + page: page, + page_size: page_size, + filter_by_ontologies: acronyms) + + render json: results + end private diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index bf327b4c9..8905626ea 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -196,6 +196,14 @@ def onts_for_select onts_for_select end + def link_last_part(url) + if url.include?('#') + url.split('#').last + else + url.split('/').last + end + end + def get_categories_data(categories = nil) @categories_for_select = [] @categories_map = {} diff --git a/app/helpers/auto_complete_helper.rb b/app/helpers/auto_complete_helper.rb new file mode 100644 index 000000000..18b9afe23 --- /dev/null +++ b/app/helpers/auto_complete_helper.rb @@ -0,0 +1,21 @@ +module AutoCompleteHelper + + def ontologies_autocomplete + render OntologySearchInputComponent.new + end + + def ontologies_content_autocomplete(id: '', name: '', search_icon_type: 'home') + render SearchInputComponent.new(id: id, name: name, ajax_url: ajax_search_ontologies_content_path(search: ''), search_icon_type: search_icon_type, + item_base_url: "", id_key: 'id', placeholder: t("ontologies.ontology_search_prompt"), + use_cache: false, + actions_links: { search_ontology_content: "/search?query=o", browse_all_ontologies: "/ontologies?search=o" }) do |s| + s.template do + link_to "LINK", class: "search-content", 'data-turbo-frame': '_top' do + content_tag(:div, class: 'search-element home-searched-ontology flex-column') do + content_tag(:p, "LABEL") + content_tag(:small, "NAME") + content_tag(:small, "ACRONYM", class: 'text-primary') + end + content_tag(:p, "TYPE", class: 'home-result-type') + end + end + end + end +end \ No newline at end of file diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 6a1e1d9c5..7a010b73d 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -43,7 +43,7 @@ = t('home.index.welcome', site: $SITE) %p = t('home.index.tagline') - = render OntologySearchInputComponent.new(search_icon_type: 'home') + = ontologies_content_autocomplete .home-body-container .home-section diff --git a/app/views/layouts/_topnav.html.haml b/app/views/layouts/_topnav.html.haml index 973764cb6..4d0a01374 100644 --- a/app/views/layouts/_topnav.html.haml +++ b/app/views/layouts/_topnav.html.haml @@ -22,7 +22,7 @@ %input - else .nav-search-container - = render OntologySearchInputComponent.new(placeholder: t('layout.header.search_prompt', portal_name: portal_name ), scroll_down: false, search_icon_type: 'nav') + = ontologies_content_autocomplete(search_icon_type: 'nav') - if session[:user].nil? %a.nav-a{:href => "/login"}= t('layout.header.login') diff --git a/config/routes.rb b/config/routes.rb index 54656c39a..02d26d2c5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -193,6 +193,7 @@ # Search get 'search', to: 'search#index' + get 'ajax/search/ontologies/content', to: 'search#json_ontology_content_search' get 'check_resolvability' => 'check_resolvability#index' get 'check_url_resolvability' => 'check_resolvability#check_resolvability'