From ca8f5a5deab7d107c0955ce361871e1505b03f7d Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Thu, 22 Aug 2024 14:59:34 +0200 Subject: [PATCH 01/45] Fix: remove no more used mappings code (#733) * remove no more used mappings code * Fix: use multilingual helper to select the right label to show in search page (#735) * use multilingual helper to select the right label to show in search page * extract search_concept_label helper to re-use in concept jump to * change submission anchor to class, because can used multiple time in same page --- app/assets/javascripts/application.js | 1 - app/assets/javascripts/bp_mappings.js.erb | 145 ------------------ app/assets/stylesheets/mappings.scss | 16 -- .../display/search_result_component.rb | 2 + .../search_result_component.html.haml | 5 +- .../submission_status_component.html.haml | 2 +- app/controllers/concerns/search_aggregator.rb | 32 ++-- app/controllers/search_controller.rb | 5 +- app/helpers/multi_languages_helper.rb | 2 +- test/application_system_test_case.rb | 1 + test/system/submission_flows_test.rb | 2 +- 11 files changed, 28 insertions(+), 185 deletions(-) delete mode 100644 app/assets/javascripts/bp_mappings.js.erb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 3fe97607a4..1a42c65807 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_mappings //= require bp_admin //= require concepts //= require projects diff --git a/app/assets/javascripts/bp_mappings.js.erb b/app/assets/javascripts/bp_mappings.js.erb deleted file mode 100644 index c47edb29f9..0000000000 --- a/app/assets/javascripts/bp_mappings.js.erb +++ /dev/null @@ -1,145 +0,0 @@ -"use strict"; - -// The count returned may not match the actual number of mappings -// To get around this, we re-calculate based on the mapping table size - -function updateMappingCount() { - // Go through every tr in the concept mappings tables. Add 1 to the count if the tr is not filled with "currently no mappings" - var rows = jQuery("#concept_mappings_table tbody tr"), mappings_count = 0; - rows.each(function() { - if (this.innerHTML.indexOf("currently no") < 0) { - mappings_count++; - } - }); - if (mappings_count === null) { - mappings_count = rows.length; - } - jQuery("#mapping_count").html(mappings_count); -} - -// Also in bp_create_mappings.js -function updateMappingDeletePermissions() { - var mapping_permission_checkbox = jQuery("#delete_mappings_permission"); - if (mapping_permission_checkbox.length === 0){ - //console.error("Failed to select #delete_mappings_permission"); - jQuery("#delete_mappings_button").hide(); - jQuery(".delete_mappings_column").hide(); - jQuery("input[name='delete_mapping_checkbox']").prop('disabled', true); - } else { - // Ensure the permission checkbox is hidden. - mapping_permission_checkbox.hide(); - if (mapping_permission_checkbox.is(':checked')) { - jQuery("#delete_mappings_button").show(); - jQuery(".delete_mappings_column").show(); - jQuery("input[name='delete_mapping_checkbox']").prop('disabled', false); - } else { - jQuery("#delete_mappings_button").hide(); - jQuery(".delete_mappings_column").hide(); - jQuery("input[name='delete_mapping_checkbox']").prop('disabled', true); - } - } - jQuery("input[name='delete_mapping_checkbox']").prop('checked', false); -} - -function loadMappings(value) { - jQuery('#mappingCount').html(""); - jQuery('#mapping_load').show(); - - jQuery('#mappingCount').load('/mappings/count/' + value, function () { - jQuery('#mapping_load').hide(); - let mappings_table_elem = jQuery("#mapping_count_table"); - mappings_table_elem.effect("highlight", {color: "#F5F5F5"}, 500); - mappings_table_elem.dataTable({ - "paging": false, - "info": false, - "searching": false, - "columns": [ - { - "className": "text-left" - }, - { - "type": "num-fmt", - "orderSequence": ["desc", "asc"], - "className": "text-right" - } - ] - }); - }); -} - -jQuery(document).ready(function(){ - updateMappingCount(); - updateMappingDeletePermissions(); - - // Handle visualization changes - jQuery("#display_options input").on("click", function(e, button){ - var selectedOnt = jQuery("#search_ontologies").val(); - if (selectedOnt !== "") { - loadMappings(selectedOnt); - } - }); - - jQuery("#mappings-help").on("click", bpPopWindow); -}); - -jQuery(document).bind("tree_changed", updateMappingCount); - -// deleteMappings() is a callback that is called by "#delete_mappings_button" created in -// /app/views/mappings/_concept_mappings.html.haml -// The appearance of that button is controlled by updateMappingDeletePermissions(), which -// relies on @delete_mapping_permission in /app/views/mappings/_mapping_table.html.haml; which, -// in turn, is set by /app/controllers/application_controller.check_delete_mapping_permission() -function deleteMappings() { - var mappingsToDelete = [], params; - var ontologyId = jQuery(document).data().bp.ont_viewer.ontology_id; - var conceptId = jQuery(document).data().bp.ont_viewer.concept_id; - - jQuery("#delete_mappings_error").html(""); - jQuery("#delete_mappings_spinner").show(); - - jQuery("input[name='delete_mapping_checkbox']:checked").each(function(){ - mappingsToDelete.push(jQuery(this).val()); - }); - - params = { - mappingids: mappingsToDelete.join(","), - _method: "delete", - ontologyid: ontologyId, - conceptid: conceptId - }; - - jQuery("#delete_mappings_error").html(""); - - jQuery.ajax({ - url: "/mappings/mappingids", // routed to mappings_controller::destroy - type: "POST", - data: params, - - success: function(data){ - jQuery("#delete_mappings_spinner").hide(); - - var i, rowId, len = data.success.length; - for (i=0; i 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 diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index fbf58b4f65..841f0f948a 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -47,11 +47,12 @@ def json_search # record_type = format_record_type(result[:recordType], result[:obsolete]) record_type = "" - target_value = result.prefLabel.select{|x| x.include?( params[:q].delete('*'))}.first || result.prefLabel.first + label = search_concept_label(result.prefLabel) + target_value = label case params[:target] when "name" - target_value = result.prefLabel + target_value = label when "shortid" target_value = result.id when "uri" diff --git a/app/helpers/multi_languages_helper.rb b/app/helpers/multi_languages_helper.rb index 1c3b117831..d21d41a302 100644 --- a/app/helpers/multi_languages_helper.rb +++ b/app/helpers/multi_languages_helper.rb @@ -166,7 +166,7 @@ def display_in_multiple_languages(label) content_tag(:div) do raw(label.map do |key, value| Array(value).map do |v| - content_tag(:div) do + content_tag(:div, class: 'definition') do concat content_tag(:span, v) concat content_tag(:span, key.upcase, class: 'badge badge-secondary ml-1') unless key.to_s.upcase.eql?('NONE') || key.to_s.upcase.eql?('@NONE') end diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 9ed6c19be8..aaba7ccc84 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -136,6 +136,7 @@ def date_picker_fill_in(selector, value, index = 0) end def agent_search(name) + sleep 1 within(".search-inputs:last-of-type") do input = find("input[name^='agent']") agent_id = input[:name].split('agent').last diff --git a/test/system/submission_flows_test.rb b/test/system/submission_flows_test.rb index 54adbeab5a..6045308d04 100644 --- a/test/system/submission_flows_test.rb +++ b/test/system/submission_flows_test.rb @@ -148,7 +148,7 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text submission_2.URI assert_text submission_2.versionIRI - assert_selector '#submission-status', text: submission_2.version + assert_selector '.submission-status', text: submission_2.version assert_selector ".flag-icon-fr" # todo fix this submission_2.identifier.each do |id| assert_text id From 00ba8a18d673f5023b9602d3ca0c7dbf16319a53 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Thu, 22 Aug 2024 16:27:47 +0200 Subject: [PATCH 02/45] Feature: Remove chosen dependecy (#734) * use color gem to generate better colors for skos collections * replace chosen with tomselect in the collections and scheme selector * remove jquery chosen files --- Gemfile | 5 +- Gemfile.lock | 94 +- app/assets/javascripts/vendor.js | 1 - .../stylesheets/application.css.scss.erb | 1 - app/helpers/collections_helper.rb | 12 +- .../controllers/chosen_controller.js | 68 - app/javascript/controllers/index.js | 4 +- .../skos_collection_colors_controller.js | 22 +- .../controllers/skos_collections_selector.js | 49 + app/javascript/mixins/useChosen.js | 8 - .../_collections_picker.html.haml | 4 +- .../_concepts_tree.html.haml | 2 +- .../_scheme_picker.html.haml | 2 +- vendor/assets/images/chosen/chosen-sprite.png | Bin 538 -> 0 bytes .../assets/images/chosen/chosen-sprite@2x.png | Bin 738 -> 0 bytes vendor/assets/javascripts/chosen.jquery.js | 1269 ----------------- vendor/assets/stylesheets/chosen.css.scss | 452 ------ 17 files changed, 135 insertions(+), 1858 deletions(-) delete mode 100644 app/javascript/controllers/chosen_controller.js create mode 100644 app/javascript/controllers/skos_collections_selector.js delete mode 100644 app/javascript/mixins/useChosen.js delete mode 100755 vendor/assets/images/chosen/chosen-sprite.png delete mode 100755 vendor/assets/images/chosen/chosen-sprite@2x.png delete mode 100755 vendor/assets/javascripts/chosen.jquery.js delete mode 100755 vendor/assets/stylesheets/chosen.css.scss diff --git a/Gemfile b/Gemfile index edbac0dc24..51dc38ee9b 100644 --- a/Gemfile +++ b/Gemfile @@ -111,6 +111,9 @@ gem 'omniauth-google-oauth2' gem 'omniauth-keycloak' gem 'omniauth-orcid' +# Used to generate colors randomly +gem "color", "~> 1.8" + group :staging, :production, :appliance do # Application performance monitoring gem 'newrelic_rpm' @@ -176,4 +179,4 @@ group :test do # Testing framework for Rails gem 'rspec-rails' -end +end \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index e7a0c97f2a..a44d7ee5bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,13 +82,13 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) airbrussh (1.5.2) sshkit (>= 1.6.1, != 1.7.0) ast (2.4.2) - autoprefixer-rails (10.4.16.0) + autoprefixer-rails (10.4.19.0) execjs (~> 2) base64 (0.2.0) bcrypt_pbkdf (1.1.1) @@ -96,22 +96,22 @@ GEM bigdecimal (3.1.8) bindata (2.5.0) bindex (0.8.1) - bootsnap (1.18.3) + bootsnap (1.18.4) msgpack (~> 1.2) bootstrap (4.2.1) autoprefixer-rails (>= 9.1.0) popper_js (>= 1.14.3, < 2) sassc-rails (>= 2.0.0) brakeman (5.4.1) - bugsnag (6.27.0) + bugsnag (6.27.1) concurrent-ruby (~> 1.0) builder (3.3.0) - capistrano (3.18.1) + capistrano (3.19.1) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (2.1.0) + capistrano-bundler (2.1.1) capistrano (~> 3.1) capistrano-locally (0.3.0) capistrano (~> 3.0) @@ -133,9 +133,11 @@ GEM xpath (~> 3.2) chart-js-rails (0.1.7) railties (> 3.1) - childprocess (5.0.0) + childprocess (5.1.0) + logger (~> 1.5) coderay (1.1.3) - concurrent-ruby (1.3.3) + color (1.8) + concurrent-ruby (1.3.4) crack (1.0.0) bigdecimal rexml @@ -149,12 +151,12 @@ GEM reline (>= 0.3.8) deepl-rb (2.5.3) diff-lcs (1.5.1) - docile (1.4.0) + docile (1.4.1) domain_name (0.6.20240107) ed25519 (1.3.0) erubi (1.13.0) erubis (2.7.0) - excon (0.110.0) + excon (0.111.0) execjs (2.9.1) faraday (2.0.1) faraday-net_http (~> 2.0) @@ -168,13 +170,15 @@ GEM multipart-post (~> 2) faraday-net_http (2.1.0) ffi (1.16.3) + fiber-storage (1.0.0) flag-icons-rails (3.4.6.1) sass-rails globalid (1.2.1) activesupport (>= 6.1) - graphql (2.3.5) + graphql (2.3.14) base64 - graphql-client (0.22.0) + fiber-storage + graphql-client (0.23.0) activesupport (>= 3.0) graphql (>= 1.13.0) haml (5.2.2) @@ -185,7 +189,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - hashdiff (1.1.0) + hashdiff (1.1.1) hashie (5.0.0) highline (2.1.0) html2haml (2.3.0) @@ -196,7 +200,7 @@ GEM htmlbeautifier (1.4.3) htmlentities (4.3.4) http-accept (1.7.0) - http-cookie (1.0.6) + http-cookie (1.0.7) domain_name (~> 0.5) i18n (1.14.5) concurrent-ruby (~> 1.0) @@ -220,7 +224,7 @@ GEM activesupport (>= 3.0) nokogiri (>= 1.6) io-console (0.7.2) - irb (1.13.1) + irb (1.14.0) rdoc (>= 4.0.0) reline (>= 0.4.2) iso-639 (0.3.6) @@ -230,7 +234,7 @@ GEM thor (>= 0.14, < 2.0) jquery-ui-rails (7.0.0) railties (>= 3.2.16) - jsbundling-rails (1.3.0) + jsbundling-rails (1.3.1) railties (>= 6.0.0) json (2.7.2) json-jwt (1.16.6) @@ -240,7 +244,7 @@ GEM bindata faraday (~> 2.0) faraday-follow_redirects - jwt (2.8.1) + jwt (2.8.2) base64 language_server-protocol (3.17.0.3) launchy (3.0.1) @@ -256,6 +260,7 @@ GEM listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.0) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -283,21 +288,20 @@ GEM method_source (1.1.0) mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2024.0604) + mime-types-data (3.2024.0820) mini_mime (1.1.5) - minitest (5.23.1) + minitest (5.25.1) msgpack (1.7.2) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.4.1) - mutex_m (0.2.0) mysql2 (0.5.6) net-ftp (0.2.1) net-protocol time net-http (0.3.2) uri - net-imap (0.4.12) + net-imap (0.4.14) date net-protocol net-pop (0.1.2) @@ -312,7 +316,7 @@ GEM net-protocol net-ssh (7.2.3) netrc (0.11.0) - newrelic_rpm (9.10.2) + newrelic_rpm (9.12.0) nio4r (2.7.3) nokogiri (1.15.6-x86_64-darwin) racc (~> 1.4) @@ -325,8 +329,9 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0) version_gem (~> 1.1) - oj (3.16.4) + oj (3.16.5) bigdecimal (>= 3.0) + ostruct (>= 0.2) omniauth (2.1.2) hashie (>= 3.4.6) rack (>= 2.2.3) @@ -353,8 +358,9 @@ GEM omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) - parallel (1.25.1) - parser (3.3.3.0) + ostruct (0.6.0) + parallel (1.26.3) + parser (3.3.4.2) ast (~> 2.4.1) racc popper_js (1.16.1) @@ -363,10 +369,10 @@ GEM method_source (~> 1.0) psych (5.1.2) stringio - public_suffix (5.0.5) + public_suffix (5.1.1) puma (5.6.8) nio4r (~> 2.0) - racc (1.8.0) + racc (1.8.1) rack (2.2.9) rack-protection (3.2.0) base64 (>= 0.1.0) @@ -422,18 +428,18 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.3.0) + rexml (3.3.6) strscan rouge (4.3.0) rspec-core (3.13.0) rspec-support (~> 3.13.0) - rspec-expectations (3.13.1) + rspec-expectations (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.2) + rspec-rails (6.1.4) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -442,23 +448,23 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.1) - rubocop (1.64.1) + rubocop (1.65.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) + regexp_parser (>= 2.4, < 3.0) rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.3) + rubocop-ast (1.32.1) parser (>= 3.3.1.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) ruby_dig (0.0.2) - ruby_parser (3.21.0) + ruby_parser (3.21.1) racc (~> 1.5) sexp_processor (~> 4.16) rubyzip (2.3.2) @@ -476,7 +482,7 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sexp_processor (4.17.1) + sexp_processor (4.17.2) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -493,17 +499,16 @@ GEM sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) - sprockets-rails (3.5.1) + sprockets-rails (3.5.2) actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sshkit (1.22.2) + sshkit (1.23.0) base64 - mutex_m net-scp (>= 1.1.2) net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) - stimulus-rails (1.3.3) + stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.1) strscan (3.1.0) @@ -513,11 +518,11 @@ GEM terser (1.2.3) execjs (>= 0.3.0, < 3) thor (1.3.1) - tilt (2.3.0) + tilt (2.4.0) time (0.3.0) date timeout (0.4.1) - turbo-rails (2.0.5) + turbo-rails (2.0.6) actionpack (>= 6.0.0) activejob (>= 6.0.0) railties (>= 6.0.0) @@ -539,7 +544,7 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - websocket (1.2.10) + websocket (1.2.11) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -547,7 +552,7 @@ GEM xpath (3.2.0) nokogiri (~> 1.8) yard (0.9.36) - zeitwerk (2.6.15) + zeitwerk (2.6.17) PLATFORMS x86_64-darwin-23 @@ -569,6 +574,7 @@ DEPENDENCIES capistrano-yarn capybara chart-js-rails + color (~> 1.8) dalli debug deepl-rb diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index f395ce5982..93592936db 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -16,7 +16,6 @@ //= require bootstrap-sprockets // Other //= require jquery.dataTables -//= require chosen.jquery //= require autocomplete // Alertify is used the admin page //= require alertify diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb index 7f4793fcf7..8bf1869fa3 100755 --- a/app/assets/stylesheets/application.css.scss.erb +++ b/app/assets/stylesheets/application.css.scss.erb @@ -10,7 +10,6 @@ * *= require jquery-ui *= require alertify - *= require chosen *= require jquery-ui-1.8.1.custom *= require jquery.autocomplete *= require trumbowyg diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index 595cddabf3..bb81e39cef 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -75,9 +75,19 @@ def link_to_collection(collection, selected_collection_id) private + require 'color' + + def random_color + hue = rand(0..360) + saturation = rand(50..100) # Higher saturation for more vibrant colors + lightness = rand(30..70) # Middle lightness to avoid extremes + + Color::HSL.new(hue, saturation, lightness).html + end + def generate_collections_colors(collections) collections.each do |c| - c.color = format('#%06x', (rand * 0xffffff)) + c.color = random_color end end end diff --git a/app/javascript/controllers/chosen_controller.js b/app/javascript/controllers/chosen_controller.js deleted file mode 100644 index 2115f93e45..0000000000 --- a/app/javascript/controllers/chosen_controller.js +++ /dev/null @@ -1,68 +0,0 @@ -import {Controller} from "@hotwired/stimulus" -import {useChosen} from "../mixins/useChosen"; - -// Connects to data-controller="chosen" -export default class extends Controller { - // TODO to update to use TomSelect - static values = { - name: String, - enableColors: {type: Boolean, default: false} - } - - connect() { - useChosen(this.element, {width: '100%', allow_single_deselect: true}, this.#onChange.bind(this)) - } - - #onChange(event) { - const selected = Array.from(event.currentTarget.selectedOptions) - - if(this.enableColorsValue){ - this.#setColors(selected) - } - - const key = this.hasNameValue ? this.nameValue : event.currentTarget.name - const newData = { - [key]: selected.map(x => x.value) - } - this.element.dispatchEvent(new CustomEvent('changed', { - detail: { - data: newData - } - })) - } - - #setColors(selected){ - const snakeCase = string => { - return string.replace(/\W+/g, " ") - .split(/ |\B(?=[A-Z])/) - .map(word => word.toLowerCase()) - .join('_'); - } - const allChosenSelected = Array.from(document.querySelectorAll(`#${snakeCase(this.element.id +'_chosen')} .search-choice`)) - selected.forEach((s) => { - let color = s.dataset.color - if (color) { - const chosenSelected = allChosenSelected.filter(x => x.firstElementChild.textContent === s.text).pop() - const chosenText = chosenSelected.firstElementChild - const chosenClose = chosenSelected.lastElementChild - - chosenSelected.style.display = "flex" - chosenSelected.style.padding = "1px" - chosenSelected.style.paddingTop = "2px" - chosenSelected.style.paddingBottom = "2px" - - - chosenText.style.backgroundColor = color - chosenText.style.display = "inline-block" - chosenText.style.padding = "5px" - chosenText.style.borderRadius = "5px" - chosenText.style.color = "#fff" - chosenText.style.marginLeft = "2px" - chosenText.style.marginRight = "2px" - - chosenClose.style.position = "unset" - chosenClose.style.margin = "auto" - } - }) - } -} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 804ee3b3a7..24f39bf453 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -7,8 +7,8 @@ import { application } from "./application" import BrowseFiltersController from "./browse_filters_controller" application.register("browse-filters", BrowseFiltersController) -import ChosenController from "./chosen_controller" -application.register("chosen", ChosenController) +import Skos_collections_selector from "./skos_collections_selector" +application.register("skos-collections-selector", Skos_collections_selector) import ClassSearchAutoCompleteController from "./class_search_auto_complete_controller" application.register("class-search-auto-complete", ClassSearchAutoCompleteController) diff --git a/app/javascript/controllers/skos_collection_colors_controller.js b/app/javascript/controllers/skos_collection_colors_controller.js index 58baddf4d2..833b49205c 100644 --- a/app/javascript/controllers/skos_collection_colors_controller.js +++ b/app/javascript/controllers/skos_collection_colors_controller.js @@ -25,7 +25,7 @@ export default class extends Controller { let activeCollections = this.#getMatchedCollections(collectionElem, collections, this.selected) this.#removeColors(collectionElem) - this.#addColorsTags(collectionElem, this.#getCollectionColors(activeCollections)) + this.#addColorsTags(collectionElem, this.#getCollections(activeCollections)) } collectionTargetConnected (collectionElem) { @@ -48,7 +48,10 @@ export default class extends Controller { if (options) { Array.from(options.options).forEach(s => { if (s.value !== '') { - out[s.value] = s.dataset.color + out[s.value] = { + color: s.dataset.color, + title: s.textContent + } } }) } @@ -60,10 +63,12 @@ export default class extends Controller { return selected.filter(c => collections.includes(c)) } - #getCollectionColors (collectionsIds) { - return Object.entries(this.allCollections).filter(([key]) => collectionsIds.includes(key)).map(x => x[1]) + #getCollections(collectionsIds) { + return Object.entries(this.allCollections).filter(([key]) => collectionsIds.includes(key)) } + + #getElemCollections (elem) { return JSON.parse(elem.dataset.collectionsValue) } @@ -72,20 +77,23 @@ export default class extends Controller { return JSON.parse(elem.dataset.activeCollectionsValue) } - #addColorsTags (elem, colors) { + #addColorsTags (elem, collections) { const colorsContainer = document.createElement('span') - colors.forEach(c => this.#elemAddColorTag(colorsContainer, c)) + collections.forEach(c => this.#elemAddColorTag(colorsContainer, c[1].title, c[1].color)) elem.appendChild(colorsContainer) } - #elemAddColorTag (elem, color) { + #elemAddColorTag (elem, title, color) { const span = document.createElement('span') + span.dataset.controller = 'tooltip' + span.title = title span.style.backgroundColor = color span.style.height = '10px' span.style.width = '10px' span.style.borderRadius = '50%' span.style.display = 'inline-block' span.className += 'mx-1' + elem.appendChild(span) } } diff --git a/app/javascript/controllers/skos_collections_selector.js b/app/javascript/controllers/skos_collections_selector.js new file mode 100644 index 0000000000..f005e7b2d7 --- /dev/null +++ b/app/javascript/controllers/skos_collections_selector.js @@ -0,0 +1,49 @@ +import {Controller} from "@hotwired/stimulus" +import {useTomSelect} from "../mixins/useTomSelect"; + +// Connects to data-controller="skos-collection-selector" +export default class extends Controller { + // TODO to update to use TomSelect + static values = { + name: String, + enableColors: {type: Boolean, default: false} + } + + connect() { + this.select = useTomSelect(this.element, { + width: '100%', + plugins: ['remove_button'], + render: { + item: (data, escape) => { + if(this.enableColorsValue){ + const bgColor = data.color || '#ccc'; + return '
' + escape(data.text) + '
'; + }else { + return '
' + escape(data.text) + '
' + } + + } + } + }, + this.#onChange.bind(this)) + } + + #onChange(event) { + const selected = this.#getSelectedOptions() + const key = this.hasNameValue ? this.nameValue : event.currentTarget.name + const newData = { + [key]: selected.map(x => x.value) + } + this.element.dispatchEvent(new CustomEvent('changed', { + detail: { + data: newData + }, + bubbles: true, + cancelable: true, + })) + } + + #getSelectedOptions() { + return this.select.items.map(item => this.select.options[item]) + } +} diff --git a/app/javascript/mixins/useChosen.js b/app/javascript/mixins/useChosen.js deleted file mode 100644 index 5f2c6c5412..0000000000 --- a/app/javascript/mixins/useChosen.js +++ /dev/null @@ -1,8 +0,0 @@ -export function useChosen(element, params, onChange = null) { - const chosen = $(element).chosen(params) - if(onChange){ - chosen.change(onChange) - } - - return chosen; -} \ No newline at end of file diff --git a/app/views/ontologies/concepts_browsers/_collections_picker.html.haml b/app/views/ontologies/concepts_browsers/_collections_picker.html.haml index 1191df3ac7..638bc9758b 100644 --- a/app/views/ontologies/concepts_browsers/_collections_picker.html.haml +++ b/app/views/ontologies/concepts_browsers/_collections_picker.html.haml @@ -8,5 +8,5 @@ %label{:for => id} Collections %div = select_tag(:collections,options_for_select(collections_labels.map{|s| [s["prefLabel"].last, s["@id"], {'data-color': s['color']}]}, selected_collection.map{|x| x["@id"]}), - { multiple: multiple, id: id, class:"form-control", include_blank: true, - data:{controller:'chosen', 'chosen-name-value': :collectionid}.merge(data)}) \ No newline at end of file + { multiple: multiple, id: id, + data:{controller:'skos-collections-selector', 'skos-collections-selector-name-value': :collectionid}.merge(data)}) \ No newline at end of file diff --git a/app/views/ontologies/concepts_browsers/_concepts_tree.html.haml b/app/views/ontologies/concepts_browsers/_concepts_tree.html.haml index d4affe93f9..1160a7d29f 100644 --- a/app/views/ontologies/concepts_browsers/_concepts_tree.html.haml +++ b/app/views/ontologies/concepts_browsers/_concepts_tree.html.haml @@ -13,7 +13,7 @@ = render partial: 'ontologies/concepts_browsers/collections_picker', locals: {multiple: true, id: 'collection-color-filter', data: {action: 'changed->skos-collection-colors#updateCollectionTags' , - 'chosen-enable-colors-value': 'true'}} + 'skos-collection-colors-enable-colors-value': 'true'}} -# Class tree %div#sd_content.px-1{style: 'overflow-y: scroll; height: 60vh;'} - if skos? && @roots&.empty? diff --git a/app/views/ontologies/concepts_browsers/_scheme_picker.html.haml b/app/views/ontologies/concepts_browsers/_scheme_picker.html.haml index 958807977a..97046b62ca 100644 --- a/app/views/ontologies/concepts_browsers/_scheme_picker.html.haml +++ b/app/views/ontologies/concepts_browsers/_scheme_picker.html.haml @@ -12,6 +12,6 @@ = select_tag(:concept_schemes, options_for_select(schemes_labels.map{|s| [s["prefLabel"].last, s["@id"]]}, selected_scheme.compact.map{|x| x["@id"]} ), {multiple: true, id: "schemes_select", - data:{controller:'chosen', 'chosen-name-value': :concept_schemes, + data:{controller:'skos-collections-selector', 'skos-collections-selector-name-value': :concept_schemes, action: 'changed->history#updateURL changed->turbo-frame#updateFrame'}}) diff --git a/vendor/assets/images/chosen/chosen-sprite.png b/vendor/assets/images/chosen/chosen-sprite.png deleted file mode 100755 index c57da70b4b5b1e08a6977ddde182677af0e5e1b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 538 zcmV+#0_FXQP)cz2)-WJLkv8J@4bb5L`rsE?Kc|FrXHkKz)ov z76MHYM&Apx%05P7orE!>9=yZ~6O0^V?1%{=1UASqa<2Pgnk7fs!OIs9gh{NCN+@=) z>Gfttd5uq;oeR{%NHjtqV~jEQeY?tDff=(jqx>~SZ_e+iN26HR*`0Q!Re)~HD85p> zbL()Mw}bI^#`7wp0+cv&7*LhrtOmR)?PK>(-BeLm#jL5Jfogv-QS(TBnUb;))Krqm zD}uDDeVLNhm1G*pFB`O?iA=dnWBEpqHk8Yh%Qu45EIG=&F-dDmt|;|nN@|3lOkVZ7>z*~a1?_t?U)c+&|JFJke1`&0-a z#PjhRlg?=$KTo4|rU@NyV_fzDy@>h!lVyKShsO8>V>$xyIXRbHK%H~^Aaz=s$Jz^V zlb?KfaKdZqu3^#m$mintvgJ15@j`sb2Zr%69Sn=xN01Tm5r)NQanT=jhwm7zqj2>O cEB}D~0$b-QdD7|v=>Px#07*qoM6N<$g6AXnUH||9 diff --git a/vendor/assets/images/chosen/chosen-sprite@2x.png b/vendor/assets/images/chosen/chosen-sprite@2x.png deleted file mode 100755 index 6b50545202cb4770039362c55025b0b9824663ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 738 zcmV<80v-K{P)oJoIWh{eAG@xkM<0ryd(K3(} zP8JV&;uuIJ4nL%g8!wSG9E$P+3QVMGgj><+00}M5I5kMzaT<~M;uJ`UhLfbp9Ahdsrux5(g+(>Q*+9wU{AuYPH0}W_u4`|q(9c->{ zt>Jn|lbhH<_x5jU6prFi#S}&XMZ=~Y5VyC3+ZN%hXciz8 zPcLpJgbIK#a49e31-%wf2zh2F&&(Nq;AL%4zA(=QJRGq`sx3y3#0_cg9Fim739XTOu1NKKjlWs`52Q+3 Uja*K~(*OVf07*qoM6N<$g3mu-GXMYp diff --git a/vendor/assets/javascripts/chosen.jquery.js b/vendor/assets/javascripts/chosen.jquery.js deleted file mode 100755 index 060c162842..0000000000 --- a/vendor/assets/javascripts/chosen.jquery.js +++ /dev/null @@ -1,1269 +0,0 @@ -/*! -Chosen, a Select Box Enhancer for jQuery and Prototype -by Patrick Filler for Harvest, http://getharvest.com - -Version 1.6.2 -Full source at https://github.com/harvesthq/chosen -Copyright (c) 2011-2016 Harvest http://getharvest.com - -MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md -This file is generated by `grunt build`, do not edit it by hand. -*/ - -(function() { - var $, AbstractChosen, Chosen, SelectParser, _ref, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - - SelectParser = (function() { - function SelectParser() { - this.options_index = 0; - this.parsed = []; - } - - SelectParser.prototype.add_node = function(child) { - if (child.nodeName.toUpperCase() === "OPTGROUP") { - return this.add_group(child); - } else { - return this.add_option(child); - } - }; - - SelectParser.prototype.add_group = function(group) { - var group_position, option, _i, _len, _ref, _results; - group_position = this.parsed.length; - this.parsed.push({ - array_index: group_position, - group: true, - label: this.escapeExpression(group.label), - title: group.title ? group.title : void 0, - children: 0, - disabled: group.disabled, - classes: group.className - }); - _ref = group.childNodes; - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - option = _ref[_i]; - _results.push(this.add_option(option, group_position, group.disabled)); - } - return _results; - }; - - SelectParser.prototype.add_option = function(option, group_position, group_disabled) { - if (option.nodeName.toUpperCase() === "OPTION") { - if (option.text !== "") { - if (group_position != null) { - this.parsed[group_position].children += 1; - } - this.parsed.push({ - array_index: this.parsed.length, - options_index: this.options_index, - value: option.value, - text: option.text, - html: option.innerHTML, - title: option.title ? option.title : void 0, - selected: option.selected, - disabled: group_disabled === true ? group_disabled : option.disabled, - group_array_index: group_position, - group_label: group_position != null ? this.parsed[group_position].label : null, - classes: option.className, - style: option.style.cssText - }); - } else { - this.parsed.push({ - array_index: this.parsed.length, - options_index: this.options_index, - empty: true - }); - } - return this.options_index += 1; - } - }; - - SelectParser.prototype.escapeExpression = function(text) { - var map, unsafe_chars; - if ((text == null) || text === false) { - return ""; - } - if (!/[\&\<\>\"\'\`]/.test(text)) { - return text; - } - map = { - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; - unsafe_chars = /&(?!\w+;)|[\<\>\"\'\`]/g; - return text.replace(unsafe_chars, function(chr) { - return map[chr] || "&"; - }); - }; - - return SelectParser; - - })(); - - SelectParser.select_to_array = function(select) { - var child, parser, _i, _len, _ref; - parser = new SelectParser(); - _ref = select.childNodes; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - child = _ref[_i]; - parser.add_node(child); - } - return parser.parsed; - }; - - AbstractChosen = (function() { - function AbstractChosen(form_field, options) { - this.form_field = form_field; - this.options = options != null ? options : {}; - if (!AbstractChosen.browser_is_supported()) { - return; - } - this.is_multiple = this.form_field.multiple; - this.set_default_text(); - this.set_default_values(); - this.setup(); - this.set_up_html(); - this.register_observers(); - this.on_ready(); - } - - AbstractChosen.prototype.set_default_values = function() { - var _this = this; - this.click_test_action = function(evt) { - return _this.test_active_click(evt); - }; - this.activate_action = function(evt) { - return _this.activate_field(evt); - }; - this.active_field = false; - this.mouse_on_container = false; - this.results_showing = false; - this.result_highlighted = null; - this.allow_single_deselect = (this.options.allow_single_deselect != null) && (this.form_field.options[0] != null) && this.form_field.options[0].text === "" ? this.options.allow_single_deselect : false; - this.disable_search_threshold = this.options.disable_search_threshold || 0; - this.disable_search = this.options.disable_search || false; - this.enable_split_word_search = this.options.enable_split_word_search != null ? this.options.enable_split_word_search : true; - this.group_search = this.options.group_search != null ? this.options.group_search : true; - this.search_contains = this.options.search_contains || false; - this.single_backstroke_delete = this.options.single_backstroke_delete != null ? this.options.single_backstroke_delete : true; - this.max_selected_options = this.options.max_selected_options || Infinity; - this.inherit_select_classes = this.options.inherit_select_classes || false; - this.display_selected_options = this.options.display_selected_options != null ? this.options.display_selected_options : true; - this.display_disabled_options = this.options.display_disabled_options != null ? this.options.display_disabled_options : true; - this.include_group_label_in_selected = this.options.include_group_label_in_selected || false; - this.max_shown_results = this.options.max_shown_results || Number.POSITIVE_INFINITY; - return this.case_sensitive_search = this.options.case_sensitive_search || false; - }; - - AbstractChosen.prototype.set_default_text = function() { - if (this.form_field.getAttribute("data-placeholder")) { - this.default_text = this.form_field.getAttribute("data-placeholder"); - } else if (this.is_multiple) { - this.default_text = this.options.placeholder_text_multiple || this.options.placeholder_text || AbstractChosen.default_multiple_text; - } else { - this.default_text = this.options.placeholder_text_single || this.options.placeholder_text || AbstractChosen.default_single_text; - } - return this.results_none_found = this.form_field.getAttribute("data-no_results_text") || this.options.no_results_text || AbstractChosen.default_no_result_text; - }; - - AbstractChosen.prototype.choice_label = function(item) { - if (this.include_group_label_in_selected && (item.group_label != null)) { - return "" + item.group_label + "" + item.html; - } else { - return item.html; - } - }; - - AbstractChosen.prototype.mouse_enter = function() { - return this.mouse_on_container = true; - }; - - AbstractChosen.prototype.mouse_leave = function() { - return this.mouse_on_container = false; - }; - - AbstractChosen.prototype.input_focus = function(evt) { - var _this = this; - if (this.is_multiple) { - if (!this.active_field) { - return setTimeout((function() { - return _this.container_mousedown(); - }), 50); - } - } else { - if (!this.active_field) { - return this.activate_field(); - } - } - }; - - AbstractChosen.prototype.input_blur = function(evt) { - var _this = this; - if (!this.mouse_on_container) { - this.active_field = false; - return setTimeout((function() { - return _this.blur_test(); - }), 100); - } - }; - - AbstractChosen.prototype.results_option_build = function(options) { - var content, data, data_content, shown_results, _i, _len, _ref; - content = ''; - shown_results = 0; - _ref = this.results_data; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - data = _ref[_i]; - data_content = ''; - if (data.group) { - data_content = this.result_add_group(data); - } else { - data_content = this.result_add_option(data); - } - if (data_content !== '') { - shown_results++; - content += data_content; - } - if (options != null ? options.first : void 0) { - if (data.selected && this.is_multiple) { - this.choice_build(data); - } else if (data.selected && !this.is_multiple) { - this.single_set_selected_text(this.choice_label(data)); - } - } - if (shown_results >= this.max_shown_results) { - break; - } - } - return content; - }; - - AbstractChosen.prototype.result_add_option = function(option) { - var classes, option_el; - if (!option.search_match) { - return ''; - } - if (!this.include_option_in_results(option)) { - return ''; - } - classes = []; - if (!option.disabled && !(option.selected && this.is_multiple)) { - classes.push("active-result"); - } - if (option.disabled && !(option.selected && this.is_multiple)) { - classes.push("disabled-result"); - } - if (option.selected) { - classes.push("result-selected"); - } - if (option.group_array_index != null) { - classes.push("group-option"); - } - if (option.classes !== "") { - classes.push(option.classes); - } - option_el = document.createElement("li"); - option_el.className = classes.join(" "); - option_el.style.cssText = option.style; - option_el.setAttribute("data-option-array-index", option.array_index); - option_el.innerHTML = option.search_text; - if (option.title) { - option_el.title = option.title; - } - return this.outerHTML(option_el); - }; - - AbstractChosen.prototype.result_add_group = function(group) { - var classes, group_el; - if (!(group.search_match || group.group_match)) { - return ''; - } - if (!(group.active_options > 0)) { - return ''; - } - classes = []; - classes.push("group-result"); - if (group.classes) { - classes.push(group.classes); - } - group_el = document.createElement("li"); - group_el.className = classes.join(" "); - group_el.innerHTML = group.search_text; - if (group.title) { - group_el.title = group.title; - } - return this.outerHTML(group_el); - }; - - AbstractChosen.prototype.results_update_field = function() { - this.set_default_text(); - if (!this.is_multiple) { - this.results_reset_cleanup(); - } - this.result_clear_highlight(); - this.results_build(); - if (this.results_showing) { - return this.winnow_results(); - } - }; - - AbstractChosen.prototype.reset_single_select_options = function() { - var result, _i, _len, _ref, _results; - _ref = this.results_data; - _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - result = _ref[_i]; - if (result.selected) { - _results.push(result.selected = false); - } else { - _results.push(void 0); - } - } - return _results; - }; - - AbstractChosen.prototype.results_toggle = function() { - if (this.results_showing) { - return this.results_hide(); - } else { - return this.results_show(); - } - }; - - AbstractChosen.prototype.results_search = function(evt) { - if (this.results_showing) { - return this.winnow_results(); - } else { - return this.results_show(); - } - }; - - AbstractChosen.prototype.winnow_results = function() { - var escapedSearchText, option, regex, results, results_group, searchText, startpos, text, zregex, _i, _len, _ref; - this.no_results_clear(); - results = 0; - searchText = this.get_search_text(); - escapedSearchText = searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - zregex = new RegExp(escapedSearchText, 'i'); - regex = this.get_search_regex(escapedSearchText); - _ref = this.results_data; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - option = _ref[_i]; - option.search_match = false; - results_group = null; - if (this.include_option_in_results(option)) { - if (option.group) { - option.group_match = false; - option.active_options = 0; - } - if ((option.group_array_index != null) && this.results_data[option.group_array_index]) { - results_group = this.results_data[option.group_array_index]; - if (results_group.active_options === 0 && results_group.search_match) { - results += 1; - } - results_group.active_options += 1; - } - option.search_text = option.group ? option.label : option.html; - if (!(option.group && !this.group_search)) { - option.search_match = this.search_string_match(option.search_text, regex); - if (option.search_match && !option.group) { - results += 1; - } - if (option.search_match) { - if (searchText.length) { - startpos = option.search_text.search(zregex); - text = option.search_text.substr(0, startpos + searchText.length) + '' + option.search_text.substr(startpos + searchText.length); - option.search_text = text.substr(0, startpos) + '' + text.substr(startpos); - } - if (results_group != null) { - results_group.group_match = true; - } - } else if ((option.group_array_index != null) && this.results_data[option.group_array_index].search_match) { - option.search_match = true; - } - } - } - } - this.result_clear_highlight(); - if (results < 1 && searchText.length) { - this.update_results_content(""); - return this.no_results(searchText); - } else { - this.update_results_content(this.results_option_build()); - return this.winnow_results_set_highlight(); - } - }; - - AbstractChosen.prototype.get_search_regex = function(escaped_search_string) { - var regex_anchor, regex_flag; - regex_anchor = this.search_contains ? "" : "^"; - regex_flag = this.case_sensitive_search ? "" : "i"; - return new RegExp(regex_anchor + escaped_search_string, regex_flag); - }; - - AbstractChosen.prototype.search_string_match = function(search_string, regex) { - var part, parts, _i, _len; - if (regex.test(search_string)) { - return true; - } else if (this.enable_split_word_search && (search_string.indexOf(" ") >= 0 || search_string.indexOf("[") === 0)) { - parts = search_string.replace(/\[|\]/g, "").split(" "); - if (parts.length) { - for (_i = 0, _len = parts.length; _i < _len; _i++) { - part = parts[_i]; - if (regex.test(part)) { - return true; - } - } - } - } - }; - - AbstractChosen.prototype.choices_count = function() { - var option, _i, _len, _ref; - if (this.selected_option_count != null) { - return this.selected_option_count; - } - this.selected_option_count = 0; - _ref = this.form_field.options; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - option = _ref[_i]; - if (option.selected) { - this.selected_option_count += 1; - } - } - return this.selected_option_count; - }; - - AbstractChosen.prototype.choices_click = function(evt) { - evt.preventDefault(); - if (!(this.results_showing || this.is_disabled)) { - return this.results_show(); - } - }; - - AbstractChosen.prototype.keyup_checker = function(evt) { - var stroke, _ref; - stroke = (_ref = evt.which) != null ? _ref : evt.keyCode; - this.search_field_scale(); - switch (stroke) { - case 8: - if (this.is_multiple && this.backstroke_length < 1 && this.choices_count() > 0) { - return this.keydown_backstroke(); - } else if (!this.pending_backstroke) { - this.result_clear_highlight(); - return this.results_search(); - } - break; - case 13: - evt.preventDefault(); - if (this.results_showing) { - return this.result_select(evt); - } - break; - case 27: - if (this.results_showing) { - this.results_hide(); - } - return true; - case 9: - case 38: - case 40: - case 16: - case 91: - case 17: - case 18: - break; - default: - return this.results_search(); - } - }; - - AbstractChosen.prototype.clipboard_event_checker = function(evt) { - var _this = this; - return setTimeout((function() { - return _this.results_search(); - }), 50); - }; - - AbstractChosen.prototype.container_width = function() { - if (this.options.width != null) { - return this.options.width; - } else { - return "" + this.form_field.offsetWidth + "px"; - } - }; - - AbstractChosen.prototype.include_option_in_results = function(option) { - if (this.is_multiple && (!this.display_selected_options && option.selected)) { - return false; - } - if (!this.display_disabled_options && option.disabled) { - return false; - } - if (option.empty) { - return false; - } - return true; - }; - - AbstractChosen.prototype.search_results_touchstart = function(evt) { - this.touch_started = true; - return this.search_results_mouseover(evt); - }; - - AbstractChosen.prototype.search_results_touchmove = function(evt) { - this.touch_started = false; - return this.search_results_mouseout(evt); - }; - - AbstractChosen.prototype.search_results_touchend = function(evt) { - if (this.touch_started) { - return this.search_results_mouseup(evt); - } - }; - - AbstractChosen.prototype.outerHTML = function(element) { - var tmp; - if (element.outerHTML) { - return element.outerHTML; - } - tmp = document.createElement("div"); - tmp.appendChild(element); - return tmp.innerHTML; - }; - - AbstractChosen.browser_is_supported = function() { - if ("Microsoft Internet Explorer" === window.navigator.appName) { - return document.documentMode >= 8; - } - if (/iP(od|hone)/i.test(window.navigator.userAgent) || /IEMobile/i.test(window.navigator.userAgent) || /Windows Phone/i.test(window.navigator.userAgent) || /BlackBerry/i.test(window.navigator.userAgent) || /BB10/i.test(window.navigator.userAgent) || /Android.*Mobile/i.test(window.navigator.userAgent)) { - return false; - } - return true; - }; - - AbstractChosen.default_multiple_text = "Select Some Options"; - - AbstractChosen.default_single_text = "Select an Option"; - - AbstractChosen.default_no_result_text = "No results match"; - - return AbstractChosen; - - })(); - - $ = jQuery; - - $.fn.extend({ - chosen: function(options) { - if (!AbstractChosen.browser_is_supported()) { - return this; - } - return this.each(function(input_field) { - var $this, chosen; - $this = $(this); - chosen = $this.data('chosen'); - if (options === 'destroy') { - if (chosen instanceof Chosen) { - chosen.destroy(); - } - return; - } - if (!(chosen instanceof Chosen)) { - $this.data('chosen', new Chosen(this, options)); - } - }); - } - }); - - Chosen = (function(_super) { - __extends(Chosen, _super); - - function Chosen() { - _ref = Chosen.__super__.constructor.apply(this, arguments); - return _ref; - } - - Chosen.prototype.setup = function() { - this.form_field_jq = $(this.form_field); - this.current_selectedIndex = this.form_field.selectedIndex; - return this.is_rtl = this.form_field_jq.hasClass("chosen-rtl"); - }; - - Chosen.prototype.set_up_html = function() { - var container_classes, container_props; - container_classes = ["chosen-container"]; - container_classes.push("chosen-container-" + (this.is_multiple ? "multi" : "single")); - if (this.inherit_select_classes && this.form_field.className) { - container_classes.push(this.form_field.className); - } - if (this.is_rtl) { - container_classes.push("chosen-rtl"); - } - container_props = { - 'class': container_classes.join(' '), - 'style': "width: " + (this.container_width()) + ";", - 'title': this.form_field.title - }; - if (this.form_field.id.length) { - container_props.id = this.form_field.id.replace(/[^\w]/g, '_') + "_chosen"; - } - this.container = $("
", container_props); - if (this.is_multiple) { - this.container.html('
    '); - } else { - this.container.html('' + this.default_text + '
      '); - } - this.form_field_jq.hide().after(this.container); - this.dropdown = this.container.find('div.chosen-drop').first(); - this.search_field = this.container.find('input').first(); - this.search_results = this.container.find('ul.chosen-results').first(); - this.search_field_scale(); - this.search_no_results = this.container.find('li.no-results').first(); - if (this.is_multiple) { - this.search_choices = this.container.find('ul.chosen-choices').first(); - this.search_container = this.container.find('li.search-field').first(); - } else { - this.search_container = this.container.find('div.chosen-search').first(); - this.selected_item = this.container.find('.chosen-single').first(); - } - this.results_build(); - this.set_tab_index(); - return this.set_label_behavior(); - }; - - Chosen.prototype.on_ready = function() { - return this.form_field_jq.trigger("chosen:ready", { - chosen: this - }); - }; - - Chosen.prototype.register_observers = function() { - var _this = this; - this.container.bind('touchstart.chosen', function(evt) { - _this.container_mousedown(evt); - return evt.preventDefault(); - }); - this.container.bind('touchend.chosen', function(evt) { - _this.container_mouseup(evt); - return evt.preventDefault(); - }); - this.container.bind('mousedown.chosen', function(evt) { - _this.container_mousedown(evt); - }); - this.container.bind('mouseup.chosen', function(evt) { - _this.container_mouseup(evt); - }); - this.container.bind('mouseenter.chosen', function(evt) { - _this.mouse_enter(evt); - }); - this.container.bind('mouseleave.chosen', function(evt) { - _this.mouse_leave(evt); - }); - this.search_results.bind('mouseup.chosen', function(evt) { - _this.search_results_mouseup(evt); - }); - this.search_results.bind('mouseover.chosen', function(evt) { - _this.search_results_mouseover(evt); - }); - this.search_results.bind('mouseout.chosen', function(evt) { - _this.search_results_mouseout(evt); - }); - this.search_results.bind('mousewheel.chosen DOMMouseScroll.chosen', function(evt) { - _this.search_results_mousewheel(evt); - }); - this.search_results.bind('touchstart.chosen', function(evt) { - _this.search_results_touchstart(evt); - }); - this.search_results.bind('touchmove.chosen', function(evt) { - _this.search_results_touchmove(evt); - }); - this.search_results.bind('touchend.chosen', function(evt) { - _this.search_results_touchend(evt); - }); - this.form_field_jq.bind("chosen:updated.chosen", function(evt) { - _this.results_update_field(evt); - }); - this.form_field_jq.bind("chosen:activate.chosen", function(evt) { - _this.activate_field(evt); - }); - this.form_field_jq.bind("chosen:open.chosen", function(evt) { - _this.container_mousedown(evt); - }); - this.form_field_jq.bind("chosen:close.chosen", function(evt) { - _this.input_blur(evt); - }); - this.search_field.bind('blur.chosen', function(evt) { - _this.input_blur(evt); - }); - this.search_field.bind('keyup.chosen', function(evt) { - _this.keyup_checker(evt); - }); - this.search_field.bind('keydown.chosen', function(evt) { - _this.keydown_checker(evt); - }); - this.search_field.bind('focus.chosen', function(evt) { - _this.input_focus(evt); - }); - this.search_field.bind('cut.chosen', function(evt) { - _this.clipboard_event_checker(evt); - }); - this.search_field.bind('paste.chosen', function(evt) { - _this.clipboard_event_checker(evt); - }); - if (this.is_multiple) { - return this.search_choices.bind('click.chosen', function(evt) { - _this.choices_click(evt); - }); - } else { - return this.container.bind('click.chosen', function(evt) { - evt.preventDefault(); - }); - } - }; - - Chosen.prototype.destroy = function() { - $(this.container[0].ownerDocument).unbind("click.chosen", this.click_test_action); - if (this.search_field[0].tabIndex) { - this.form_field_jq[0].tabIndex = this.search_field[0].tabIndex; - } - this.container.remove(); - this.form_field_jq.removeData('chosen'); - return this.form_field_jq.show(); - }; - - Chosen.prototype.search_field_disabled = function() { - this.is_disabled = this.form_field_jq[0].disabled; - if (this.is_disabled) { - this.container.addClass('chosen-disabled'); - this.search_field[0].disabled = true; - if (!this.is_multiple) { - this.selected_item.unbind("focus.chosen", this.activate_action); - } - return this.close_field(); - } else { - this.container.removeClass('chosen-disabled'); - this.search_field[0].disabled = false; - if (!this.is_multiple) { - return this.selected_item.bind("focus.chosen", this.activate_action); - } - } - }; - - Chosen.prototype.container_mousedown = function(evt) { - if (!this.is_disabled) { - if (evt && evt.type === "mousedown" && !this.results_showing) { - evt.preventDefault(); - } - if (!((evt != null) && ($(evt.target)).hasClass("search-choice-close"))) { - if (!this.active_field) { - if (this.is_multiple) { - this.search_field.val(""); - } - $(this.container[0].ownerDocument).bind('click.chosen', this.click_test_action); - this.results_show(); - } else if (!this.is_multiple && evt && (($(evt.target)[0] === this.selected_item[0]) || $(evt.target).parents("a.chosen-single").length)) { - evt.preventDefault(); - this.results_toggle(); - } - return this.activate_field(); - } - } - }; - - Chosen.prototype.container_mouseup = function(evt) { - if (evt.target.nodeName === "ABBR" && !this.is_disabled) { - return this.results_reset(evt); - } - }; - - Chosen.prototype.search_results_mousewheel = function(evt) { - var delta; - if (evt.originalEvent) { - delta = evt.originalEvent.deltaY || -evt.originalEvent.wheelDelta || evt.originalEvent.detail; - } - if (delta != null) { - evt.preventDefault(); - if (evt.type === 'DOMMouseScroll') { - delta = delta * 40; - } - return this.search_results.scrollTop(delta + this.search_results.scrollTop()); - } - }; - - Chosen.prototype.blur_test = function(evt) { - if (!this.active_field && this.container.hasClass("chosen-container-active")) { - return this.close_field(); - } - }; - - Chosen.prototype.close_field = function() { - $(this.container[0].ownerDocument).unbind("click.chosen", this.click_test_action); - this.active_field = false; - this.results_hide(); - this.container.removeClass("chosen-container-active"); - this.clear_backstroke(); - this.show_search_field_default(); - return this.search_field_scale(); - }; - - Chosen.prototype.activate_field = function() { - this.container.addClass("chosen-container-active"); - this.active_field = true; - this.search_field.val(this.search_field.val()); - return this.search_field.focus(); - }; - - Chosen.prototype.test_active_click = function(evt) { - var active_container; - active_container = $(evt.target).closest('.chosen-container'); - if (active_container.length && this.container[0] === active_container[0]) { - return this.active_field = true; - } else { - return this.close_field(); - } - }; - - Chosen.prototype.results_build = function() { - this.parsing = true; - this.selected_option_count = null; - this.results_data = SelectParser.select_to_array(this.form_field); - if (this.is_multiple) { - this.search_choices.find("li.search-choice").remove(); - } else if (!this.is_multiple) { - this.single_set_selected_text(); - if (this.disable_search || this.form_field.options.length <= this.disable_search_threshold) { - this.search_field[0].readOnly = true; - this.container.addClass("chosen-container-single-nosearch"); - } else { - this.search_field[0].readOnly = false; - this.container.removeClass("chosen-container-single-nosearch"); - } - } - this.update_results_content(this.results_option_build({ - first: true - })); - this.search_field_disabled(); - this.show_search_field_default(); - this.search_field_scale(); - return this.parsing = false; - }; - - Chosen.prototype.result_do_highlight = function(el) { - var high_bottom, high_top, maxHeight, visible_bottom, visible_top; - if (el.length) { - this.result_clear_highlight(); - this.result_highlight = el; - this.result_highlight.addClass("highlighted"); - maxHeight = parseInt(this.search_results.css("maxHeight"), 10); - visible_top = this.search_results.scrollTop(); - visible_bottom = maxHeight + visible_top; - high_top = this.result_highlight.position().top + this.search_results.scrollTop(); - high_bottom = high_top + this.result_highlight.outerHeight(); - if (high_bottom >= visible_bottom) { - return this.search_results.scrollTop((high_bottom - maxHeight) > 0 ? high_bottom - maxHeight : 0); - } else if (high_top < visible_top) { - return this.search_results.scrollTop(high_top); - } - } - }; - - Chosen.prototype.result_clear_highlight = function() { - if (this.result_highlight) { - this.result_highlight.removeClass("highlighted"); - } - return this.result_highlight = null; - }; - - Chosen.prototype.results_show = function() { - if (this.is_multiple && this.max_selected_options <= this.choices_count()) { - this.form_field_jq.trigger("chosen:maxselected", { - chosen: this - }); - return false; - } - this.container.addClass("chosen-with-drop"); - this.results_showing = true; - this.search_field.focus(); - this.search_field.val(this.search_field.val()); - this.winnow_results(); - return this.form_field_jq.trigger("chosen:showing_dropdown", { - chosen: this - }); - }; - - Chosen.prototype.update_results_content = function(content) { - return this.search_results.html(content); - }; - - Chosen.prototype.results_hide = function() { - if (this.results_showing) { - this.result_clear_highlight(); - this.container.removeClass("chosen-with-drop"); - this.form_field_jq.trigger("chosen:hiding_dropdown", { - chosen: this - }); - } - return this.results_showing = false; - }; - - Chosen.prototype.set_tab_index = function(el) { - var ti; - if (this.form_field.tabIndex) { - ti = this.form_field.tabIndex; - this.form_field.tabIndex = -1; - return this.search_field[0].tabIndex = ti; - } - }; - - Chosen.prototype.set_label_behavior = function() { - var _this = this; - this.form_field_label = this.form_field_jq.parents("label"); - if (!this.form_field_label.length && this.form_field.id.length) { - this.form_field_label = $("label[for='" + this.form_field.id + "']"); - } - if (this.form_field_label.length > 0) { - return this.form_field_label.bind('click.chosen', function(evt) { - if (_this.is_multiple) { - return _this.container_mousedown(evt); - } else { - return _this.activate_field(); - } - }); - } - }; - - Chosen.prototype.show_search_field_default = function() { - if (this.is_multiple && this.choices_count() < 1 && !this.active_field) { - this.search_field.val(this.default_text); - return this.search_field.addClass("default"); - } else { - this.search_field.val(""); - return this.search_field.removeClass("default"); - } - }; - - Chosen.prototype.search_results_mouseup = function(evt) { - var target; - target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first(); - if (target.length) { - this.result_highlight = target; - this.result_select(evt); - return this.search_field.focus(); - } - }; - - Chosen.prototype.search_results_mouseover = function(evt) { - var target; - target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first(); - if (target) { - return this.result_do_highlight(target); - } - }; - - Chosen.prototype.search_results_mouseout = function(evt) { - if ($(evt.target).hasClass("active-result" || $(evt.target).parents('.active-result').first())) { - return this.result_clear_highlight(); - } - }; - - Chosen.prototype.choice_build = function(item) { - var choice, close_link, - _this = this; - choice = $('
    • ', { - "class": "search-choice" - }).html("" + (this.choice_label(item)) + ""); - if (item.disabled) { - choice.addClass('search-choice-disabled'); - } else { - close_link = $('', { - "class": 'search-choice-close', - 'data-option-array-index': item.array_index - }); - close_link.bind('click.chosen', function(evt) { - return _this.choice_destroy_link_click(evt); - }); - choice.append(close_link); - } - return this.search_container.before(choice); - }; - - Chosen.prototype.choice_destroy_link_click = function(evt) { - evt.preventDefault(); - evt.stopPropagation(); - if (!this.is_disabled) { - return this.choice_destroy($(evt.target)); - } - }; - - Chosen.prototype.choice_destroy = function(link) { - if (this.result_deselect(link[0].getAttribute("data-option-array-index"))) { - this.show_search_field_default(); - if (this.is_multiple && this.choices_count() > 0 && this.search_field.val().length < 1) { - this.results_hide(); - } - link.parents('li').first().remove(); - return this.search_field_scale(); - } - }; - - Chosen.prototype.results_reset = function() { - this.reset_single_select_options(); - this.form_field.options[0].selected = true; - this.single_set_selected_text(); - this.show_search_field_default(); - this.results_reset_cleanup(); - this.form_field_jq.trigger("change"); - if (this.active_field) { - return this.results_hide(); - } - }; - - Chosen.prototype.results_reset_cleanup = function() { - this.current_selectedIndex = this.form_field.selectedIndex; - return this.selected_item.find("abbr").remove(); - }; - - Chosen.prototype.result_select = function(evt) { - var high, item; - if (this.result_highlight) { - high = this.result_highlight; - this.result_clear_highlight(); - if (this.is_multiple && this.max_selected_options <= this.choices_count()) { - this.form_field_jq.trigger("chosen:maxselected", { - chosen: this - }); - return false; - } - if (this.is_multiple) { - high.removeClass("active-result"); - } else { - this.reset_single_select_options(); - } - high.addClass("result-selected"); - item = this.results_data[high[0].getAttribute("data-option-array-index")]; - item.selected = true; - this.form_field.options[item.options_index].selected = true; - this.selected_option_count = null; - if (this.is_multiple) { - this.choice_build(item); - } else { - this.single_set_selected_text(this.choice_label(item)); - } - if (!((evt.metaKey || evt.ctrlKey) && this.is_multiple)) { - this.results_hide(); - } - this.show_search_field_default(); - if (this.is_multiple || this.form_field.selectedIndex !== this.current_selectedIndex) { - this.form_field_jq.trigger("change", { - 'selected': this.form_field.options[item.options_index].value - }); - } - this.current_selectedIndex = this.form_field.selectedIndex; - evt.preventDefault(); - return this.search_field_scale(); - } - }; - - Chosen.prototype.single_set_selected_text = function(text) { - if (text == null) { - text = this.default_text; - } - if (text === this.default_text) { - this.selected_item.addClass("chosen-default"); - } else { - this.single_deselect_control_build(); - this.selected_item.removeClass("chosen-default"); - } - return this.selected_item.find("span").html(text); - }; - - Chosen.prototype.result_deselect = function(pos) { - var result_data; - result_data = this.results_data[pos]; - if (!this.form_field.options[result_data.options_index].disabled) { - result_data.selected = false; - this.form_field.options[result_data.options_index].selected = false; - this.selected_option_count = null; - this.result_clear_highlight(); - if (this.results_showing) { - this.winnow_results(); - } - this.form_field_jq.trigger("change", { - deselected: this.form_field.options[result_data.options_index].value - }); - this.search_field_scale(); - return true; - } else { - return false; - } - }; - - Chosen.prototype.single_deselect_control_build = function() { - if (!this.allow_single_deselect) { - return; - } - if (!this.selected_item.find("abbr").length) { - this.selected_item.find("span").first().after(""); - } - return this.selected_item.addClass("chosen-single-with-deselect"); - }; - - Chosen.prototype.get_search_text = function() { - return $('
      ').text($.trim(this.search_field.val())).html(); - }; - - Chosen.prototype.winnow_results_set_highlight = function() { - var do_high, selected_results; - selected_results = !this.is_multiple ? this.search_results.find(".result-selected.active-result") : []; - do_high = selected_results.length ? selected_results.first() : this.search_results.find(".active-result").first(); - if (do_high != null) { - return this.result_do_highlight(do_high); - } - }; - - Chosen.prototype.no_results = function(terms) { - var no_results_html; - no_results_html = $('
    • ' + this.results_none_found + ' ""
    • '); - no_results_html.find("span").first().html(terms); - this.search_results.append(no_results_html); - return this.form_field_jq.trigger("chosen:no_results", { - chosen: this - }); - }; - - Chosen.prototype.no_results_clear = function() { - return this.search_results.find(".no-results").remove(); - }; - - Chosen.prototype.keydown_arrow = function() { - var next_sib; - if (this.results_showing && this.result_highlight) { - next_sib = this.result_highlight.nextAll("li.active-result").first(); - if (next_sib) { - return this.result_do_highlight(next_sib); - } - } else { - return this.results_show(); - } - }; - - Chosen.prototype.keyup_arrow = function() { - var prev_sibs; - if (!this.results_showing && !this.is_multiple) { - return this.results_show(); - } else if (this.result_highlight) { - prev_sibs = this.result_highlight.prevAll("li.active-result"); - if (prev_sibs.length) { - return this.result_do_highlight(prev_sibs.first()); - } else { - if (this.choices_count() > 0) { - this.results_hide(); - } - return this.result_clear_highlight(); - } - } - }; - - Chosen.prototype.keydown_backstroke = function() { - var next_available_destroy; - if (this.pending_backstroke) { - this.choice_destroy(this.pending_backstroke.find("a").first()); - return this.clear_backstroke(); - } else { - next_available_destroy = this.search_container.siblings("li.search-choice").last(); - if (next_available_destroy.length && !next_available_destroy.hasClass("search-choice-disabled")) { - this.pending_backstroke = next_available_destroy; - if (this.single_backstroke_delete) { - return this.keydown_backstroke(); - } else { - return this.pending_backstroke.addClass("search-choice-focus"); - } - } - } - }; - - Chosen.prototype.clear_backstroke = function() { - if (this.pending_backstroke) { - this.pending_backstroke.removeClass("search-choice-focus"); - } - return this.pending_backstroke = null; - }; - - Chosen.prototype.keydown_checker = function(evt) { - var stroke, _ref1; - stroke = (_ref1 = evt.which) != null ? _ref1 : evt.keyCode; - this.search_field_scale(); - if (stroke !== 8 && this.pending_backstroke) { - this.clear_backstroke(); - } - switch (stroke) { - case 8: - this.backstroke_length = this.search_field.val().length; - break; - case 9: - if (this.results_showing && !this.is_multiple) { - this.result_select(evt); - } - this.mouse_on_container = false; - break; - case 13: - if (this.results_showing) { - evt.preventDefault(); - } - break; - case 32: - if (this.disable_search) { - evt.preventDefault(); - } - break; - case 38: - evt.preventDefault(); - this.keyup_arrow(); - break; - case 40: - evt.preventDefault(); - this.keydown_arrow(); - break; - } - }; - - Chosen.prototype.search_field_scale = function() { - var div, f_width, h, style, style_block, styles, w, _i, _len; - if (this.is_multiple) { - h = 0; - w = 0; - style_block = "position:absolute; left: -1000px; top: -1000px; display:none;"; - styles = ['font-size', 'font-style', 'font-weight', 'font-family', 'line-height', 'text-transform', 'letter-spacing']; - for (_i = 0, _len = styles.length; _i < _len; _i++) { - style = styles[_i]; - style_block += style + ":" + this.search_field.css(style) + ";"; - } - div = $('
      ', { - 'style': style_block - }); - div.text(this.search_field.val()); - $('body').append(div); - w = div.width() + 25; - div.remove(); - f_width = this.container.outerWidth(); - if (w > f_width - 10) { - w = f_width - 10; - } - return this.search_field.css({ - 'width': w + 'px' - }); - } - }; - - return Chosen; - - })(AbstractChosen); - -}).call(this); diff --git a/vendor/assets/stylesheets/chosen.css.scss b/vendor/assets/stylesheets/chosen.css.scss deleted file mode 100755 index 5627b3578b..0000000000 --- a/vendor/assets/stylesheets/chosen.css.scss +++ /dev/null @@ -1,452 +0,0 @@ -/*! -Chosen, a Select Box Enhancer for jQuery and Prototype -by Patrick Filler for Harvest, http://getharvest.com - -Version 1.6.2 -Full source at https://github.com/harvesthq/chosen -Copyright (c) 2011-2016 Harvest http://getharvest.com - -MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md -This file is generated by `grunt build`, do not edit it by hand. -*/ - -/* @group Base */ -.chosen-container { - position: relative; - display: inline-block; - vertical-align: middle; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - width: 100%; - /* height: calc(2.25rem + 2px); */ - padding: 0.375rem 0.75rem; - font-size: 1rem; - line-height: 1.5; - color: #495057; - background-color: #fff; - background-clip: padding-box; - border: 1px solid #ced4da; - border-radius: 0.25rem; - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -.chosen-container * { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -.chosen-container .chosen-drop { - position: absolute; - top: 100%; - left: -9999px; - z-index: 1010; - width: 100%; - border: 1px solid #aaa; - border-top: 0; - background: #fff; - box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); -} -.chosen-container.chosen-with-drop .chosen-drop { - left: 0; -} -.chosen-container a { - cursor: pointer; -} -.chosen-container .search-choice .group-name, .chosen-container .chosen-single .group-name { - margin-right: 4px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - font-weight: normal; - color: #999999; -} -.chosen-container .search-choice .group-name:after, .chosen-container .chosen-single .group-name:after { - content: ":"; - padding-left: 2px; - vertical-align: top; -} - -/* @end */ -/* @group Single Chosen */ -.chosen-container-single .chosen-single { - position: relative; - display: block; - overflow: hidden; - padding: 0 0 0 8px; - height: 25px; - border: 1px solid #aaa; - border-radius: 5px; - background-color: #fff; - background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4)); - background: -webkit-linear-gradient(#ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); - background: -moz-linear-gradient(#ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); - background: -o-linear-gradient(#ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); - background: linear-gradient(#ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); - background-clip: padding-box; - box-shadow: 0 0 3px white inset, 0 1px 1px rgba(0, 0, 0, 0.1); - color: #444; - text-decoration: none; - white-space: nowrap; - line-height: 24px; -} -.chosen-container-single .chosen-default { - color: #999; -} -.chosen-container-single .chosen-single span { - display: block; - overflow: hidden; - margin-right: 26px; - text-overflow: ellipsis; - white-space: nowrap; -} -.chosen-container-single .chosen-single-with-deselect span { - margin-right: 38px; -} -.chosen-container-single .chosen-single abbr { - position: absolute; - top: 6px; - right: 26px; - display: block; - width: 12px; - height: 12px; - background: image-url('chosen/chosen-sprite.png') -42px 1px no-repeat; - font-size: 1px; -} -.chosen-container-single .chosen-single abbr:hover { - background-position: -42px -10px; -} -.chosen-container-single.chosen-disabled .chosen-single abbr:hover { - background-position: -42px -10px; -} -.chosen-container-single .chosen-single div { - position: absolute; - top: 0; - right: 0; - display: block; - width: 18px; - height: 100%; -} -.chosen-container-single .chosen-single div b { - display: block; - width: 100%; - height: 100%; - background: image-url('chosen/chosen-sprite.png') no-repeat 0px 2px; -} -.chosen-container-single .chosen-search { - position: relative; - z-index: 1010; - margin: 0; - padding: 3px 4px; - white-space: nowrap; -} -.chosen-container-single .chosen-search input[type="text"] { - margin: 1px 0; - padding: 4px 20px 4px 5px; - width: 100%; - height: auto; - outline: 0; - border: 1px solid #aaa; - background: white image-url('chosen/chosen-sprite.png') no-repeat 100% -20px; - background: image-url('chosen/chosen-sprite.png') no-repeat 100% -20px; - font-size: 1em; - font-family: sans-serif; - line-height: normal; - border-radius: 0; -} -.chosen-container-single .chosen-drop { - margin-top: -1px; - border-radius: 0 0 4px 4px; - background-clip: padding-box; -} -.chosen-container-single.chosen-container-single-nosearch .chosen-search { - position: absolute; - left: -9999px; -} - -/* @end */ -/* @group Results */ -.chosen-container .chosen-results { - color: #444; - position: relative; - overflow-x: hidden; - overflow-y: auto; - margin: 0 4px 4px 0; - padding: 0 0 0 4px; - max-height: 240px; - -webkit-overflow-scrolling: touch; -} -.chosen-container .chosen-results li { - display: none; - margin: 0; - padding: 5px 6px; - list-style: none; - line-height: 15px; - word-wrap: break-word; - -webkit-touch-callout: none; -} -.chosen-container .chosen-results li.active-result { - display: list-item; - cursor: pointer; -} -.chosen-container .chosen-results li.disabled-result { - display: list-item; - color: #ccc; - cursor: default; -} -.chosen-container .chosen-results li.highlighted { - background-color: #3875d7; - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc)); - background-image: -webkit-linear-gradient(#3875d7 20%, #2a62bc 90%); - background-image: -moz-linear-gradient(#3875d7 20%, #2a62bc 90%); - background-image: -o-linear-gradient(#3875d7 20%, #2a62bc 90%); - background-image: linear-gradient(#3875d7 20%, #2a62bc 90%); - color: #fff; -} -.chosen-container .chosen-results li.no-results { - color: #777; - display: list-item; - background: #f4f4f4; -} -.chosen-container .chosen-results li.group-result { - display: list-item; - font-weight: bold; - cursor: default; -} -.chosen-container .chosen-results li.group-option { - padding-left: 15px; -} -.chosen-container .chosen-results li em { - font-style: normal; - text-decoration: underline; -} - -/* @end */ -/* @group Multi Chosen */ -.chosen-container-multi .chosen-choices { - position: relative; - overflow: hidden; - margin: 0; - padding: 0 5px; - width: 100%; - height: auto; - cursor: text; - -} -.chosen-container-multi .chosen-choices li { - float: left; - list-style: none; -} -.chosen-container-multi .chosen-choices li.search-field { - margin: 0; - padding: 0; - white-space: nowrap; - display: contents; -} -.chosen-container-multi .chosen-choices li.search-field input[type="text"] { - margin: 1px 0; - padding: 0; - height: 25px; - outline: 0; - border: 0 !important; - background: transparent !important; - box-shadow: none; - color: #999; - font-size: 100%; - font-family: sans-serif; - line-height: normal; - border-radius: 0; -} -.chosen-container-multi .chosen-choices li.search-choice { - position: relative; - margin: 3px 5px 3px 0; - padding: 3px 20px 3px 5px; - border: 1px solid #aaa; - max-width: 100%; - border-radius: 3px; - background-color: #eeeeee; - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee)); - background-image: -webkit-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); - background-image: -moz-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); - background-image: -o-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); - background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); - background-size: 100% 19px; - background-repeat: repeat-x; - background-clip: padding-box; - box-shadow: 0 0 2px white inset, 0 1px 0 rgba(0, 0, 0, 0.05); - color: #333; - line-height: 13px; - cursor: default; -} -.chosen-container-multi .chosen-choices li.search-choice span { - word-wrap: break-word; -} -.chosen-container-multi .chosen-choices li.search-choice .search-choice-close { - position: absolute; - top: 4px; - right: 3px; - display: block; - width: 12px; - height: 12px; - background: image-url('chosen/chosen-sprite.png') -42px 1px no-repeat; - font-size: 1px; -} -.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover { - background-position: -42px -10px; -} -.chosen-container-multi .chosen-choices li.search-choice-disabled { - padding-right: 5px; - border: 1px solid #ccc; - background-color: #e4e4e4; - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee)); - background-image: -webkit-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); - background-image: -moz-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); - background-image: -o-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); - background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); - color: #666; -} -.chosen-container-multi .chosen-choices li.search-choice-focus { - background: #d4d4d4; -} -.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close { - background-position: -42px -10px; -} -.chosen-container-multi .chosen-results { - margin: 0; - padding: 0; -} -.chosen-container-multi .chosen-drop .result-selected { - display: list-item; - color: #ccc; - cursor: default; -} - -/* @end */ -/* @group Active */ -.chosen-container-active .chosen-single { - border: 1px solid #5897fb; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); -} -.chosen-container-active.chosen-with-drop .chosen-single { - border: 1px solid #aaa; - -moz-border-radius-bottomright: 0; - border-bottom-right-radius: 0; - -moz-border-radius-bottomleft: 0; - border-bottom-left-radius: 0; - background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #eeeeee), color-stop(80%, #ffffff)); - background-image: -webkit-linear-gradient(#eeeeee 20%, #ffffff 80%); - background-image: -moz-linear-gradient(#eeeeee 20%, #ffffff 80%); - background-image: -o-linear-gradient(#eeeeee 20%, #ffffff 80%); - background-image: linear-gradient(#eeeeee 20%, #ffffff 80%); - box-shadow: 0 1px 0 #fff inset; -} -.chosen-container-active.chosen-with-drop .chosen-single div { - border-left: none; - background: transparent; -} -.chosen-container-active.chosen-with-drop .chosen-single div b { - background-position: -18px 2px; -} -.chosen-container-active .chosen-choices { - border: 1px solid #5897fb; - box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); -} -.chosen-container-active .chosen-choices li.search-field input[type="text"] { - color: #222 !important; -} - -/* @end */ -/* @group Disabled Support */ -.chosen-disabled { - opacity: 0.5 !important; - cursor: default; -} -.chosen-disabled .chosen-single { - cursor: default; -} -.chosen-disabled .chosen-choices .search-choice .search-choice-close { - cursor: default; -} - -/* @end */ -/* @group Right to Left */ -.chosen-rtl { - text-align: right; -} -.chosen-rtl .chosen-single { - overflow: visible; - padding: 0 8px 0 0; -} -.chosen-rtl .chosen-single span { - margin-right: 0; - margin-left: 26px; - direction: rtl; -} -.chosen-rtl .chosen-single-with-deselect span { - margin-left: 38px; -} -.chosen-rtl .chosen-single div { - right: auto; - left: 3px; -} -.chosen-rtl .chosen-single abbr { - right: auto; - left: 26px; -} -.chosen-rtl .chosen-choices li { - float: right; -} -.chosen-rtl .chosen-choices li.search-field input[type="text"] { - direction: rtl; -} -.chosen-rtl .chosen-choices li.search-choice { - margin: 3px 5px 3px 0; - padding: 3px 5px 3px 19px; -} -.chosen-rtl .chosen-choices li.search-choice .search-choice-close { - right: auto; - left: 4px; -} -.chosen-rtl.chosen-container-single-nosearch .chosen-search, -.chosen-rtl .chosen-drop { - left: 9999px; -} -.chosen-rtl.chosen-container-single .chosen-results { - margin: 0 0 4px 4px; - padding: 0 4px 0 0; -} -.chosen-rtl .chosen-results li.group-option { - padding-right: 15px; - padding-left: 0; -} -.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div { - border-right: none; -} -.chosen-rtl .chosen-search input[type="text"] { - padding: 4px 5px 4px 20px; - background: white image-url('chosen/chosen-sprite.png') no-repeat -30px -20px; - background: image-url('chosen/chosen-sprite.png') no-repeat -30px -20px; - direction: rtl; -} -.chosen-rtl.chosen-container-single .chosen-single div b { - background-position: 6px 2px; -} -.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b { - background-position: -12px 2px; -} - -/* @end */ -/* @group Retina compatibility */ -@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi), only screen and (min-resolution: 1.5dppx) { - .chosen-rtl .chosen-search input[type="text"], - .chosen-container-single .chosen-single abbr, - .chosen-container-single .chosen-single div b, - .chosen-container-single .chosen-search input[type="text"], - .chosen-container-multi .chosen-choices .search-choice .search-choice-close, - .chosen-container .chosen-results-scroll-down span, - .chosen-container .chosen-results-scroll-up span { - background-image: image-url('chosen/chosen-sprite@2x.png') !important; - background-size: 52px 37px !important; - background-repeat: no-repeat !important; - } -} -/* @end */ From 76479295444d05ea952758c111804fecc72175e2 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Mon, 26 Aug 2024 11:01:50 +0200 Subject: [PATCH 03/45] Fix: losing context when changing language in concepts page (#642) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: home annotator block redirection to annotator page with empty text (#629) * Fix: Download button in summary export metadata (#628) * fix xml download button in summary page, export metadata * adjust the position of the export metadata download button and make the hover cursor pointer for it * Fix: the contexual json icon for to the concept adapt to the select tab (#539) * refactor concepts json code and put it in a stimulus controller * adjust the position of the concepts json link * remove non related code to concepts json PR * remove non related code to contextual json pull request * pass base class URL directly to concepts json stimulus controller * clean concepts json stimulus code * remove undesired code from concepts json button PR * fix typo in agent_search_input_component.html.haml * rename concepts json button stimulus controller * clean concepts json button controller code * clean concepts json button related code --------- Co-authored-by: Syphax bouazzouni Co-authored-by: Bilel KIHAL Co-authored-by: @SirineMhedhbi * feature: make view of clickable in summary page header (#643) * feature: use the ontologies selector by name and acronym in edit groups and categories (#641) * Fix: Reject the current ontology from appearing in the ontologies selector in editing the submission's relations (#640) * reject current ontology from the ontologies selector in relations section - edit submission * pass reject ontology as a param in generate ontology seect input method * Feature: Make text area resisable in upload ontology and edit submission forms (#644) * make text areas resisable in upload ontology and edit submission forms * make forms list input text areas resisable * Fix: The issue when the agent has no acronym (#645) * fix the issue when the agent has no acronym * clean agent affiliations combination * fix: the concepts chips links redirecting to the wrong destination (#638) * Feature: Scroll to the selected term in the tree view component (#646) * scroll to the selected term in the tree view * prevent the whole page scrolling when centring the scroll to the selected tree view element * fix: change local test "URL" to "URI" in check resolvability components (#651) * Feature: Add content negotiation redirection tests (#622) * Add uri redirection for /ontologies/:acronym/:id to the appropriate page * small fixes: remove binding.pry and return resource_id in find_type_by_search * Add copy agroportal link functionality - this functionality is using the ClipboardComponent because it works the same but differ only in the content and icon - the clipboard component has been changed to accept title and icon * redirect to content finder page when no type is valid * Updated route to use redirect action instead of show_redirection * Use default icon and title arguments in the Clipboard Component * rename and internationalize clipboard component titles * small fix: remove % from svg icon * Add the copy title to the components section in en.yml and fr.yml * Show the generated uri when user hover over the copy link icon * Make the dynamic route content negotiable - based on the accept header we will - if "text/html" we will redirect to the agroportal page - else we will call the api and get the content serialized in the format specified and return it * update serialize content to return accept_header * translate copy_original_uri and copy_portal_uri to french * reuse search content concern in uri redirection concern * remove duplicate link_last_part method * Add htaccess functionality * Add apache and nginx instruction for htaccess redirection * Clean te code in the view file * Add OntologiesRedirectionController - remove redirect and generate_htaccess from ontologies_controller - clean code the generate_htaccess method - add "ontology_portal_uri" in @identifiers * Regroupe routes * Make ontologies_redirection controller and refactor generate_htaccess code * Internationalization of redirection rewrite rules modal * Add note for url that has # * link contact support button to the feedback page * Change regex to redirect only url of type: /path/resource_id - for urls that has the # it will redirect to the ontology page * Add ontology redirection based on the accept header format * move the ontology redirection route to the bottom to have less priority * Change rewrite rules note using alert component * Add /ontologies/ACRONYM/download?format=FORMAT route * ontology redirection based on the accept header using /ontologies/acronym/download route * Add redirect assertion in ontologies controller test * Fix content serialization when calling /ontologies/:acronym/:id * create private function for accept header and remove ontology redirection * remove generate_rewrite_rules function * remove redirect assertion in ontologies controller test * change htaccess route to /ontologies/:acronym/htaccess * fix copy internal links in LinkFieldComponent * add raw to copy internal links in identifiers card * return all response and add text/n3 format in content serializer * escape id when redirecting to content_finder * choose the right result from the results of search content * add algorithm to choose the right accept header, not_acceptable if no format valid * add an additional security to the uri redirection to have an exact match * make the check resolvability icon clickable & redirect to the tool page * make the check resolvability extend to accept equivalent formats * move the redirection function from ontologies_controller to uri concern * fix check_resolvability_tool to redirect to the full url not only the path - the problem was because everytime it is redirecting to the uri.path and uri.path does not include the ?format parameter - add the octet-stream accept header in the accepted format for xml * add reference to the url of the original file when format not acceptable * put again the resolvability timeout to 5 secondes * setup content negotiation and redirection tests * remove redirection when request ontology with format - the redirection is removed when request ontology in any format, by making request to the api and forward the result * add ids to the resource format icons and modal * assert the content type for html and json - the xml and csv is returning 500 internal server error from the api, but it's working locally * refactor content_redirection tests - still not completed * add multiple response status for ontology xml and csv format * add test for the content format of the resource * add turtle format based on hasOntologySyntax field * assert success and not_acceptable in turtle format it depends on hasOntologySyntax * add NTriples format for ontologies based on hasOntologySyntax field * test content negotiation for ontology resources * fix redirection url for resource html format * fix ontologies controller test indentation * assert redirection location in resource html format * update the admin user creation in tests possible only by an admin now --------- Co-authored-by: Syphax bouazzouni * Feature: add Annotator UI tests (#627) * setup annotator page UI tests * check if all the inputs and filters are present in annotator page test * add annotator http requests to test fixtures * test annotator results and count them * test annotator empty illustration * add comments in annotator page tests * use dynamic api in recommender tests * test that we have the exact correct annotations in the annotator test * undo adding ids for elements to run annotator tests * add a default ANNOTATOR_URL value for test config --------- Co-authored-by: Syphax bouazzouni * Fix: multiple highlighted terms in concepts date view (#657) * Fix multiple highlighted terms in the date view * refactor concepts by date method code * Fix: concept details table layout max-with when long strings (#649) * fix concepts table layout when we put very long strings * fix concepts json button position * use a css class intead of an id for the concepts_json_button * move concepts_json_button css class from concept details file to concepts file * remvoe #concepts_json_link unsued style --------- Co-authored-by: Syphax Bouazzouni * Feature: Clean no more used gems and code (#658) * remove cube ruby * remove not used gems and add documentation to the Gemfile * remove miniprofiler * remove bpdi resolver and redirect_to_new_api * remove ajax proxy and no more used helpers * remove no more used helpers * handle the case reset_agents in edit submissions error state are already Agents (#663) * Feature: Add issues and requests link to the footer and feedback form (#665) * add issues and requests link to the footer and feedback form * add $GITHUB_ISSUES to sample config file * Fix a typo in bioportal_config_env.rb.sample * fix: upload ontology on errors non returned values and no default values (#625) * move pry gem outside development block in gemfile (#671) * Feature: Add back slices (#598) * Add slices section to home page * Update the design of the slice top notice bar * fix home slice name styles * fix slices error on home page, when there are no slices available * make the annotator support slices * make browse page compatible with slices * update home slices section to include the number of ontologies per slice and the description * make mappings page supports slices * rearrange home page sections to be in this order: slices, ontoportal instances then collaborations * remove unnecessary code in submission filter file * Adjust the slice link to comply with the production URL * update home page slices section style * add a description to the home page slice section * update home page slices section style * update ontoportal instances and support home sections styles * change the button of add new slice to suggest a new group/slice in the home page * update slices descriptive texti in the home page * add a description for ontoportal instances in the home page * move home_ontoportal_tooltip to home helper * add ontoportal links in config file * remove http from slice link to make it work with https * add slice notice to lang local files * remove mappings statistics slices support in the UI cause it's already done in the back * update ontoportal website links to be upper case * fix slices https issue --------- Co-authored-by: Syphax bouazzouni * fix docker compose to make local env dev work in port 3000 * Fix: submissions properties selector labels and include ontologies properties (#669) * add ontologies properties to the submission properties selector * show submission attributes label instead of the keys in the selector * make slices open in the same tab not in a new tab * put the ontoportal instances directly before the logos instead of a tooltip * Fix: Redirect to the login page when accessing private ontology while being not authenticated (#673) * Feature: Distinguish reused terms in tree view (#650) * distinguish reused properties in the tree view * use preferredNamespaceUri to distinguish reused terms in the tree view if uriRegexPattern is not present * use preferredNamespaceUri to distinguish reused properties in the tree view * distinguish reuses for instances tree * clean terms reuses code * extract is_reused to a separate function in components helper * add concepts reuses to the date view * pass submission directly instead of ontology_uri_pattern * pass direcly the submission instead of passing by a function in terms reuses * Clean terms reuses concern code * add reuses to schemes tree * add reuses to schemes tree when we perform a search * add reuses for collections tree * add reuses to properties tree when we perform a search * add reuses for concepts list view * Clean terms reuses code * extract submission variable form the concept date render helper * remove binding.pry comments left in the code --------- Co-authored-by: Syphax Bouazzouni * Fix: run production mode (#679) * remove duplicate from gemfile * add port 3000 for production to use it with ontoportal_docker * add commands to run production correctly * Fix: json file extention in export metadata summary page (#683) * hide analytics features if not enabled (#691) * add search placeholder to data table component (#688) * fix browse page contact titleizing (#689) * fix mappings concept link not opening empty pages (#690) * Fix docker compose to make tests work (#695) * Fix: check resolvability tool redirection (#694) * Feature: add rewrite condition in URI redirection rules (#684) * add rewrite condition in the rewrite rules * refactor and clean code of htaccess and nginx redirection rules * Fix: User ontologies custom set updating (#685) * Fix ontologies custom set feature * enhance custom set feature style * remove duplicated action in home controller account, which is the same as show in users controller * clean application helper ontologies selector code * remove unecessary server calls in account page * Remove JQuery code in accounts page * remove repeated server call in custom ontologies features * fix the login flow test after deleting the repeated action related to `/acount` * Feauture: add acronym column in the agents table in the admin page (#704) * Fix: subscribe button issues (#681) * fix clicking on subscribe button while being not logged in * remove non related code to fix subscribe button issues PR * fix subscribe button displays wrong values when using french * replace UnWatch by unwatch * clean subscriptions method code in ontologies controller * Fix the color of the loader component spinning loader * remove duplicated code in subscription button fix * disable cache for subscription button calls * remove invalidate cache for the user calls * Feature: add response time calculation in check resolvability tool (#717) * add response time calculation in check resolvability tool * add other format equivalents to handle most cases of request accept header * move the AVG time to check_resolvability_message instead of appending it * remove the usage of inexistent variable in that block scope * remove comment as the variable name is already clear --------- Co-authored-by: Syphax Bouazzouni * Feature: add ontology viewOf to the summary page relation network (#716) * Feature: Remove subject values from the project and usage block in summary page (#719) * Feature: Add cookie banner (#702) * add cookies action to show and save cookie state in a session * add link button component helper * add cookie modal view * enhance the style of the cookies banner --------- Co-authored-by: Bilel KIHAL * Fix: update summary page categories display to show acronym with tooltip (#713) * update summary page categories display to show acronym with tooltip * fix submission flow system tests after changing their display * make show_category return the full domain if not a category * update the show category name to use clickable state if not a category and is a link * fix remove agent usages (#697) * Feature: Enhance agents search input component (#703) * make agents search field component results unlimited to only 4 * make agents cherchable by acronym, affiliations, email and home page * Update the agent search input to display the result in this format Orga name (ACRONYM) – ROR:003vg9w96 and for persons Person Name – ORCID:0000-1524-5487-5487 * chagne display mode value type from string to boolean in search input component * remove agents search logic from UI side and limit it to only name, acronym and identifiers * use the endpoint '/search/agents' for the agents search input component * Fix: saving and cancel button locals text capitalization (#724) * Feature: add qf param in agents search (#727) * Fix: Hide affiliations when agent is organization in agent form (#696) * Hide affiliations when the agent is an organization in the agents form * Change the name of the function affliations? to is_organization? to make it more clear * fix submission flows test after the fix of the PR #713 * fix agent tests after changing the behavior of displaying affiliations --------- Co-authored-by: Syphax * Feature: Make agent indentifiers field set by default based on agent type (#721) * remove the restriction for certain submission fields to create only organization type or person type agents * make orcid or ror default input based on agent type in agent form * fix disappeared signup form icons * fix nested agents creation * fix agent form not filling saved identifier values * create an agent identifier input helper * disable cookie banner in development and test mode * fix agent tests after enforcing ROR for organizations --------- Co-authored-by: Syphax * Fix: propertyid param auto added to summary page URL (#723) * Fix propertyId param auto added to summary page URL * remove added PropertyId param to the URL when opening summary page * Fix auto added propertyId to the summary page URL * Fix auto added instanceid to the summary page URL * Fix auto added propertyId to the summary page URL * add back the constraint of making frames loading lazy while env.development * make by default properties section displays the first property * make by default instances section displays the first instance * change the name of the function instances_tree_first_id by search_first_instance_id * add a clarification comment in search_first_instance_id method * move get_property function to properties controller --------- Co-authored-by: Syphax * fix losing context when changing language in concepts page * fix concept not added to browser URL param issue * refrech the whole page when we change concepts language * fix not saving the current url context when changing the language * Revert "Merge remote-tracking branch 'origin/fix/change-language-lost-context' into fix/change-language-lost-context" This reverts commit c7b6d08572c6e674993fc1ebda583d27ee91b3d3, reversing changes made to cb3b7ba702255b0eda51fba42c8b0628cebaa03b. * update turbo frame controller to handle ontology viewer special case --------- Co-authored-by: SirineMhedhbi <31127782+SirineMhedhbi@users.noreply.github.com> Co-authored-by: Syphax bouazzouni Co-authored-by: Imad Bourouche --- .../controllers/turbo_frame_controller.js | 37 ++++++++++++++++++- app/javascript/mixins/useHistory.js | 6 +-- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/app/javascript/controllers/turbo_frame_controller.js b/app/javascript/controllers/turbo_frame_controller.js index 41c67dd5d9..6520acb64a 100644 --- a/app/javascript/controllers/turbo_frame_controller.js +++ b/app/javascript/controllers/turbo_frame_controller.js @@ -24,8 +24,41 @@ export default class extends Controller { this.frame.innerHTML = this.placeHolderValue } else { this.frame.innerHTML = "" - this.urlValue = new HistoryService().getUpdatedURL(this.urlValue, data) - this.frame.src = this.urlValue + + if(this.#isCurrentPage()){ + this.urlValue = this.#currentPageUrl() + } + + this.urlValue = this.#updatedPageUrl(data) + + this.frame.src = this.urlValue + + } + } + + #isCurrentPage(){ + + let currentDisplayedUrl = new URL(this.#currentPageUrl(), document.location.origin) + + let initUrl = new URL(this.urlValue, document.location.origin) + + if (currentDisplayedUrl.toString().includes(this.urlValue)){ + return true + } else if (currentDisplayedUrl.searchParams.get('p') === initUrl.searchParams.get('p')){ + // this is a custom fix for only the ontology viewer page, + // that use the parameter ?p=section to tell which section is displayed + return true } + + return false + } + + + #currentPageUrl(){ + return document.location.pathname + document.location.search + } + + #updatedPageUrl(newUrlParams){ + return new HistoryService().getUpdatedURL(this.urlValue, newUrlParams) } } diff --git a/app/javascript/mixins/useHistory.js b/app/javascript/mixins/useHistory.js index ce895f1a64..e34b3c908e 100644 --- a/app/javascript/mixins/useHistory.js +++ b/app/javascript/mixins/useHistory.js @@ -59,9 +59,9 @@ export class HistoryService { return newState } - #updateURLFromState(urlParams, state) { - Object.entries(state).forEach(([key, val]) => { - if (key !== 'p'){ + #updateURLFromState(urlParams, oldState) { + Object.entries(oldState).forEach(([key, val]) => { + if (key !== 'p' && !urlParams.has(key)) { urlParams.set(key, val) } }) From ec7d6c37472d1f216f1bb85877cad843947dfc8a Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Mon, 26 Aug 2024 11:02:03 +0200 Subject: [PATCH 04/45] Feature: Add properties range domain the propery show page (#738) * add domain, range and properties to the property show view * fix the selection of the property using params[:propertyid] if given --- app/controllers/ontologies_controller.rb | 3 +-- app/views/properties/_show.html.haml | 17 ++++++++++------- config/locales/en.yml | 2 ++ config/locales/fr.yml | 2 ++ 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index a8ea520497..a8c744fe4a 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -98,8 +98,7 @@ def classes def properties @acronym = @ontology.acronym @properties = LinkedData::Client::HTTP.get("/ontologies/#{@acronym}/properties/roots", { lang: request_lang }) - - @property = get_property(@properties.first.id, @acronym, include: 'all') unless @property || @properties.empty? + @property = get_property(params[:propertyid] || @properties.first.id, @acronym, include: 'all') unless @property || @properties.empty? if request.xhr? render 'ontologies/sections/properties', layout: false diff --git a/app/views/properties/_show.html.haml b/app/views/properties/_show.html.haml index c034301496..d448430feb 100644 --- a/app/views/properties/_show.html.haml +++ b/app/views/properties/_show.html.haml @@ -3,14 +3,17 @@ - if @property.errors = render Display::AlertComponent.new(type:'info', message: @property.errors.join) - else + - properties = LinkedData::Client::Models::Property.properties_to_hash(@property).first = render ConceptDetailsComponent.new(id:'property-details', acronym: @acronym, concept_id: @property.id, - properties: OpenStruct.new(LinkedData::Client::Models::Property.properties_to_hash(@property).first), + properties: @property.properties, top_keys: [], - bottom_keys: [], + bottom_keys: properties.keys.map(&:to_s), exclude_keys: []) do |c| - c.header(stripped: true) do |t| - - t.add_row({th: t('properties.id')}, {td: link_to_with_actions(c.concept_properties[:id][:values], acronym: @acronym)}) if c.concept_properties[:id][:values].present? - - t.add_row({th: t('properties.type')}, {td: c.concept_properties[:type][:values] }) if c.concept_properties[:id][:values].present? - - t.add_row({th: t('properties.preferred_name')}, {td: display_in_multiple_languages(c.concept_properties[:label][:values])}) if c.concept_properties[:label][:values].present? - - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(c.concept_properties[:definition][:values])}) if c.concept_properties[:definition][:values].present? - - t.add_row({th: t('properties.parent')}, {td: display_in_multiple_languages(c.concept_properties[:parents][:values])}) if c.concept_properties[:parents][:values].present? \ No newline at end of file + - t.add_row({th: t('properties.id')}, {td: link_to_with_actions(@property.id, acronym: @acronym)}) + - t.add_row({th: t('properties.type')}, {td: @property.type }) + - t.add_row({th: t('properties.preferred_name')}, {td: display_in_multiple_languages(@property.label)}) unless @property.label.blank? + - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(@property.definition)}) unless @property.definition.blank? + - t.add_row({th: t('properties.parent')}, {td: display_in_multiple_languages(@property.parents)}) unless @property.parents.blank? + - t.add_row({th: t('properties.domain')}, {td: get_link_for_cls_ajax(@property.domain, @acronym)}) unless @property.domain.blank? + - t.add_row({th: t('properties.range')}, {td: get_link_for_cls_ajax(@property.range, @acronym)}) unless @property.range.blank? \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index cebfe790cf..8af840241b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1436,6 +1436,8 @@ en: preferred_name: Preferred name definitions: Definitions parent: Parent + domain: Domain + range: Range no_properties_alert: "%{acronym} does not contain properties" visits: ontology_visits: Ontology visits diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9aee5151f1..2c36d9bc1c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1468,6 +1468,8 @@ fr: preferred_name: Nom préféré definitions: Définitions parent: Parent + domain: Domaine + range: Étendue no_properties_alert: "%{acronym} ne contient pas de propriétés" visits: From ba306bd138ea6fbb3b9d98415fe5aa402661a6d7 Mon Sep 17 00:00:00 2001 From: Syphax Date: Wed, 4 Sep 2024 23:44:52 +0200 Subject: [PATCH 05/45] fix browse page showing an error if second page --- app/controllers/concerns/submission_filter.rb | 4 ++-- app/helpers/ontologies_helper.rb | 4 ++-- app/views/ontologies/browser/_ontologies.html.haml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/concerns/submission_filter.rb b/app/controllers/concerns/submission_filter.rb index 44d4f6e52f..45746e36e2 100644 --- a/app/controllers/concerns/submission_filter.rb +++ b/app/controllers/concerns/submission_filter.rb @@ -61,8 +61,8 @@ def submissions_paginate_filter(params) [@page.collection, @page.totalCount, count, filter_params] end - def ontologies_filter_url(filters, page: 1, count: false) - helpers.ontologies_filter_url(filters, page: page, count: count) + def ontologies_with_filters_url(filters, page: 1, count: false) + helpers.ontologies_with_filters_url(filters, page: page, count: count) end private diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index 4e05ad775c..4d7816cb6a 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -104,8 +104,8 @@ def ontologies_browse_skeleton(pagesize = 5) end end - def ontologies_filter_url(filters, page: 1, count: false) - url = 'ontologies_filter?' + def ontologies_with_filters_url(filters, page: 1, count: false) + url = '/ontologies_filter?' url += "page=#{page}" if page url += "count=#{page}" if count if filters diff --git a/app/views/ontologies/browser/_ontologies.html.haml b/app/views/ontologies/browser/_ontologies.html.haml index 8e3d8bd304..bd3aed0cf5 100644 --- a/app/views/ontologies/browser/_ontologies.html.haml +++ b/app/views/ontologies/browser/_ontologies.html.haml @@ -1,6 +1,6 @@ = render InfiniteScrollComponent.new(id: 'ontologies_list', collection: @ontologies, - next_url: ontologies_filter_url(@request_params, page: @page.nextPage), + next_url: ontologies_with_filters_url(@request_params, page: @page.nextPage), current_page: @page.page, next_page: @page.nextPage) do |c| - if @page.page.eql?(1) From 6735b717394477eddff99c69608842671993563f Mon Sep 17 00:00:00 2001 From: Syphax Date: Thu, 5 Sep 2024 00:48:59 +0200 Subject: [PATCH 06/45] handle the case of errors in indexing the ontology name or acronym --- app/controllers/concerns/search_aggregator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index 5656815b1c..d50d369a8f 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -68,7 +68,7 @@ def search_result_elem(class_object, ontology_acronym, title) { uri: class_object.id.to_s, - title: title.empty? ? label : "#{label} - #{title}", + title: title.nil? || title.empty? ? "#{label} - #{ontology_acronym}" : "#{label} - #{title}", ontology_acronym: ontology_acronym, link: "/ontologies/#{ontology_acronym}?p=classes&conceptid=#{escape(class_object.id)}#{helpers.request_lang&.eql?("ALL") ? '' : "&language="+helpers.request_lang.to_s}", definition: class_object.definition @@ -78,7 +78,7 @@ def search_result_elem(class_object, ontology_acronym, title) def ontology_name_acronym(ontologies, selected_acronym) ontology = ontologies.select { |x| x.acronym.eql?(selected_acronym.split('/').last) }.first - "#{ontology.name} (#{ontology.acronym})" + "#{ontology.name} (#{ontology.acronym})" if ontology end def aggregate_by_ontology(results) From 9e3841e6a78dc0b1d1d41ea375e20db6c6968661 Mon Sep 17 00:00:00 2001 From: Syphax Date: Thu, 5 Sep 2024 01:31:00 +0200 Subject: [PATCH 07/45] remove parents from the properties show partial --- app/views/properties/_show.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/properties/_show.html.haml b/app/views/properties/_show.html.haml index c034301496..5df24c0bf1 100644 --- a/app/views/properties/_show.html.haml +++ b/app/views/properties/_show.html.haml @@ -12,5 +12,4 @@ - t.add_row({th: t('properties.id')}, {td: link_to_with_actions(c.concept_properties[:id][:values], acronym: @acronym)}) if c.concept_properties[:id][:values].present? - t.add_row({th: t('properties.type')}, {td: c.concept_properties[:type][:values] }) if c.concept_properties[:id][:values].present? - t.add_row({th: t('properties.preferred_name')}, {td: display_in_multiple_languages(c.concept_properties[:label][:values])}) if c.concept_properties[:label][:values].present? - - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(c.concept_properties[:definition][:values])}) if c.concept_properties[:definition][:values].present? - - t.add_row({th: t('properties.parent')}, {td: display_in_multiple_languages(c.concept_properties[:parents][:values])}) if c.concept_properties[:parents][:values].present? \ No newline at end of file + - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(c.concept_properties[:definition][:values])}) if c.concept_properties[:definition][:values].present? \ No newline at end of file From 4f09782e9000efc37d0566df813a2edafb25e730 Mon Sep 17 00:00:00 2001 From: Syphax Date: Thu, 5 Sep 2024 01:31:33 +0200 Subject: [PATCH 08/45] don't cache the fairscore call response if empty or status error --- .../ontology_browse_card_component.html.haml | 2 +- app/helpers/fair_score_helper.rb | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml index 92e80c05d8..aaf97e85fd 100644 --- a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml +++ b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml @@ -16,7 +16,7 @@ = t('components.show_more') - - unless ontology[:fairScore].nil? || ontology[:acronym] == 'AGROVOC' + - unless ontology[:fairScore].nil? || ontology[:fairScore].zero? || ontology[:acronym] == 'AGROVOC' .browse-fair %p.browse-fair-title = t('components.fair_score') diff --git a/app/helpers/fair_score_helper.rb b/app/helpers/fair_score_helper.rb index 7abb76eb66..ef5b81e2df 100644 --- a/app/helpers/fair_score_helper.rb +++ b/app/helpers/fair_score_helper.rb @@ -16,18 +16,22 @@ def get_fairness_json(ontologies_acronyms, apikey = user_apikey) if Rails.cache.exist?("fairness-#{ontologies_acronyms.gsub(',', '-')}-#{apikey}") out = read_large_data("fairness-#{ontologies_acronyms.gsub(',', '-')}-#{apikey}") else - out = "{}" + out = '{}' begin time = Benchmark.realtime do conn = Faraday.new do |conn| conn.options.timeout = 30 end response = conn.get(get_fairness_service_url(apikey) + "&ontologies=#{ontologies_acronyms}&combined") - out = response.body.force_encoding('ISO-8859-1').encode('UTF-8') - cache_large_data("fairness-#{ontologies_acronyms.gsub(',', '-')}-#{apikey}", out) + if response.status.eql?(200) + out = response.body.force_encoding('ISO-8859-1').encode('UTF-8') + unless out.empty? || out.strip.eql?('{}') + cache_large_data("fairness-#{ontologies_acronyms.gsub(',', '-')}-#{apikey}", out) + end + end end puts "Call fairness service for: #{ontologies_acronyms} (#{time}s)" - rescue + rescue StandardError Rails.logger.warn t('fair_score.fairness_unreachable_warning') end end @@ -140,7 +144,7 @@ def print_score(score) def fairness_link(style: '', ontology: nil) custom_style = "font-size: 50px; line-height: 0.5; margin-left: 6px; #{style}".strip ontology = ontology || 'all' - render IconWithTooltipComponent.new(icon: "json.svg",link: "#{get_fairness_service_url}&ontologies=#{ontology}&combined=true", target: '_blank', title: t('fair_score.go_to_api'), size:'small', style: custom_style) + render IconWithTooltipComponent.new(icon: 'json.svg',link: "#{get_fairness_service_url}&ontologies=#{ontology}&combined=true", target: '_blank', title: t('fair_score.go_to_api'), size:'small', style: custom_style) end private From 1c6e49398241de03a8a38cbf91757cd797da840e Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 9 Sep 2024 00:45:58 +0200 Subject: [PATCH 09/45] fix properties and domain showing content missing after on chips click --- app/views/properties/_show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/properties/_show.html.haml b/app/views/properties/_show.html.haml index 329fcb3ac9..f74964f85e 100644 --- a/app/views/properties/_show.html.haml +++ b/app/views/properties/_show.html.haml @@ -14,5 +14,5 @@ - t.add_row({th: t('properties.type')}, {td: @property.type }) - t.add_row({th: t('properties.preferred_name')}, {td: display_in_multiple_languages(@property.label)}) unless @property.label.blank? - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(@property.definition)}) unless @property.definition.blank? - - t.add_row({th: t('properties.domain')}, {td: get_link_for_cls_ajax(@property.domain, @acronym)}) unless @property.domain.blank? - - t.add_row({th: t('properties.range')}, {td: get_link_for_cls_ajax(@property.range, @acronym)}) unless @property.range.blank? + - t.add_row({th: t('properties.domain')}, {td: get_link_for_cls_ajax(@property.domain, @acronym, '_top')}) unless @property.domain.blank? + - t.add_row({th: t('properties.range')}, {td: get_link_for_cls_ajax(@property.range, @acronym, '_top')}) unless @property.range.blank? From 40c6c1f8d260db9fe893591e2b0cef6558147c01 Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 9 Sep 2024 00:46:28 +0200 Subject: [PATCH 10/45] update cookie banner state to be permanent and not show again --- app/controllers/home_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 759965b544..3f75bdd3f6 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -47,7 +47,7 @@ def index end def set_cookies - session[:cookies_accepted] = params[:cookies] if params[:cookies] + cookies.permanent[:cookies_accepted] = params[:cookies] if params[:cookies] render 'cookies', layout: nil end From 9b563a645c4c3d8adcc9b209d2bc5fd5b6430bf2 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Mon, 9 Sep 2024 01:20:08 +0200 Subject: [PATCH 11/45] Feature: Add groups and categories pages (#736) * create taxonomy (groups&categories) route, view, controller * add groupes and categories tabs sections * do the layout of the groupes section in taxonomy page * extract taxonomy card to a helper * add slice icon for the groupes associated with a slice in the portal * display categories in taxonomy page * put taxonomy page texts in local lang files * use models methods instead of get http calls in taxonomy controller * create a viewcomponent for taxonomy card instead of a helper * fix name of groups section view (from groupes) * add the possibility to add parent category in the admin page * display categories children in categories page * make slice icon clickable in groupes cards * make ontologies clickable in groups and categories cards * add /groups and /categories routes * change browser link when user change section in taxonomy page * put parent category in local lang files * fix some typos in groups and categories page * show ontology names in tooltip in groups&categories page * add link to groups and categories in the topnav * add link to taxonomy page in browse page * limite description text size in taxonomy card component * create limited container component and use it in taxonomy card component to limit ontologies chips height * update taxonomy page description * extract categories_for_select to a resusable helper * clean taxonomy controller routes * move browse_taxonomy_tooltip from application helper to ontologies helper * extract categories select to a reusable helper * clean taxonomy partials * fix limited container component hight mesuring * make the tabs overwriting apply only for the groups categories page * fix the info tooltip text of the community dropdown --------- Co-authored-by: Syphax --- app/assets/images/icons/slices.svg | 4 +- .../stylesheets/application.css.scss.erb | 1 + app/assets/stylesheets/taxonomy.scss | 112 ++++++++++++++++++ .../display/limited_container_component.rb | 6 + .../limited_container_component.html.haml | 6 + .../limited_container_component_controller.js | 25 ++++ .../display/taxonomy_card_component.rb | 10 ++ .../taxonomy_card_component.html.haml | 27 +++++ .../admin/categories_controller.rb | 1 - app/controllers/taxonomy_controller.rb | 46 +++++++ app/helpers/application_helper.rb | 4 + app/helpers/ontologies_helper.rb | 7 ++ app/javascript/component_controllers/index.js | 3 +- app/views/admin/categories/_form.html.haml | 5 + app/views/layouts/_topnav.html.haml | 2 + app/views/ontologies/browser/browse.html.haml | 6 +- .../ontologies/sections/_metadata.html.haml | 4 +- app/views/taxonomy/_taxonomies.html.haml | 10 ++ app/views/taxonomy/index.html.haml | 24 ++++ config/locales/en.yml | 8 +- config/locales/fr.yml | 8 +- config/routes.rb | 3 + 22 files changed, 313 insertions(+), 9 deletions(-) create mode 100644 app/assets/stylesheets/taxonomy.scss create mode 100644 app/components/display/limited_container_component.rb create mode 100644 app/components/display/limited_container_component/limited_container_component.html.haml create mode 100644 app/components/display/limited_container_component/limited_container_component_controller.js create mode 100644 app/components/display/taxonomy_card_component.rb create mode 100644 app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml create mode 100644 app/controllers/taxonomy_controller.rb create mode 100644 app/views/taxonomy/_taxonomies.html.haml create mode 100644 app/views/taxonomy/index.html.haml diff --git a/app/assets/images/icons/slices.svg b/app/assets/images/icons/slices.svg index e92cb02f9d..ce6a9a3d98 100644 --- a/app/assets/images/icons/slices.svg +++ b/app/assets/images/icons/slices.svg @@ -1,4 +1,4 @@ - - + + diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb index 8bf1869fa3..ac5160afc2 100755 --- a/app/assets/stylesheets/application.css.scss.erb +++ b/app/assets/stylesheets/application.css.scss.erb @@ -58,6 +58,7 @@ @import "agent_tooltip"; @import "content_finder"; @import "tools"; +@import "taxonomy"; /* Bootstrap and Font Awesome */ @import "bootstrap"; diff --git a/app/assets/stylesheets/taxonomy.scss b/app/assets/stylesheets/taxonomy.scss new file mode 100644 index 0000000000..2a9bf66eca --- /dev/null +++ b/app/assets/stylesheets/taxonomy.scss @@ -0,0 +1,112 @@ +.taxonomy-page-container { + display: flex; + justify-content: center; + } +.taxonomy-page-subcontainer { + width: 1248px; + padding: 20px 50px; +} +.taxonomy-page-title .text{ + font-size: 25px; + font-weight: 700; +} + +.taxonomy-page-title .line{ + height: 2px; + width: 57px; + background-color: var(--primary-color); + border-radius: 10px; +} +.taxonomy-page-decription{ + color: #888888; + margin: 20px 0; +} +.taxonomy-section{ + display: flex; +} +.taxonomy-section .second-row{ + margin-left: 10px; +} +.taxonomy-card{ + border: 1px solid #DFDFDF; + padding: 15px 20px; + width: 569px; + border-radius: 5px; + margin-top: 10px; +} + +.taxonomy-card .title-bar{ + display: flex; + justify-content: space-between; + align-items: center; +} +.taxonomy-card .title{ + color: var(--primary-color); + font-size: 18px; + font-weight: bold; +} + +.taxonomy-card .ontologies{ + display: flex; + align-items: center; + color: var(--primary-color); + margin: 3px 0; +} +.taxonomy-card .ontologies svg{ + width: 15px; + height: 15px; + margin-right: 10px; +} + +.taxonomy-card .ontologies svg path{ + fill: var(--primary-color); +} +.taxonomy-card .description{ + color: #666666; + padding-bottom: 5px; +} +.taxonomy-slice-svg{ + width: 35px; + height: 35px; + margin-bottom: 2px; +} + +.taxonomy-children-reveal{ + display: flex; + align-items: center; + cursor: pointer; + color: var(--primary-color); +} +.taxonomy-children-reveal svg{ + margin-left: 5px; + margin-top: 3px; +} + +.taxonomy-children .taxonomy-card{ + border: none; + padding: 0px 18px; + width: unset; +} + +.taxonomy-children-container{ + display: flex; + margin-top: 10px +} + +.taxonomy-children-line{ + width: 1px; + background-color: var(--primary-color); +} + +.taxonomy-section { + .tab-content > .tab-pane { + visibility: hidden !important; + display: block !important; + position: absolute; + } + .tab-content > .active { + visibility: visible !important; + display: block; + position: unset; + } +} diff --git a/app/components/display/limited_container_component.rb b/app/components/display/limited_container_component.rb new file mode 100644 index 0000000000..62b290c83a --- /dev/null +++ b/app/components/display/limited_container_component.rb @@ -0,0 +1,6 @@ +class Display::LimitedContainerComponent < ViewComponent::Base + renders_one :revealButton + def initialize(max_height: '90') + @max_height = max_height + end +end diff --git a/app/components/display/limited_container_component/limited_container_component.html.haml b/app/components/display/limited_container_component/limited_container_component.html.haml new file mode 100644 index 0000000000..ee52898915 --- /dev/null +++ b/app/components/display/limited_container_component/limited_container_component.html.haml @@ -0,0 +1,6 @@ +%div{'data-controller': 'limited-container', 'data-limited-container-height-value': @max_height} + .limited-container-component{'data-limited-container-target': "content", style: "max-height: #{@max_height}px; overflow: hidden;"} + = content + + .reveal-button{'data-limited-container-target': "revealbutton", 'data-action': "click->limited-container#toggle"} + = revealButton \ No newline at end of file diff --git a/app/components/display/limited_container_component/limited_container_component_controller.js b/app/components/display/limited_container_component/limited_container_component_controller.js new file mode 100644 index 0000000000..6cae826929 --- /dev/null +++ b/app/components/display/limited_container_component/limited_container_component_controller.js @@ -0,0 +1,25 @@ +import { Controller } from '@hotwired/stimulus' + +// Connects to data-controller="limited-container" + +export default class extends Controller { + static targets = [ "content", "revealbutton"] + static values = { + height: Number + } + connect(){ + this.contentTarget + if(this.contentTarget.offsetHeightreveal-component#toggle", 'data-id': reveal_id} + .text + = t('taxonomy.show_sub_categories') + = inline_svg_tag 'icons/arrow-down.svg' + .taxonomy-children-container + .taxonomy-children-line + .taxonomy-children.d-none{id: reveal_id} + - @taxonomy.children.each do |child| + = render Display::TaxonomyCardComponent.new(taxonomy: child, ontologies_names: @ontologies_names) \ No newline at end of file diff --git a/app/controllers/admin/categories_controller.rb b/app/controllers/admin/categories_controller.rb index 9a4e357c98..f2c0d3ea00 100644 --- a/app/controllers/admin/categories_controller.rb +++ b/app/controllers/admin/categories_controller.rb @@ -15,7 +15,6 @@ def index def new @category = LinkedData::Client::Models::Category.new - respond_to do |format| format.html { render "new", :layout => false } end diff --git a/app/controllers/taxonomy_controller.rb b/app/controllers/taxonomy_controller.rb new file mode 100644 index 0000000000..06832abe8b --- /dev/null +++ b/app/controllers/taxonomy_controller.rb @@ -0,0 +1,46 @@ +class TaxonomyController < ApplicationController + + layout :determine_layout + + def index + initialize_taxonomy + @category_section_active = request.path.eql?('/categories') + end + + private + + def initialize_taxonomy + @groups = LinkedData::Client::Models::Group.all + slices = LinkedData::Client::Models::Slice.all + slices_acronyms = slices.map { |slice| slice.acronym.downcase } + @groups.each do |group| + if slices_acronyms.include?(group.acronym.downcase) + group[:is_slice] = true + end + end + @categories = LinkedData::Client::Models::Category.all(display: 'name,acronym,description,ontologies,parentCategory') + @categories = nest_categories_children(@categories) + ontologies = LinkedData::Client::Models::Ontology.all + @ontologies_names = {} + ontologies.each do |o| + @ontologies_names[o.id] = o.name + end + end + + def nest_categories_children(categories) + category_index = {} + categories.each do |category| + category_index[category[:id]] = category + end + categories.each do |category| + if category.parentCategory + parent = category_index[category.parentCategory] + parent[:children] ||= [] + parent[:children] << category + end + end + categories.reject! { |category| category.parentCategory } + categories + end + +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index deb125bdec..c9fb873758 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -544,4 +544,8 @@ def cancel_button_component(class_name: nil, id: , value:, data: nil) end end + def categories_select(id: nil, name: nil, selected: 'None') + categories_for_select = LinkedData::Client::Models::Category.all.map{|x| ["#{x.name} (#{x.acronym})", x.id]}.unshift(["None", '']) + render Input::SelectComponent.new(id: id, name: name, value: categories_for_select, selected: selected) + end end diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index 4d7816cb6a..33adc9efd5 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -780,5 +780,12 @@ def id_to_acronym(id) id.split('/').last end + def browse_taxonomy_tooltip(texonomy) + content_tag(:div, class: 'd-flex') do + content_tag(:div, "See more information about #{texonomy} in ", class: 'mr-1') + + content_tag(:a, 'here', href: "/#{texonomy}", target: '_blank') + end + end + end diff --git a/app/javascript/component_controllers/index.js b/app/javascript/component_controllers/index.js index eaf1728f09..72893043fd 100644 --- a/app/javascript/component_controllers/index.js +++ b/app/javascript/component_controllers/index.js @@ -25,7 +25,7 @@ import Table_component_controller from '../../components/table_component/table_c import clipboard_component_controller from '../../components/clipboard_component/clipboard_component_controller' import range_slider_component_controller from '../../components/input/range_slider_component/range_slider_component_controller' import RDFHighlighter from '../../components/display/rdf_highlighter_component/rdf_highlighter_component_controller' - +import limited_container_component_controller from '../../components/display/limited_container_component/limited_container_component_controller' application.register("rdf-highlighter", RDFHighlighter) application.register('turbo-modal', TurboModalController) @@ -43,3 +43,4 @@ application.register('reveal-component', Reveal_component_controller) application.register('table-component', Table_component_controller) application.register('clipboard', clipboard_component_controller) application.register('range-slider', range_slider_component_controller) +application.register('limited-container', limited_container_component_controller) diff --git a/app/views/admin/categories/_form.html.haml b/app/views/admin/categories/_form.html.haml index a1abde7419..75974b5a3a 100644 --- a/app/views/admin/categories/_form.html.haml +++ b/app/views/admin/categories/_form.html.haml @@ -30,6 +30,11 @@ = t('admin.categories.form.description') %td.top = f.text_area :description, class: "form-control" + %tr + %th + = t('admin.categories.form.parent_category') + %td.top + = categories_select(id: 'category_parent_select', name: 'category[parentCategory]', selected: @category&.parentCategory) - unless new_record %tr %th diff --git a/app/views/layouts/_topnav.html.haml b/app/views/layouts/_topnav.html.haml index 4d0a013748..c73008dfeb 100644 --- a/app/views/layouts/_topnav.html.haml +++ b/app/views/layouts/_topnav.html.haml @@ -60,4 +60,6 @@ = link_to(t('layout.header.cite_us'), $FOOTER_LINKS[:sections][:about][:cite_us], target: "_blank") - s.item do = link_to(t('layout.header.release_notes'), $FOOTER_LINKS[:sections][:products][:release_notes], target: "_blank") + - s.item do + = link_to(t('taxonomy.groups_and_categories'), '/groups') diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index 83da56b6c5..d1979fd359 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -56,7 +56,11 @@ = browse_filter_section_label(key) %span.badge.badge-primary{"data-show-filter-count-target":"countSpan", style: "#{values[2] && values[2].positive? ? '' : 'display: none;'}"} = values[2] - = inline_svg_tag 'arrow-down.svg' + .d-flex.align-items-center + - if key.eql?(:categories) || key.eql?(:groups) + .mr-2 + = render Display::InfoTooltipComponent.new(text: browse_taxonomy_tooltip(key.to_s)) + = inline_svg_tag 'arrow-down.svg' .collapse{id: "browse-#{key}-filter", class: "#{values[2].positive? ? 'show': ''}"} .browse-filter-checks-container - values.first.each do |object| diff --git a/app/views/ontologies/sections/_metadata.html.haml b/app/views/ontologies/sections/_metadata.html.haml index 9dd1a07df7..69aadbc570 100755 --- a/app/views/ontologies/sections/_metadata.html.haml +++ b/app/views/ontologies/sections/_metadata.html.haml @@ -36,7 +36,7 @@ document.querySelector("[data-target='#projects_section']")?.click() } = properties_dropdown('methodology',t("ontologies.sections.methodology_and_provenance"), t("ontologies.sections.info_tooltip_properties_dropdown"), @methodology_properties) - = properties_dropdown('community',t("ontologies.sections.community"), t("ontologies.sections.info_tooltip_properties_dropdown"), nil ) do |c| + = properties_dropdown('community',t("ontologies.sections.community"), t("ontologies.sections.info_tooltip_community_dropdown", site: portal_name), nil ) do |c| - properties_list_component(c, @community_properties, truncate: false) - unless Array(@ontology.group).empty? - c.row do @@ -44,7 +44,7 @@ = horizontal_list_container(@ontology.group) do |v| = render ChipButtonComponent.new(text: show_group_name(v), type: "static", tooltip: show_group_name(v)) - = properties_dropdown('content',t("ontologies.sections.content"), t("ontologies.sections.info_tooltip_properties_dropdown"), nil) do |c| + = properties_dropdown('content',t("ontologies.sections.content"), t("ontologies.sections.info_tooltip_properties_dropdown", site: portal_name), nil) do |c| - properties_list_component(c, @content_properties.reject{|k, v| %w[keyClasses metadataVoc].include?(k.to_s)}) - c.row do = render FieldContainerComponent.new(label: attr_label('metadataVoc', attr_metadata: attr_metadata("metadataVoc"), show_tooltip: false)) do diff --git a/app/views/taxonomy/_taxonomies.html.haml b/app/views/taxonomy/_taxonomies.html.haml new file mode 100644 index 0000000000..0dd56f6876 --- /dev/null +++ b/app/views/taxonomy/_taxonomies.html.haml @@ -0,0 +1,10 @@ +.taxonomy-section + - pairs, impairs = taxonomies.each_with_index.partition { |_, index| index.even? } + - taxonomies_first_row = pairs.map(&:first) + - taxonomies_second_row = impairs.map(&:first) + .first-row + - taxonomies_first_row.each do |taxonomy| + = render Display::TaxonomyCardComponent.new(taxonomy: taxonomy, ontologies_names: @ontologies_names) + .second-row + - taxonomies_second_row.each do |taxonomy| + = render Display::TaxonomyCardComponent.new(taxonomy: taxonomy, ontologies_names: @ontologies_names) \ No newline at end of file diff --git a/app/views/taxonomy/index.html.haml b/app/views/taxonomy/index.html.haml new file mode 100644 index 0000000000..967f63d330 --- /dev/null +++ b/app/views/taxonomy/index.html.haml @@ -0,0 +1,24 @@ +.taxonomy-page-container + .taxonomy-page-subcontainer + .taxonomy-page-title + .text + = t('taxonomy.groups_and_categories') + .line + .taxonomy-page-decription + = t('taxonomy.description') + + = render TabsContainerComponent.new do |c| + - c.item(title: 'Groups', selected: !@category_section_active) + - c.item_content do + = render partial: '/taxonomy/taxonomies', locals: { taxonomies: @groups } + + - c.item(title: 'Categories', selected: @category_section_active) + - c.item_content do + = render partial: '/taxonomy/taxonomies', locals: { taxonomies: @categories } +:javascript + document.getElementById('categories_tab').addEventListener('click', function(event) { + window.history.pushState({ path: '/categories' }, '', '/categories'); + }) + document.getElementById('groups_tab').addEventListener('click', function(event) { + window.history.pushState({ path: '/groups' }, '', '/groups'); + }) \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 8af840241b..fd04ba863c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -203,6 +203,7 @@ en: description: Description created: Created ontologies: Ontologies + parent_category: Parent category edit: edit_category: Edit category save: Save @@ -1275,6 +1276,7 @@ en: community: Community content: Content info_tooltip_properties_dropdown: Metadata properties primarily encompass the design, methods, and actions to create the ontology. This includes elements such as the tools and software employed by the creator of the ontology during its configuration. + info_tooltip_community_dropdown: Metadata properties related to community information such as the code repository and issue tracker, the mailing list or the target audience. This includes also the Groups in %{site}. See more information about Groups and Categories. visits: Visits views: Views of %{acronym} create_new_view: Create new view @@ -1490,4 +1492,8 @@ en: paragraph1: "%{portal} uses cookies to help you navigate efficiently and for audience measurement. You will find detailed information about all cookies in the \"Privacy policy\" link" paragraph2: "The cookies are functional and non-optional. By staying on %{portal}, you acknowledge the information was delivered to you." accept_button: "Accept" - privacy_link: "Privacy policy" \ No newline at end of file + privacy_link: "Privacy policy" + taxonomy: + groups_and_categories: Groups and Categories + description: In AgroPortal, ontologies are organized in groups and tagged with categories. Typically, groups associate ontologies from the same project or organization for better identification of the provenance. Whereas categories are about subjects/topics and enable to classify ontologies. As of 2016, AgroPortal's categories were established in cooperation with FAO AIMS. In 2024, we moved to UNESCO nomenclature for fields of science and technology. Groups and categories, along with other metadata, can be used on the “Browse” page of AgroPortal to filter out the list of ontologies. + show_sub_categories: Show sub categories \ No newline at end of file diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2c36d9bc1c..f537fc23ad 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -209,6 +209,7 @@ fr: description: Description created: Créé ontologies: Ontologies + parent_category: Catégorie parent edit: edit_category: Modifier la catégorie save: Sauvegarder @@ -1301,6 +1302,7 @@ fr: community: Communauté content: Contenu info_tooltip_properties_dropdown: Propriétés de métadonnées englobant principalement la conception, les méthodes et les actions pour créer l'ontologie. Cela inclut des éléments tels que les outils et logiciels utilisés par le créateur de l'ontologie lors de sa configuration. + info_tooltip_community_dropdown: Propriétés des métadonnées liées aux informations communautaires telles que le dépôt de code et le suivi des problèmes, la liste de diffusion ou le public cible. Cela inclut également les Groupes dans %{site}. Voir plus d'informations sur les Groupes et les Catégories. visits: Visites views: Vues de %{acronym} create_new_view: Créer une nouvelle vue @@ -1529,4 +1531,8 @@ fr: paragraph1: "%{portal} utilise des cookies pour vous aider à naviguer efficacement et pour mesurer l'audience. Vous trouverez des informations détaillées sur tous les cookies dans le lien \"Vie privé\"" paragraph2: "Les cookies sont fonctionnels et non facultatifs. En restant sur %{portal}, vous reconnaissez que les informations vous ont été transmises." accept_button: "Accepter" - privacy_link: "Vie privé" \ No newline at end of file + privacy_link: "Vie privé" + taxonomy: + groups_and_categories: Groupes et Catégories + description: Dans AgroPortal, les ontologies sont organisées en groupes et étiquetées avec des catégories. Typiquement, les groupes associent des ontologies provenant du même projet ou de la même organisation pour une meilleure identification de la provenance. Tandis que les catégories concernent des sujets/thématiques et permettent de classifier les ontologies. En 2016, les catégories d'AgroPortal ont été établies en coopération avec FAO AIMS. En 2024, nous sommes passés à la nomenclature de l'UNESCO pour les domaines des sciences et des technologies. Les groupes et les catégories, ainsi que d'autres métadonnées, peuvent être utilisés sur la page “Parcourir” d'AgroPortal pour filtrer la liste des ontologies. + show_sub_categories: Afficher les sous-catégories \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 6d5b78ba88..f31620d09a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -207,6 +207,9 @@ get '/login_as/:login_as' => 'login#login_as', constraints: { login_as: /[\d\w\.\-\%\+ ]+/ } post '/login/send_pass', to: 'login#send_pass' + get '/groups' => 'taxonomy#index' + get '/categories' => 'taxonomy#index' + # Search get 'search', to: 'search#index' get 'ajax/search/ontologies/content', to: 'search#json_ontology_content_search' From b7eec8371fdabd0faffc47612b86c8456c0754cc Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 9 Sep 2024 12:00:34 +0200 Subject: [PATCH 12/45] fix: cookies banner state not disappearing --- app/views/home/cookies.html.haml | 2 +- app/views/layouts/_header.html.erb | 2 +- app/views/layouts/appliance.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/home/cookies.html.haml b/app/views/home/cookies.html.haml index 0a625a2beb..d02f687a9b 100644 --- a/app/views/home/cookies.html.haml +++ b/app/views/home/cookies.html.haml @@ -1,5 +1,5 @@ = turbo_frame_tag :cookies_modal do - - if session[:cookies_accepted].nil? # don't re-render if a true/false selected + - if cookies[:cookies_accepted].nil? # don't re-render if a true/false selected %section.cookies-modal %h4 = t('cookies_modal.title') diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 07d28d73d6..1340bdbb26 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -57,7 +57,7 @@ <%= render partial: 'layouts/topnav' %> -<%= turbo_frame_tag :cookies_modal, src: cookies_path if session[:cookies_accepted].nil? && !(Rails.env.development? || Rails.env.test?) %> +<%= turbo_frame_tag :cookies_modal, src: cookies_path if cookies[:cookies_accepted].nil? && !(Rails.env.development? || Rails.env.test?) %>
      <%= render partial: 'layouts/notices' %> \ No newline at end of file diff --git a/app/views/layouts/appliance.html.haml b/app/views/layouts/appliance.html.haml index d40fbdce68..3e306068c0 100644 --- a/app/views/layouts/appliance.html.haml +++ b/app/views/layouts/appliance.html.haml @@ -26,7 +26,7 @@ %body{:class => "#{controller_name} #{action_name}"} = render partial: "layouts/topnav" - = turbo_frame_tag :cookies_modal, src: cookies_path if session[:cookies_accepted].nil? + = turbo_frame_tag :cookies_modal, src: cookies_path if cookies[:cookies_accepted].nil? %div.flex-grow-1 = render partial: "layouts/notices" From b84cf71609b4ed22a710b30a786a920c589cde5c Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 9 Sep 2024 02:31:06 +0200 Subject: [PATCH 13/45] use master version api client meanwhile fix the federation userapikey --- Gemfile | 4 +- Gemfile.lock | 175 +++++++++++++++++++++++++-------------------------- 2 files changed, 86 insertions(+), 93 deletions(-) diff --git a/Gemfile b/Gemfile index 51dc38ee9b..c40e3640c5 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' # Main Rails gem # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '7.0.3' +gem 'rails', '7.0.4' # JavaScript bundling for Rails gem 'jsbundling-rails' @@ -96,7 +96,7 @@ gem 'flag-icons-rails', '~> 3.4' gem 'iso-639', '~> 0.3.6' # Custom API client -gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'development' +gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'master' # Ruby 2.7.8 pinned gems (to remove when migrating to Ruby >= 3.0) gem 'ffi', '~> 1.16.3' diff --git a/Gemfile.lock b/Gemfile.lock index a44d7ee5bc..51f97081af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ GIT remote: https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git - revision: abea6c217c0ee93bcfd80c47c2af52747365d491 - branch: development + revision: 24fb2549f7b69841e052491439bc8375ed5acfd9 + branch: master specs: ontologies_api_client (2.2.0) activesupport @@ -17,67 +17,67 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.3) - actionpack (= 7.0.3) - activesupport (= 7.0.3) + actioncable (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.3) - actionpack (= 7.0.3) - activejob (= 7.0.3) - activerecord (= 7.0.3) - activestorage (= 7.0.3) - activesupport (= 7.0.3) + actionmailbox (7.0.4) + actionpack (= 7.0.4) + activejob (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.3) - actionpack (= 7.0.3) - actionview (= 7.0.3) - activejob (= 7.0.3) - activesupport (= 7.0.3) + actionmailer (7.0.4) + actionpack (= 7.0.4) + actionview (= 7.0.4) + activejob (= 7.0.4) + activesupport (= 7.0.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.3) - actionview (= 7.0.3) - activesupport (= 7.0.3) + actionpack (7.0.4) + actionview (= 7.0.4) + activesupport (= 7.0.4) rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.3) - actionpack (= 7.0.3) - activerecord (= 7.0.3) - activestorage (= 7.0.3) - activesupport (= 7.0.3) + actiontext (7.0.4) + actionpack (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.3) - activesupport (= 7.0.3) + actionview (7.0.4) + activesupport (= 7.0.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.3) - activesupport (= 7.0.3) + activejob (7.0.4) + activesupport (= 7.0.4) globalid (>= 0.3.6) - activemodel (7.0.3) - activesupport (= 7.0.3) - activerecord (7.0.3) - activemodel (= 7.0.3) - activesupport (= 7.0.3) - activestorage (7.0.3) - actionpack (= 7.0.3) - activejob (= 7.0.3) - activerecord (= 7.0.3) - activesupport (= 7.0.3) + activemodel (7.0.4) + activesupport (= 7.0.4) + activerecord (7.0.4) + activemodel (= 7.0.4) + activesupport (= 7.0.4) + activestorage (7.0.4) + actionpack (= 7.0.4) + activejob (= 7.0.4) + activerecord (= 7.0.4) + activesupport (= 7.0.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.3) + activesupport (7.0.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -85,14 +85,13 @@ GEM addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) - airbrussh (1.5.2) + airbrussh (1.5.3) sshkit (>= 1.6.1, != 1.7.0) ast (2.4.2) autoprefixer-rails (10.4.19.0) execjs (~> 2) base64 (0.2.0) bcrypt_pbkdf (1.1.1) - bcrypt_pbkdf (1.1.1-x86_64-darwin) bigdecimal (3.1.8) bindata (2.5.0) bindex (0.8.1) @@ -220,7 +219,7 @@ GEM actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - inline_svg (1.9.0) + inline_svg (1.10.0) activesupport (>= 3.0) nokogiri (>= 1.6) io-console (0.7.2) @@ -260,7 +259,7 @@ GEM listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.0) + logger (1.6.1) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -288,7 +287,7 @@ GEM method_source (1.1.0) mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2024.0820) + mime-types-data (3.2024.0903) mini_mime (1.1.5) minitest (5.25.1) msgpack (1.7.2) @@ -301,7 +300,7 @@ GEM time net-http (0.3.2) uri - net-imap (0.4.14) + net-imap (0.4.16) date net-protocol net-pop (0.1.2) @@ -316,10 +315,8 @@ GEM net-protocol net-ssh (7.2.3) netrc (0.11.0) - newrelic_rpm (9.12.0) + newrelic_rpm (9.13.0) nio4r (2.7.3) - nokogiri (1.15.6-x86_64-darwin) - racc (~> 1.4) nokogiri (1.15.6-x86_64-linux) racc (~> 1.4) oauth2 (2.0.9) @@ -339,7 +336,7 @@ GEM omniauth-github (2.0.1) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) - omniauth-google-oauth2 (1.1.2) + omniauth-google-oauth2 (1.1.3) jwt (>= 2.0) oauth2 (~> 2.0) omniauth (~> 2.0) @@ -360,7 +357,7 @@ GEM omniauth (~> 2.0) ostruct (0.6.0) parallel (1.26.3) - parser (3.3.4.2) + parser (3.3.5.0) ast (~> 2.4.1) racc popper_js (1.16.1) @@ -379,20 +376,20 @@ GEM rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.3) - actioncable (= 7.0.3) - actionmailbox (= 7.0.3) - actionmailer (= 7.0.3) - actionpack (= 7.0.3) - actiontext (= 7.0.3) - actionview (= 7.0.3) - activejob (= 7.0.3) - activemodel (= 7.0.3) - activerecord (= 7.0.3) - activestorage (= 7.0.3) - activesupport (= 7.0.3) + rails (7.0.4) + actioncable (= 7.0.4) + actionmailbox (= 7.0.4) + actionmailer (= 7.0.4) + actionpack (= 7.0.4) + actiontext (= 7.0.4) + actionview (= 7.0.4) + activejob (= 7.0.4) + activemodel (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) bundler (>= 1.15.0) - railties (= 7.0.3) + railties (= 7.0.4) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -403,9 +400,9 @@ GEM rails-i18n (7.0.9) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.3) - actionpack (= 7.0.3) - activesupport (= 7.0.3) + railties (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) method_source rake (>= 12.2) thor (~> 1.0) @@ -421,45 +418,43 @@ GEM json redcarpet (3.6.0) regexp_parser (2.9.2) - reline (0.5.9) + reline (0.5.10) io-console (~> 0.5) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.3.6) - strscan + rexml (3.3.7) rouge (4.3.0) - rspec-core (3.13.0) + rspec-core (3.13.1) rspec-support (~> 3.13.0) - rspec-expectations (3.13.2) + rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.4) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) + rspec-rails (7.0.1) + actionpack (>= 7.0) + activesupport (>= 7.0) + railties (>= 7.0) rspec-core (~> 3.13) rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.1) - rubocop (1.65.1) + rubocop (1.66.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.4, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.1) + rubocop-ast (1.32.3) parser (>= 3.3.1.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) @@ -490,7 +485,7 @@ GEM simplecov-cobertura (2.1.0) rexml simplecov (~> 0.19) - simplecov-html (0.12.3) + simplecov-html (0.13.0) simplecov_json_formatter (0.1.4) snaky_hash (2.0.1) hashie @@ -503,23 +498,23 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sshkit (1.23.0) + sshkit (1.23.1) base64 net-scp (>= 1.1.2) net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) + ostruct stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.1) - strscan (3.1.0) temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) terser (1.2.3) execjs (>= 0.3.0, < 3) - thor (1.3.1) + thor (1.3.2) tilt (2.4.0) - time (0.3.0) + time (0.4.0) date timeout (0.4.1) turbo-rails (2.0.6) @@ -529,7 +524,7 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) - uri (0.13.0) + uri (0.13.1) version_gem (1.1.4) view_component (2.83.0) activesupport (>= 5.2.0, < 8.0) @@ -551,12 +546,10 @@ GEM will_paginate (3.3.1) xpath (3.2.0) nokogiri (~> 1.8) - yard (0.9.36) - zeitwerk (2.6.17) + yard (0.9.37) + zeitwerk (2.6.18) PLATFORMS - x86_64-darwin-23 - x86_64-linux x86_64-linux x86_64-linux-musl @@ -611,7 +604,7 @@ DEPENDENCIES ontologies_api_client! pry puma (~> 5.0) - rails (= 7.0.3) + rails (= 7.0.4) rails-i18n (~> 7.0.0) recaptcha (~> 5.9.0) rest-client @@ -632,4 +625,4 @@ DEPENDENCIES will_paginate (~> 3.0) BUNDLED WITH - 2.3.23 + 2.4.22 From 1eb77d2171960b794333861abce13a0d5594e7b1 Mon Sep 17 00:00:00 2001 From: Bilel KIHAL Date: Mon, 9 Sep 2024 12:38:58 +0200 Subject: [PATCH 14/45] remove limited container component from taxonomy page --- app/assets/stylesheets/taxonomy.scss | 13 +--------- .../display/limited_container_component.rb | 6 ----- .../limited_container_component.html.haml | 6 ----- .../limited_container_component_controller.js | 25 ------------------- .../taxonomy_card_component.html.haml | 5 ++-- app/javascript/component_controllers/index.js | 4 +-- 6 files changed, 4 insertions(+), 55 deletions(-) delete mode 100644 app/components/display/limited_container_component.rb delete mode 100644 app/components/display/limited_container_component/limited_container_component.html.haml delete mode 100644 app/components/display/limited_container_component/limited_container_component_controller.js diff --git a/app/assets/stylesheets/taxonomy.scss b/app/assets/stylesheets/taxonomy.scss index 2a9bf66eca..6043973aa8 100644 --- a/app/assets/stylesheets/taxonomy.scss +++ b/app/assets/stylesheets/taxonomy.scss @@ -98,15 +98,4 @@ background-color: var(--primary-color); } -.taxonomy-section { - .tab-content > .tab-pane { - visibility: hidden !important; - display: block !important; - position: absolute; - } - .tab-content > .active { - visibility: visible !important; - display: block; - position: unset; - } -} + diff --git a/app/components/display/limited_container_component.rb b/app/components/display/limited_container_component.rb deleted file mode 100644 index 62b290c83a..0000000000 --- a/app/components/display/limited_container_component.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Display::LimitedContainerComponent < ViewComponent::Base - renders_one :revealButton - def initialize(max_height: '90') - @max_height = max_height - end -end diff --git a/app/components/display/limited_container_component/limited_container_component.html.haml b/app/components/display/limited_container_component/limited_container_component.html.haml deleted file mode 100644 index ee52898915..0000000000 --- a/app/components/display/limited_container_component/limited_container_component.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -%div{'data-controller': 'limited-container', 'data-limited-container-height-value': @max_height} - .limited-container-component{'data-limited-container-target': "content", style: "max-height: #{@max_height}px; overflow: hidden;"} - = content - - .reveal-button{'data-limited-container-target': "revealbutton", 'data-action': "click->limited-container#toggle"} - = revealButton \ No newline at end of file diff --git a/app/components/display/limited_container_component/limited_container_component_controller.js b/app/components/display/limited_container_component/limited_container_component_controller.js deleted file mode 100644 index 6cae826929..0000000000 --- a/app/components/display/limited_container_component/limited_container_component_controller.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Controller } from '@hotwired/stimulus' - -// Connects to data-controller="limited-container" - -export default class extends Controller { - static targets = [ "content", "revealbutton"] - static values = { - height: Number - } - connect(){ - this.contentTarget - if(this.contentTarget.offsetHeightreveal-component#toggle", 'data-id': reveal_id} .text diff --git a/app/javascript/component_controllers/index.js b/app/javascript/component_controllers/index.js index 72893043fd..c0d3bcd20f 100644 --- a/app/javascript/component_controllers/index.js +++ b/app/javascript/component_controllers/index.js @@ -25,7 +25,6 @@ import Table_component_controller from '../../components/table_component/table_c import clipboard_component_controller from '../../components/clipboard_component/clipboard_component_controller' import range_slider_component_controller from '../../components/input/range_slider_component/range_slider_component_controller' import RDFHighlighter from '../../components/display/rdf_highlighter_component/rdf_highlighter_component_controller' -import limited_container_component_controller from '../../components/display/limited_container_component/limited_container_component_controller' application.register("rdf-highlighter", RDFHighlighter) application.register('turbo-modal', TurboModalController) @@ -42,5 +41,4 @@ application.register('progress-pages', Progress_pages_component_controller) application.register('reveal-component', Reveal_component_controller) application.register('table-component', Table_component_controller) application.register('clipboard', clipboard_component_controller) -application.register('range-slider', range_slider_component_controller) -application.register('limited-container', limited_container_component_controller) +application.register('range-slider', range_slider_component_controller) \ No newline at end of file From 4cdc1aaefb84c1e8609e88e4ec70041e82631abb Mon Sep 17 00:00:00 2001 From: Bilel KIHAL Date: Wed, 11 Sep 2024 10:21:14 +0200 Subject: [PATCH 15/45] limite taxonomy card ontologies chips number and add a see all button --- .../taxonomy_card_component.html.haml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml b/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml index 05903f4f4d..ab55aa41de 100644 --- a/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml +++ b/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml @@ -11,9 +11,12 @@ .description = render TextAreaFieldComponent.new(value: @taxonomy.description) .ontologies-cards - - @taxonomy.ontologies.each do |ontology| + - @taxonomy.ontologies.each_with_index do |ontology, index| + - if index>10 + = render ChipButtonComponent.new(url: "/ontologies?#{@taxonomy.id.split('/')[-2]}=#{@taxonomy.acronym}", text: "...", tooltip:"See all ontologies ...", type: "clickable") + - break = render ChipButtonComponent.new(url: "/ontologies/#{ontology.split('/').last}", text: ontology.split('/').last, tooltip: @ontologies_names[ontology], type: "clickable") - + - if @taxonomy.children .taxonomy-children-reveal{'data-action': "click->reveal-component#toggle", 'data-id': reveal_id} .text From 6fe569349889caf117caaf9ae9179c5eee27191f Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Wed, 11 Sep 2024 15:11:12 +0200 Subject: [PATCH 16/45] Feature: Federate browse page (#621) * simplify the header component to use content instead of a new section * update dropdown component to a custom title section instead of text * update browse page to use Dropdown component not bootsrap one * add color option to square badge component * add text and bg colors options to ontology browse card component * add federation helper code and config sample * add portals filters in the browse page * use federation helpers to get federation ontologies information * remove the filter using index code from submission filter as no used * refactor the filter_using_data function to be faster by using an hash * update the browse analytics cache to change depending on portals * add categories and groups ids on hover to know its origin when federated * use the last part of ids for browse counts independently of its origin * remove binding.pry from final federation code * add an error message if one of the external portal is down * remove $FEDERATED_PORTALS and use existent $PORTALS_INSTANCES variable * handle the merging of the same ontology from different portals * fix groups and categories selection at browse page init --- Gemfile | 2 +- Gemfile.lock | 12 ++- .../stylesheets/components/chip_button.scss | 1 + app/components/display/header_component.rb | 3 +- .../dropdown_container_component.rb | 5 +- .../dropdown_container_component.html.haml | 5 +- .../ontology_browse_card_component.rb | 30 +++++- .../ontology_browse_card_component.html.haml | 48 ++++++--- app/components/square_badge_component.rb | 6 +- app/controllers/application_controller.rb | 3 + app/controllers/concerns/submission_filter.rb | 100 ++++++++++-------- app/controllers/ontologies_controller.rb | 9 +- app/helpers/application_helper.rb | 2 +- app/helpers/federation_helper.rb | 97 +++++++++++++++++ app/views/home/index.html.haml | 4 +- app/views/home/tools.html.haml | 9 +- .../ontologies/browser/_ontologies.html.haml | 18 +++- app/views/ontologies/browser/browse.html.haml | 9 +- .../ontologies/sections/_metadata.html.haml | 16 ++- config/bioportal_config_env.rb.sample | 62 +++++++---- config/initializers/ontologies_api_client.rb | 1 + config/locales/en.yml | 2 +- config/locales/fr.yml | 2 +- 23 files changed, 324 insertions(+), 122 deletions(-) create mode 100644 app/helpers/federation_helper.rb diff --git a/Gemfile b/Gemfile index c40e3640c5..781e6379e2 100644 --- a/Gemfile +++ b/Gemfile @@ -96,7 +96,7 @@ gem 'flag-icons-rails', '~> 3.4' gem 'iso-639', '~> 0.3.6' # Custom API client -gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'master' +gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'development' # Ruby 2.7.8 pinned gems (to remove when migrating to Ruby >= 3.0) gem 'ffi', '~> 1.16.3' diff --git a/Gemfile.lock b/Gemfile.lock index 51f97081af..2c0afc74bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,10 @@ GIT remote: https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git - revision: 24fb2549f7b69841e052491439bc8375ed5acfd9 - branch: master + revision: 15ad7d41d9a414ed3570c073c2b75a3fb4cc19cf + branch: development specs: ontologies_api_client (2.2.0) - activesupport + activesupport (~> 7.0.4) excon faraday faraday-excon (~> 2.0.0) @@ -12,6 +12,8 @@ GIT lz4-ruby multi_json oj + parallel + request_store spawnling (= 2.1.5) GEM @@ -420,6 +422,8 @@ GEM regexp_parser (2.9.2) reline (0.5.10) io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) @@ -485,7 +489,7 @@ GEM simplecov-cobertura (2.1.0) rexml simplecov (~> 0.19) - simplecov-html (0.13.0) + simplecov-html (0.13.1) simplecov_json_formatter (0.1.4) snaky_hash (2.0.1) hashie diff --git a/app/assets/stylesheets/components/chip_button.scss b/app/assets/stylesheets/components/chip_button.scss index 1b19a29815..18916b7fec 100644 --- a/app/assets/stylesheets/components/chip_button.scss +++ b/app/assets/stylesheets/components/chip_button.scss @@ -10,6 +10,7 @@ } .chip-button-component-container { display: inline-block; + text-wrap: nowrap !important; } .chip-button-component-container svg path{ fill: var(--primary-color); diff --git a/app/components/display/header_component.rb b/app/components/display/header_component.rb index 76784068e1..d8b8ecebd0 100644 --- a/app/components/display/header_component.rb +++ b/app/components/display/header_component.rb @@ -4,7 +4,6 @@ class Display::HeaderComponent < ViewComponent::Base include ComponentsHelper - renders_one :text def initialize(text: nil, tooltip: nil) super @@ -14,7 +13,7 @@ def initialize(text: nil, tooltip: nil) def call content_tag(:div, class: 'header-component') do - out = content_tag(:p, text || @text) + out = content_tag(:p, content&.html_safe || @text) if @info && !@info.empty? out = out + info_tooltip(content_tag(:div, @info, style: 'max-width: 300px')) end diff --git a/app/components/dropdown_container_component.rb b/app/components/dropdown_container_component.rb index e06b3fa7ac..f9d8ce38d2 100644 --- a/app/components/dropdown_container_component.rb +++ b/app/components/dropdown_container_component.rb @@ -2,13 +2,16 @@ class DropdownContainerComponent < ViewComponent::Base renders_one :empty_state - def initialize(title:, id:, tooltip:nil, is_open: false) + renders_one :title + + def initialize(title: nil, id:, tooltip:nil, is_open: false) super @title = title @id = id @tooltip = tooltip @is_open = is_open end + def open_class @is_open ? "show" : "" end diff --git a/app/components/dropdown_container_component/dropdown_container_component.html.haml b/app/components/dropdown_container_component/dropdown_container_component.html.haml index 7b9d4965e8..398f3199a0 100644 --- a/app/components/dropdown_container_component/dropdown_container_component.html.haml +++ b/app/components/dropdown_container_component/dropdown_container_component.html.haml @@ -1,6 +1,9 @@ .dropdown-container .dropdown-title-bar{"data-toggle" => "collapse", "data-target" => "##{@id}"} - = render Display::HeaderComponent.new(text: @title, tooltip: @tooltip) + - if title? + = title + - else + = render Display::HeaderComponent.new(text: @title, tooltip: @tooltip) = image_tag("summary/arrow-down.svg", class: 'ml-2') .collapse{id: @id, class: open_class} diff --git a/app/components/ontology_browse_card_component.rb b/app/components/ontology_browse_card_component.rb index 84251d7f0d..dfc2c05d0e 100644 --- a/app/components/ontology_browse_card_component.rb +++ b/app/components/ontology_browse_card_component.rb @@ -1,14 +1,40 @@ # frozen_string_literal: true class OntologyBrowseCardComponent < ViewComponent::Base - include OntologiesHelper + include ApplicationHelper, OntologiesHelper, FederationHelper - def initialize(ontology: nil) + def initialize(ontology: nil, onto_link: nil, text_color: nil, bg_light_color: nil, portal_name: nil) super @ontology = ontology + @text_color = text_color + @bg_light_color = bg_light_color + @onto_link = onto_link || "/ontologies/#{@ontology[:acronym]}" if @ontology + @portal_name = portal_name end def ontology @ontology end + + def external_ontology? + !internal_ontology?(@ontology[:id]) || (Array(@ontology[:sources]).size > 1) + end + + def onto_link + @onto_link + end + + def style_text + external_ontology? ? "color: #{@text_color} !important" : '' + end + + def portal_color + @text_color + end + alias :color :portal_color + + def style_bg + external_ontology? ? "#{style_text} ; background-color: #{@bg_light_color}" : '' + end + end diff --git a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml index aaf97e85fd..6a08ccc836 100644 --- a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml +++ b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml @@ -3,9 +3,11 @@ .d-flex .browse-ontology-description .browse-ontology-title-bar - %a.browse-ontology-title{:href => "/ontologies/#{ontology[:acronym]}", data: {'turbo': 'false'}} + %a.browse-ontology-title{:href => onto_link, data: {'turbo': 'false'} , style: style_text} = ontology[:name]+" ("+ontology[:acronym]+")" = private_ontology_icon(ontology[:private]) + - if external_ontology? + = render Display::InfoTooltipComponent.new(text: "Federated ontology from #{ontology[:sources].map{|x| link_to(x,x)}.join(', ')}", icon: 'external-link.svg') - if session[:user]&.admin? - ontology_status = status_string(ontology) = render Display::InfoTooltipComponent.new(text: ontology_status, icon: submission_status_icons(ontology_status)) @@ -21,27 +23,32 @@ %p.browse-fair-title = t('components.fair_score') .browse-progress-bar - .browse-faire-progress{:style => "width: #{ontology[:normalizedFairScore].to_s+"%"}"} + .browse-faire-progress{:style => "width: #{ontology[:normalizedFairScore].to_s+"%"}; #{color ? 'background-color:' + color : ''}"} %p.browse-fair-score = ontology[:fairScore] - %a.browse-fair-details{:href => "/ontologies/#{ontology[:acronym]}#fair-details", 'data-turbo': 'false'}= t('components.details_details') + %a.browse-fair-details{:href => "#{onto_link}#fair-details", 'data-turbo': 'false', style: style_text}= t('components.details_details') - .browse-ontology-cards - = render SquareBadgeComponent.new(label: t('components.classes'), count: ontology[:class_count_formatted], link: "/ontologies/#{ontology[:acronym]}?p=classes" ) + .browse-ontology-cards{style: color ? "color: #{color} !important" : ''} + = render SquareBadgeComponent.new(label: t('components.classes'), + count: ontology[:class_count_formatted], + link: "#{onto_link}?p=classes", + color: color) = render SquareBadgeComponent.new(label: ontology[:format] == 'SKOS' ? t('components.concepts') : t('components.instances'), - count: ontology[:individual_count_formatted], - link: "/ontologies/#{ontology[:acronym]}?p=#{ontology[:format] == 'SKOS' ? "classes" : "instances"}") + count: ontology[:individual_count_formatted], color: color, + link: "#{onto_link}?p=#{ontology[:format] == 'SKOS' ? "classes" : "instances"}") - = render SquareBadgeComponent.new(label: t('components.projects'), count: ontology[:project_count], link: "/ontologies/#{ontology[:acronym]}#projects_section" ) + = render SquareBadgeComponent.new(label: t('components.projects'), count: ontology[:project_count], color: color, + link: "#{onto_link}#projects_section" ) - = render SquareBadgeComponent.new(label: t('components.notes'), count: ontology[:note_count], link: "/ontologies/#{ontology[:acronym]}?p=notes" ) + = render SquareBadgeComponent.new(label: t('components.notes'), count: ontology[:note_count], color: color, + link: "#{onto_link}?p=notes" ) - .d-flex.align-items-baseline.mt-1 + .d-flex.w-100.mt-1.flex-wrap - if ontology[:creationDate] %span.mr-1 - = render ChipButtonComponent.new(type: "clickable") do + = render ChipButtonComponent.new(type: "clickable", style: style_bg) do %span.mr-1= t('components.submitted') %span.browse-uploaded-date{data:{controller: 'timeago', 'timeago-datetime-value': ontology[:creationDate], 'timeago-add-suffix-value': 'true'}} - if ontology[:contact] @@ -52,19 +59,32 @@ - if ontology[:released] - date = render DateTimeFieldComponent.new(value: ontology[:released]) %span{data:{controller:'tooltip'}, title: t('components.creation_date', date: date)} - = render ChipButtonComponent.new(type: "clickable") do + = render ChipButtonComponent.new(type: "clickable", style: style_bg) do = DateTime.parse(date).year rescue date - if ontology[:format] %span.mx-1 - = render ChipButtonComponent.new(type: "clickable") do + = render ChipButtonComponent.new(type: "clickable", style: style_bg) do = ontology[:format] - if ontology_retired?(ontology) %span.mx-1 = ontology_retired_badge(ontology) - if ontology[:viewOfOnt] %span.mx-1{data:{controller:'tooltip'}, title: t('components.view_of_the_ontology', ontology: ontology[:viewOfOnt].split('/').last )} - = render ChipButtonComponent.new(type: "clickable", text: t('components.view')) + = render ChipButtonComponent.new(type: "clickable", text: t('components.view'), style: style_bg) + + - if external_ontology? + - ontology[:sources].each do |id| + - config = ontology_portal_config(id)&.last || internal_portal_config(id) || {} + - unless config.blank? + %div.mx-1{title: content_tag(:div, "Source #{config[:name]}"), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} + = render ChipButtonComponent.new(type: "clickable" , style: style_bg) do + = link_to ontoportal_ui_link(id), target: '_top' do + %span.d-inline + %span.mr-1 + = inline_svg 'logo-white.svg', width: "20", height: "20" + %span + = config[:name] - if session[:user]&.admin? %div.mx-1{title: content_tag(:div, debug(ontology), style: 'height: 300px; overflow: scroll'), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} diff --git a/app/components/square_badge_component.rb b/app/components/square_badge_component.rb index 9d85a8c34f..bfbc95c6db 100644 --- a/app/components/square_badge_component.rb +++ b/app/components/square_badge_component.rb @@ -2,15 +2,17 @@ class SquareBadgeComponent < ViewComponent::Base - def initialize(label: , count: ,link: nil) + def initialize(label: , count: ,link: nil, color: nil) @label = label @count = count @link = link + @color = color end + def call return if @count.to_i.zero? - link_to(@link, class: 'browse-onology-card', 'data-turbo' => 'false') do + link_to(@link, class: 'browse-onology-card', 'data-turbo' => 'false', style: @color ? "color: #{@color} !important; border-color: #{@color}" : "") do concat(content_tag(:p, @count, class: 'browse-card-number')) concat(content_tag(:p, @label, class: 'browse-card-text')) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f75a395b75..291e2ae33d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -175,6 +175,9 @@ def bp_config_json def rest_url helpers.rest_url end + def request_portals + helpers.request_portals + end def parse_response_body(response) return nil if response.nil? diff --git a/app/controllers/concerns/submission_filter.rb b/app/controllers/concerns/submission_filter.rb index 45746e36e2..b37beeb8d4 100644 --- a/app/controllers/concerns/submission_filter.rb +++ b/app/controllers/concerns/submission_filter.rb @@ -1,7 +1,7 @@ module SubmissionFilter extend ActiveSupport::Concern - include SearchContent + include FederationHelper BROWSE_ATTRIBUTES = ['ontology', 'submissionStatus', 'description', 'pullLocation', 'creationDate', 'contact', 'released', 'naturalLanguage', 'hasOntologyLanguage', @@ -21,17 +21,19 @@ def submissions_paginate_filter(params) filter_params = params.permit(@filters.keys).to_h init_filters(params) - @analytics = Rails.cache.fetch("ontologies_analytics-#{Time.now.year}-#{Time.now.month}") do + @analytics = Rails.cache.fetch("ontologies_analytics-#{Time.now.year}-#{Time.now.month}-#{request_portals.join('-')}") do helpers.ontologies_analytics end @ontologies = LinkedData::Client::Models::Ontology.all(include: 'all', also_include_views: true, display_links: false, display_context: false) + @ontologies, @errors = @ontologies.partition { |x| !x.errors } + # get fair scores of all ontologies @fair_scores = fairness_service_enabled? ? get_fair_score('all') : nil @total_ontologies = @ontologies.size - search_backend = params[:search_backend] + params = { query: @search, status: request_params[:status], show_views: @show_views, @@ -43,16 +45,12 @@ def submissions_paginate_filter(params) groups: request_params[:group], categories: request_params[:hasDomain], formats: request_params[:hasOntologyLanguage] } + submissions = filter_submissions(@ontologies, **params) - if search_backend.eql?('index') - submissions = filter_using_index(**params) - submissions = @ontologies.map{ |ont| ontology_hash(ont, submissions) } - else - submissions = filter_using_data(@ontologies, **params) - end + submissions = merge_by_acronym(submissions) - submissions = sort_submission_by(submissions, @sort_by, @search) + submissions = sort_submission_by(submissions, @sort_by, @search) @page = paginate_submissions(submissions, request_params[:page].to_i, request_params[:pagesize].to_i) @@ -67,49 +65,51 @@ def ontologies_with_filters_url(filters, page: 1, count: false) private - def filter_using_index(query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) - search_ontologies( - query: query, - status: status, - show_views: show_views, - private_only: private_only, - languages: languages, - page_size: page_size, - formality_level: formality_level, - is_of_type: is_of_type, - groups: groups, categories: categories, - formats: formats - ) + def merge_by_acronym(submissions) + merged_submissions = [] + submissions.group_by { |x| x[:ontology]&.acronym }.each do |acronym, ontologies| + if ontologies.size.eql?(1) + ontology = ontologies.first + else + ontology = ontologies.select { |x| helpers.internal_ontology?(x[:id]) }.first || ontologies.first + end + ontology[:sources] = ontologies.map { |x| x[:id] } + merged_submissions << ontology + end + merged_submissions end - def filter_using_data(ontologies, query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) + def filter_submissions(ontologies, query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) submissions = LinkedData::Client::Models::OntologySubmission.all(include: BROWSE_ATTRIBUTES.join(','), also_include_views: true, display_links: false, display_context: false) + + submissions = submissions.map { |x| x[:ontology] ? [x[:ontology][:id], x] : nil }.compact.to_h + submissions = ontologies.map { |ont| ontology_hash(ont, submissions) } submissions.map do |s| - out = ((s.ontology.viewingRestriction.eql?('public') && !private_only) || private_only && s.ontology.viewingRestriction.eql?('private')) - out = out && (groups.blank? || (s.ontology.group.map { |x| helpers.link_last_part(x) } & groups.split(',')).any?) - out = out && (categories.blank? || (s.ontology.hasDomain.map { |x| helpers.link_last_part(x) } & categories.split(',')).any?) - out = out && (status.blank? || status.eql?('alpha,beta,production,retired') || status.split(',').include?(s.status)) - out = out && (formats.blank? || formats.split(',').any? { |f| s.hasOntologyLanguage.eql?(f) }) - out = out && (is_of_type.blank? || is_of_type.split(',').any? { |f| helpers.link_last_part(s.isOfType).eql?(f) }) - out = out && (formality_level.blank? || formality_level.split(',').any? { |f| helpers.link_last_part(s.hasFormalityLevel).eql?(f) }) - out = out && (languages.blank? || languages.split(',').any? { |f| s.naturalLanguage.any? { |n| helpers.link_last_part(n).eql?(f) } }) - out = out && (s.ontology.viewOf.blank? || (show_views && !s.ontology.viewOf.blank?)) - - out = out && (query.blank? || [s.description, s.ontology.name, s.ontology.acronym].any? { |x| (x|| '').downcase.include?(query.downcase) }) + out = ((s[:ontology].viewingRestriction.eql?('public') && !private_only) || private_only && s[:ontology].viewingRestriction.eql?('private')) + out = out && (groups.blank? || (s[:ontology].group.map { |x| helpers.link_last_part(x) } & groups.split(',')).any?) + out = out && (categories.blank? || (s[:ontology].hasDomain.map { |x| helpers.link_last_part(x) } & categories.split(',')).any?) + out = out && (status.blank? || status.eql?('alpha,beta,production,retired') || status.split(',').include?(s[:status])) + out = out && (formats.blank? || formats.split(',').any? { |f| s[:hasOntologyLanguage].eql?(f) }) + out = out && (is_of_type.blank? || is_of_type.split(',').any? { |f| helpers.link_last_part(s[:isOfType]).eql?(f) }) + out = out && (formality_level.blank? || formality_level.split(',').any? { |f| helpers.link_last_part(s[:hasFormalityLevel]).eql?(f) }) + out = out && (languages.blank? || languages.split(',').any? { |f| Array(s[:naturalLanguage]).any? { |n| helpers.link_last_part(n).eql?(f) } }) + out = out && (s[:ontology].viewOf.blank? || (show_views && !s[:ontology].viewOf.blank?)) + + out = out && (query.blank? || [s[:description], s[:ontology].name, s[:ontology].acronym].any? { |x| (x || '').downcase.include?(query.downcase) }) if out s[:rank] = 0 next s if query.blank? - s[:rank] += 3 if s.ontology.acronym && s.ontology.acronym.downcase.include?(query.downcase) + s[:rank] += 3 if s[:ontology].acronym && s[:ontology].acronym.downcase.include?(query.downcase) - s[:rank] += 2 if s.ontology.name && s.ontology.name.downcase.include?(query.downcase) + s[:rank] += 2 if s[:ontology].name && s[:ontology].name.downcase.include?(query.downcase) - s[:rank] += 1 if s.description && s.description.downcase.include?(query.downcase) + s[:rank] += 1 if s[:description] && s[:description].downcase.include?(query.downcase) s else @@ -131,7 +131,7 @@ def paginate_submissions(all_submissions, page, size) end def sort_submission_by(submissions, sort_by, query = nil) - return submissions.sort_by { |x| x[:rank] ? -x[:rank] : 0} unless query.blank? + return submissions.sort_by { |x| x[:rank] ? -x[:rank] : 0 } unless query.blank? if sort_by.eql?('visits') submissions = submissions.sort_by { |x| -(x[:popularity] || 0) } @@ -209,13 +209,17 @@ def filters_params(params, includes: BROWSE_ATTRIBUTES.join(','), page: 1, pages @filters[:search] = params[:search] end + unless params[:portals].blank? + @filters[:portals] = params[:portals] + end + request_params.delete(:order_by) if %w[visits fair].include?(request_params[:sort_by].to_s) request_params end def ontology_hash(ont, submissions) o = {} - sub = submissions.select{|x| x.ontology&.id.eql?(ont.id)}.first + sub = submissions[ont.id] o[:ontology] = ont @@ -225,7 +229,7 @@ def ontology_hash(ont, submissions) o[:hasOntologyLanguage] = sub&.hasOntologyLanguage - if sub&.metrics + if sub&.metrics && !sub.metrics.is_a?(String) o[:class_count] = sub.metrics.classes o[:individual_count] = sub.metrics.individuals else @@ -237,10 +241,10 @@ def ontology_hash(ont, submissions) o[:note_count] = ont.notes&.length || 0 o[:project_count] = ont.projects&.length || 0 - o[:popularity] = @analytics[ont.acronym] || 0 - o[:rank] = sub&[:rank] || 0 + o[:popularity] = @analytics[ont.id.split('/').last.to_s] || 0 + o[:rank] = sub ? sub[:rank] : 0 - OpenStruct.new(o) + o end def add_submission_attributes(ont_hash, sub) @@ -346,10 +350,12 @@ def check_id(name_value, objects, name_key) def object_filter(objects, object_name, name_key = 'acronym') checks = params[object_name]&.split(',') || [] - checks = checks.map { |x| check_id(x, objects, name_key) }.compact + checks = checks.map { |x| helpers.link_last_part(check_id(x, objects, name_key)) }.compact - ids = objects.map { |x| x['id'] } + objects.uniq! { |x| helpers.link_last_part(x['id']) } + ids = objects.map { |x| helpers.link_last_part(x['id']) } count = ids.count { |x| checks.include?(x) } + [objects, checks, count] end @@ -370,7 +376,7 @@ def count_objects(ontologies) object_names.each do |name| values = Array(ontology[name]) values.each do |v| - v.gsub!('http://data.bioontology.org', rest_url) + v = helpers.link_last_part(v) objects_count[name] = {} unless objects_count[name] objects_count[name][v] = (objects_count[name][v] || 0) + 1 diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index a8c744fe4a..11d0ae2ca5 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -26,6 +26,8 @@ class OntologiesController < ApplicationController before_action :authorize_and_redirect, :only => [:edit, :update, :create, :new] before_action :submission_metadata, only: [:show] + before_action :set_federated_portals, only: [:index, :ontologies_filter] + KNOWN_PAGES = Set.new(["terms", "classes", "mappings", "notes", "widgets", "summary", "properties", "instances", "schemes", "collections", "sparql"]) EXTERNAL_MAPPINGS_GRAPH = "http://data.bioontology.org/metadata/ExternalMappings" INTERPORTAL_MAPPINGS_GRAPH = "http://data.bioontology.org/metadata/InterportalMappings" @@ -49,8 +51,8 @@ def ontologies_filter streams = [prepend("ontologies_list_view-page-#{@page.page}", partial: 'ontologies/browser/ontologies')] streams += @count_objects.map do |section, values_count| values_count.map do |value, count| - replace("count_#{section}_#{value}") do - helpers.turbo_frame_tag("count_#{section}_#{value}") do + replace("count_#{section}_#{link_last_part(value)}") do + helpers.turbo_frame_tag("count_#{section}_#{link_last_part(value)}") do helpers.content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") end end @@ -568,4 +570,7 @@ def search_first_instance_id return !results.blank? ? results.first[:name] : nil end + def set_federated_portals + RequestStore.store[:federated_portals] = params[:portals]&.split(',') + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c9fb873758..c134d5d96c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -41,7 +41,7 @@ def resolve_namespaces def ontologies_analytics begin data = LinkedData::Client::Analytics.last_month.onts - data.map{|x| [x[:ont].to_s, x[:views]]}.to_h + data.map{|x| [x[:ont].split('/').last.to_s, x[:views]]}.to_h rescue StandardError {} end diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb new file mode 100644 index 0000000000..afd60dfa00 --- /dev/null +++ b/app/helpers/federation_helper.rb @@ -0,0 +1,97 @@ +module FederationHelper + + def federated_portals + $FEDERATED_PORTALS ||= LinkedData::Client.settings.federated_portals + end + + def internal_portal_config(id) + return unless internal_ontology?(id) + + { + name: portal_name, + api: rest_url, + apikey: $API_KEY, + ui: $UI_URL, + color: "var(--primary-color)", + 'light-color': 'var(--light-color)', + } + end + + def federated_portal_config(name_key) + federated_portals[name_key.to_sym] + end + + def federated_portal_name(key) + config = federated_portal_config(key) + config ? config[:name] : key + end + + def federated_portal_color(key) + config = federated_portal_config(key) + config[:color] if config + end + + def federated_portal_light_color(key) + config = federated_portal_config(key) + config[:'light-color'] if config + end + + + def ontology_portal_config(id) + rest_url = id.split('/')[0..-3].join('/') + federated_portals.select{|_, config| config[:api].start_with?(rest_url)}.first + end + + def ontology_portal_name(id) + portal_key, _ = ontology_portal_config(id) + portal_key ? federated_portal_name(portal_key) : 'not found' + end + + + def ontology_portal_color(id) + portal_key, _ = ontology_portal_config(id) + federated_portal_color(portal_key) if portal_key + end + + + def ontoportal_ui_link(id) + portal_key, config = ontology_portal_config(id) + return nil unless portal_key + + ui_link = config[:ui] + api_link = config[:api] + + id.gsub(api_link, "#{ui_link}/") rescue id + end + + def internal_ontology?(id) + id.start_with?(rest_url) + end + + def federated_ontology?(id) + !internal_ontology?(id) + end + + def request_portals + portals = RequestStore.store[:federated_portals] || [] + [portal_name] + portals + end + + def request_portals_names + request_portals.map do |x| + config = federated_portal_config(x) + + if config + name = config[:name] + color = config[:color] + elsif portal_name.downcase.eql?(x.downcase) + name = portal_name + color = nil + else + next nil + end + + content_tag(:span, federated_portal_name(name), style: color ? "color: #{color}" : "", class: color ? "" : "text-primary") + end.compact + end +end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 83fc781581..be0075bfe3 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -214,10 +214,10 @@ .home-support-items - $PORTALS_INSTANCES&.each do |portal| %div.text-center - = link_to portal[:link], target: '_blank', class: 'home-logo-instances', style: "background-color: #{portal[:color]}" do + = link_to portal[:ui], target: '_blank', class: 'home-logo-instances', style: "background-color: #{portal[:color]}" do = inline_svg 'logo-white.svg', width: "35", height: "26" %p{style: "color: #{portal[:color]}"} - = portal[:portal] + = portal[:name] .home-section .home-section-title diff --git a/app/views/home/tools.html.haml b/app/views/home/tools.html.haml index 941d60ec74..d4f0455b69 100644 --- a/app/views/home/tools.html.haml +++ b/app/views/home/tools.html.haml @@ -3,10 +3,9 @@ = link_to tool[:link], class: "mx-2 d-block", style: "width: 35%" do = render Layout::CardComponent.new do |c| - c.header do |h| - - h.text do - %div.d-flex.flex-column.align-items-center.text-primary.pt-4 - = inline_svg_tag(tool[:icon], height: "50px", width: "50px") - %h3.text-primary.mt-2 - = tool[:title] + %div.d-flex.flex-column.align-items-center.text-primary.pt-4 + = inline_svg_tag(tool[:icon], height: "50px", width: "50px") + %h3.text-primary.mt-2 + = tool[:title] %p.px-4.tool-description = tool[:description] diff --git a/app/views/ontologies/browser/_ontologies.html.haml b/app/views/ontologies/browser/_ontologies.html.haml index bd3aed0cf5..3ca8685bc0 100644 --- a/app/views/ontologies/browser/_ontologies.html.haml +++ b/app/views/ontologies/browser/_ontologies.html.haml @@ -4,11 +4,23 @@ current_page: @page.page, next_page: @page.nextPage) do |c| - if @page.page.eql?(1) - = content_tag(:p, class: "browse-desc-text", style: "margin-bottom: 12px !important;") { "#{t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies)} (#{sprintf("%.2f", @time)}s)" } - + = content_tag(:p, class: "browse-desc-text", style: "margin-bottom: 12px !important;") do + #{t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies, portals: request_portals_names.join(', ').html_safe).html_safe} (#{sprintf("%.2f", @time)}s) + - unless @errors.blank? + %div.my-1 + = render Display::AlertComponent.new(type: 'danger') do + - @errors.each do |e| + %div + = e.errors || e - ontologies = c.collection - ontologies.each do |ontology| - = render OntologyBrowseCardComponent.new(ontology: ontology) + - config = ontology_portal_config(ontology[:id])&.last || {} + + = render OntologyBrowseCardComponent.new(ontology: ontology, + portal_name: config[:name], + onto_link: ontoportal_ui_link(ontology[:id]), + text_color: config[:color], + bg_light_color: config[:'light-color']) - c.loader do - ontologies_browse_skeleton - c.error do diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index d1979fd359..fd9777e3a8 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -65,12 +65,17 @@ .browse-filter-checks-container - values.first.each do |object| - title = (key.eql?(:categories) || key.eql?(:groups)) ? nil : '' - = group_chip_component(name: key, object: object, checked: values[1]&.include?(object["id"]) || values[1]&.include?(object["value"]) , title: title) do |c| + = group_chip_component(name: key, object: object, checked: values[1].any?(link_last_part(object["id"])) || values[1].any?(link_last_part(object["value"])) , title: title) do |c| - c.count do %span.badge.badge-light.ml-1 - = turbo_frame_tag "count_#{key}_#{object["id"]}", busy: true + = turbo_frame_tag "count_#{key}_#{link_last_part(object["id"])}", busy: true %span.show-if-loading = render LoaderComponent.new(small:true) + %div{data:{controller: "show-filter-count browse-filters", action: "change->show-filter-count#updateCount change->browse-filters#dispatchFilterEvent"}, id: "portals_filter_container"} + = render DropdownContainerComponent.new(id: "browse-portal-filter", is_open: !request_portals.empty?, title: "Results from external portals") do + .browse-filter-checks-container.px-1 + - federated_portals.each do |key, config| + = group_chip_component(name: "portals", object: { "acronym" => config[:name], "value" => key }, checked: request_portals.include?(key.to_s), title: '') .browse-second-row .browse-search-bar diff --git a/app/views/ontologies/sections/_metadata.html.haml b/app/views/ontologies/sections/_metadata.html.haml index 69aadbc570..be2083e5cb 100755 --- a/app/views/ontologies/sections/_metadata.html.haml +++ b/app/views/ontologies/sections/_metadata.html.haml @@ -22,7 +22,7 @@ = properties_dropdown('person_and_organization',t("ontologies.sections.person_and_organization"),'', @agents_properties) do |values| = horizontal_list_container(values) do |v| = agent_chip_component(v) - + = properties_dropdown('link',t("ontologies.sections.other_links"), t("ontologies.sections.info_tooltip_links") , @links_properties) do |values| = horizontal_list_container(values) do |v| = render LinkFieldComponent.new(value: v, raw: true) @@ -51,11 +51,10 @@ = Array(@content_properties['metadataVoc']).map{|x| metadata_vocabulary_display(x)}.join.html_safe = render Layout::CardComponent.new do |c| - c.header do |h| - - h.text do - = t("ontologies.sections.visits") - - if visits_data(@ontology) - = link_to(@ontology.links["analytics"] + "?apikey=#{get_apikey}&format=csv", title: t("ontologies.sections.download_as_csv")) do - = inline_svg("summary/download.svg", width: '30px', height: '20px') + = t("ontologies.sections.visits") + - if visits_data(@ontology) + = link_to(@ontology.links["analytics"] + "?apikey=#{get_apikey}&format=csv", title: t("ontologies.sections.download_as_csv")) do + = inline_svg("summary/download.svg", width: '30px', height: '20px') = render Layout::ListComponent.new do |l| - l.row do @@ -64,9 +63,8 @@ - unless @ontology.view? = render Layout::CardComponent.new do |d| - d.header do |h| - - h.text do - = t("ontologies.sections.views", acronym: @ontology.acronym) - = new_element_link(t("ontologies.sections.create_new_view"), new_view_path(@ontology.id)) + = t("ontologies.sections.views", acronym: @ontology.acronym) + = new_element_link(t("ontologies.sections.create_new_view"), new_view_path(@ontology.id)) = render Layout::ListComponent.new do |l| - l.row do = render partial: 'ontology_views' diff --git a/config/bioportal_config_env.rb.sample b/config/bioportal_config_env.rb.sample index 277b972015..352805b196 100644 --- a/config/bioportal_config_env.rb.sample +++ b/config/bioportal_config_env.rb.sample @@ -1,3 +1,6 @@ +$DEBUG_API_CLIENT = false +$API_CLIENT_INVALIDATE_CACHE = false + # Organization info $ORG = ENV['ORG'] $ORG_URL = ENV['ORG_URL'] @@ -37,9 +40,6 @@ $NCBO_API_KEY = ENV['NCBO_API_KEY'] $FAIRNESS_DISABLED = ENV['FAIRNESS_DISABLED'] $FAIRNESS_URL = ENV['FAIRNESS_URL'] - - - # Used to define other bioportal that can be mapped to # Example to map to ncbo bioportal : {"ncbo" => {"api" => "http://data.bioontology.org", "ui" => "http://bioportal.bioontology.org", "apikey" => ""} # Then create the mapping using the following class in JSON : "http://purl.bioontology.org/ontology/MESH/C585345": "ncbo:MESH" @@ -211,53 +211,71 @@ $HOME_PAGE_LOGOS = [ } ] + +# Federation configuration $PORTALS_INSTANCES = [ { - color: '#31b403', - portal: 'AgroPortal', - link: 'https://agroportal.lirmm.fr/' + name: 'AgroPortal', + api: 'https://data.agroportal.lirmm.fr', + ui: 'https://agroportal.lirmm.fr/', + color: '#349696', + apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', + 'light-color': '#F1F6FA', }, { + name: 'BioPortal', + ui: 'https://bioportal.bioontology.org/', + api: 'https://data.bioontology.org/', + apikey: '8b5b7825-538d-40e0-9e9e-5ab9274a9aeb', color: '#234979', - portal: 'BioPortal', - link: 'https://bioportal.bioontology.org/' + 'light-color': '#E9F2FA', }, { + name: 'SIFR BioPortal', + ui: 'https://bioportal.lirmm.fr/', color: '#74a9cb', - portal: 'SIFR BioPortal', - link: 'https://bioportal.lirmm.fr/' }, { + name: 'EcoPortal', + ui: 'https://ecoportal.lifewatch.eu/', + api: 'https://data.ecoportal.lifewatch.eu/', + apikey: "43a437ba-a437-4bf0-affd-ab520e584719", color: '#0d508a', - portal: 'EcoPortal', - link: 'https://ecoportal.lifewatch.eu/' + 'light-color': '#E9F2FA', }, { + name: 'MedPortal', + ui: 'http://medportal.bmicc.cn/', color: '#234979', - portal: 'MedPortal', - link: 'http://medportal.bmicc.cn/' }, { + name: 'MatPortal', + ui: 'https://matportal.org/', color: '#009574', - portal: 'MatPortal', - link: 'https://matportal.org/' }, { + name: 'IndustryPortal', + ui: 'http://industryportal.enit.fr', color: '#1c0f5d', - portal: 'IndustryPortal', - link: 'http://industryportal.enit.fr' }, { + name: 'EarthPortal', + ui: 'https://earthportal.eu/', + api: 'https://data.earthportal.eu/', + apikey: "c9147279-954f-41bd-b068-da9b0c441288", color: '#1e2251', - portal: 'EarthPortal', - link: 'https://earthportal.eu/' + 'light-color': '#F0F5F6' }, { + name: 'BiodivPortal', + ui: 'https://biodivportal.gfbio.org/', + api: 'https://data.biodivportal.gfbio.org/', + apikey: "47a57aa3-7b54-4f34-b695-dbb5f5b7363e", color: '#33691B', - portal: 'BiodivPortal', - link: 'https://biodivportal.gfbio.org/' + 'light-color': '#EBF5F5', } ] + $ONTOPORTAL_WEBSITE_LINK = "https://ontoportal.org/" $ONTOPORTAL_GITHUB_REPO = "https://github.com/ontoportal" diff --git a/config/initializers/ontologies_api_client.rb b/config/initializers/ontologies_api_client.rb index ca8f07e9eb..068da53829 100644 --- a/config/initializers/ontologies_api_client.rb +++ b/config/initializers/ontologies_api_client.rb @@ -7,4 +7,5 @@ config.debug_client = $DEBUG_RUBY_CLIENT || false config.debug_client_keys = $DEBUG_RUBY_CLIENT_KEYS || [] config.apikey = $API_KEY + config.federated_portals = $PORTALS_INSTANCES ? $PORTALS_INSTANCES.map{|x| x[:api] && x[:apikey] ? [x[:name].downcase.to_sym, x] : nil }.compact.to_h : {} end diff --git a/config/locales/en.yml b/config/locales/en.yml index fd04ba863c..79ae1583e5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1162,7 +1162,7 @@ en: update_the_current_displayed_content: "will update the current displayed content to all the following submissions:" save: Save ontologies: - showing_ontologies_size: "Showing %{ontologies_size} of %{analytics_size}" + showing_ontologies_size: "Showing %{ontologies_size} of %{analytics_size} from %{portals}" filters: Filters no_license: No license view_license: View license diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f537fc23ad..f044708cb8 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1188,7 +1188,7 @@ fr: save: Sauvegarder ontologies: - showing_ontologies_size: "Affichage de %{ontologies_size} sur %{analytics_size}" + showing_ontologies_size: "Affichage de %{ontologies_size} sur %{analytics_size} de %{portals}" filters: Filtres no_license: Pas de licence view_license: Voir la licence From 1ac463340dfa26c703de0695c99308273b3ca19d Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Thu, 19 Sep 2024 13:56:07 +0200 Subject: [PATCH 17/45] Feature: Add portals configuration tooltip in homepage (#744) * add portal configuration action and view to home controller * create portal_config_tooltip helper and use in homepage portals section * enhance portal config home section style --------- Co-authored-by: Bilel KIHAL --- Gemfile.lock | 6 +- .../stylesheets/application.css.scss.erb | 1 + .../stylesheets/portal_configuration.scss | 68 +++++++++++++++++++ app/controllers/home_controller.rb | 9 +++ app/helpers/home_helper.rb | 7 ++ app/views/home/index.html.haml | 33 ++++----- app/views/home/portal_config.html.haml | 42 ++++++++++++ config/routes.rb | 1 + 8 files changed, 148 insertions(+), 19 deletions(-) create mode 100644 app/assets/stylesheets/portal_configuration.scss create mode 100644 app/views/home/portal_config.html.haml diff --git a/Gemfile.lock b/Gemfile.lock index 2c0afc74bc..31fd0e01d0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git - revision: 15ad7d41d9a414ed3570c073c2b75a3fb4cc19cf + revision: 670ce6adfe77aeda97974414ab0ed4b6c5dc9469 branch: development specs: ontologies_api_client (2.2.0) @@ -302,7 +302,7 @@ GEM time net-http (0.3.2) uri - net-imap (0.4.16) + net-imap (0.4.15) date net-protocol net-pop (0.1.2) @@ -458,7 +458,7 @@ GEM rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.3) + rubocop-ast (1.32.2) parser (>= 3.3.1.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb index ac5160afc2..c5b6723fb3 100755 --- a/app/assets/stylesheets/application.css.scss.erb +++ b/app/assets/stylesheets/application.css.scss.erb @@ -58,6 +58,7 @@ @import "agent_tooltip"; @import "content_finder"; @import "tools"; +@import "portal_configuration"; @import "taxonomy"; /* Bootstrap and Font Awesome */ diff --git a/app/assets/stylesheets/portal_configuration.scss b/app/assets/stylesheets/portal_configuration.scss new file mode 100644 index 0000000000..0f72b14730 --- /dev/null +++ b/app/assets/stylesheets/portal_configuration.scss @@ -0,0 +1,68 @@ + +.portal-configuration{ + padding: 10px; + .portal-configuration-title{ + h3{ + font-size: 20px; + font-weight: 600; + margin-bottom: 0; + } + span{ + font-size: 16px; + } + } + + h4, .home-section-title .text{ + font-size: 15px !important; + } + + p { + font-size: 14px; + } + .home-support-items div { + font-size: 12px; + } + .home-support-items a img { + height: 48px; + width: 48px; + } + .home-logo-instances-small{ + padding: 7px; + margin: 2px 5px !important; + border-radius: 50%; + display: inline-block; + width: 38px; + height: 38px; + display: flex; + justify-content: center; + align-items: center; + } + .portal-config-ontologies{ + display: flex; + align-items: center; + margin-top: 2px; + } + .portal-config-ontologies svg{ + width: 14px; + height: 14px; + margin-right: 4px; + } + .portal-config-ontologies svg path{ + fill: var(--primary-color); + } + .portal-description{ + font-size: 15px; + padding: 11px 0; + } + .portal-config-federated-with{ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-right: 15px; + } + .portal-config-title-text{ + font-weight: 500; + font-size: 15px !important; + } +} diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 3f75bdd3f6..d7e4beeb31 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -51,6 +51,15 @@ def set_cookies render 'cookies', layout: nil end + def portal_config + @config = $PORTALS_INSTANCES.select { |x| x[:name].downcase.eql?((params[:portal] || helpers.portal_name).downcase) }.first + if @config && @config[:api] + @portal_config = LinkedData::Client::Models::Ontology.top_level_links(@config[:api]).to_h + else + @portal_config = {} + end + end + def tools @tools = { search: { diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index 5df6609674..0a5498b01d 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -22,6 +22,13 @@ def format_number_abbreviated(number) end end + def portal_config_tooltip(portal_name, &block) + title = render TurboFrameComponent.new(id: "portal_config_tooltip_#{portal_name&.downcase}", src: "/config?portal=#{portal_name&.downcase}", style: "width: 600px !important; max-height: 300px; overflow: scroll") + render Display::InfoTooltipComponent.new(text: title, interactive: true) do + capture(&block) + end + end + def discover_ontologies_button render Buttons::RegularButtonComponent.new(id: 'discover-ontologies-button', value: t('home.discover_ontologies_button'), variant: "secondary", state: "regular", href: "/ontologies") do |btn| btn.icon_right do diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index be0075bfe3..fd32acc016 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -22,17 +22,17 @@ .home-bubble.home-bubble-one %a.h5{href:"/ontologies/#{@anal_ont_names[0]}", style: "color: white !important"} = @anal_ont_names[0] - %p + %p = @anal_ont_numbers[0].to_s + " " + t('visits.visits') .home-bubble.home-bubble-two %a.h5{href:"/ontologies/#{@anal_ont_names[1]}", style: "color: white !important"} = @anal_ont_names[1] - %p + %p = @anal_ont_numbers[1].to_s + " " + t('visits.visits') .home-bubble.home-bubble-three %a.h5{href:"/ontologies/#{@anal_ont_names[2]}", style: "color: white !important"} = @anal_ont_names[2] - %p + %p = @anal_ont_numbers[2].to_s + " " + t('visits.visits') %a.home-bubble.home-bubble-four{:href => "/visits"} .h5 ... @@ -44,7 +44,7 @@ %p = t('home.index.tagline') = ontologies_content_autocomplete - + .home-body-container .home-section %h4= t('home.ontology_upload') @@ -115,13 +115,13 @@ %a{:href => "/landscape#fairness_assessment"} %div.home-fair-details %p= t('home.fair_details') - + .home-sub-section-right %h4= t('home.twitter_news') %hr.home-section-line .home-card.home-twitter-news %a.twitter-timeline{"data-height" => "360", :href => "https://twitter.com/lagroportal?ref_src=twsrc%5Etfw"} - .home-twitter-loader + .home-twitter-loader = render LoaderComponent.new(type: 'pulsing') %script{:async => "", :charset => "utf-8", :src => "https://platform.twitter.com/widgets.js"} @@ -182,7 +182,7 @@ .text = "#{portal_name} slices" %hr.home-section-line/ - .home-section-description + .home-section-description .div = t('home.slices_description') .home-slices-container @@ -194,8 +194,8 @@ .home-slice-ontologies = slice.ontologies.length = inline_svg 'icons/slices.svg', width: "70", height: "70" - - .home-slice-name + + .home-slice-name = "#{slice.name} (#{slice.acronym})" = render Buttons::RegularButtonComponent.new(id:'regular-button', value: t('home.suggest_slice'), variant: "secondary", state: "regular", href: '/feedback') do |btn| - btn.icon_right do @@ -208,17 +208,18 @@ = t('home.ontoportal_instances') %hr.home-section-line/ - .home-section-description + .home-section-description .div = home_ontoportal_description .home-support-items - $PORTALS_INSTANCES&.each do |portal| - %div.text-center - = link_to portal[:ui], target: '_blank', class: 'home-logo-instances', style: "background-color: #{portal[:color]}" do - = inline_svg 'logo-white.svg', width: "35", height: "26" - %p{style: "color: #{portal[:color]}"} - = portal[:name] - + = portal_config_tooltip(portal[:name]) do + %div.text-center + = link_to portal[:ui], target: '_blank', class: 'home-logo-instances', style: "background-color: #{portal[:color]}" do + = inline_svg('logo-white.svg', width: "35", height: "26") + %p{style: "color: #{portal[:color]}"} + = portal[:name] + .home-section .home-section-title .text diff --git a/app/views/home/portal_config.html.haml b/app/views/home/portal_config.html.haml new file mode 100644 index 0000000000..b484e9431c --- /dev/null +++ b/app/views/home/portal_config.html.haml @@ -0,0 +1,42 @@ += turbo_frame_tag "portal_config_tooltip_#{params[:portal]}" do + .portal-configuration + .home-section-description.mb-1 + .div.d-flex.align-items-center + %div + %div.text-center + = link_to @portal_config[:ui] || @config[:ui], target: '_blank', class: 'home-logo-instances mr-1 m-0', style: "background-color: #{@portal_config[:color] || @config[:color]}" do + = inline_svg 'logo-white.svg', width: "35", height: "26" + %div + %div.portal-configuration-title{style: "color: #{@portal_config[:color] || @config[:color]}"} + %h3 + = @portal_config[:title] || @config[:name] + - if @portal_config[:numberOfArtefacts] + .portal-config-ontologies + = inline_svg_tag 'icons/ontology.svg' + %span + = "#{@portal_config[:numberOfArtefacts]} ontologies" + - if @portal_config[:description] + .portal-description + = @portal_config[:description] + - if @portal_config[:federated_portals] + %div.mb-1 + .home-section-title + .text + Federated with + .d-flex.flex-wrap.my-1 + - @portal_config[:federated_portals].to_h.values.compact.each do |portal| + .portal-config-federated-with + = link_to portal[:ui], target: '_blank', class: 'home-logo-instances-small', style: "background-color: #{portal[:color]};" do + = inline_svg 'logo-white.svg', width: "18", height: "13" + %p{style: "color: #{portal[:color]}"} + = portal[:name] + + - if @portal_config[:fundedBy] + %div.mb-1 + .home-section-title + .portal-config-title-text + = t('home.support_and_collaborations') + .home-support-items.d-flex.flex-wrap.my-1 + - @portal_config[:fundedBy]&.each do |logo| + %a.mx-2.my-1{href:logo[:url], target: "_blanc"} + %img{src: asset_path(logo[:img_src]), width: "18", height: "13"} diff --git a/config/routes.rb b/config/routes.rb index f31620d09a..b28c26ec02 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,7 @@ get 'auth/:provider/callback', to: 'login#create_omniauth' get 'locale/:language', to: 'language#set_locale_language' get 'metadata_export/index' + get '/config', to: 'home#portal_config' get '/notes/new_comment', to: 'notes#new_comment' get '/notes/new_proposal', to: 'notes#new_proposal' From ff624719973abf21f7466ba89ecc5052b3b44a63 Mon Sep 17 00:00:00 2001 From: MUH <58882014+muhammedBkf@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:17:54 +0100 Subject: [PATCH 18/45] Fix: localize a forgoten text in the annotator page (#757) --- app/views/annotator/index.html.haml | 2 +- config/locales/en.yml | 1 + config/locales/fr.yml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/annotator/index.html.haml b/app/views/annotator/index.html.haml index 8b83bab08c..8fcdaf4bcd 100644 --- a/app/views/annotator/index.html.haml +++ b/app/views/annotator/index.html.haml @@ -27,7 +27,7 @@ = render(ChipsComponent.new(name: 'exclude_synonyms', label: t('annotator.exclude_synonyms'), checked: params[:exclude_synonyms])) .select-ontologies - = ontologies_selector(id:'annotator_page_ontologies', label: 'Select ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) + = ontologies_selector(id:'annotator_page_ontologies', label: t('annotator.select_ontologies') ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) = show_advanced_options_button(text: t('show_advanced_options'), init: @advanced_options_open) = hide_advanced_options_button(text: t('hide_advanced_options'), init: @advanced_options_open) .more-advanced-options{'data-reveal-component-target': 'item', class: "#{@advanced_options_open ? '' : 'd-none'}"} diff --git a/config/locales/en.yml b/config/locales/en.yml index 79ae1583e5..12d5bd6b51 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -303,6 +303,7 @@ en: and fungi, in an attempt to replace existing methods of chemical control and avoid extensive use of fungicides, which often lead to resistance in plant pathogens. In agriculture, plant growth-promoting and biocontrol microorganisms have emerged as safe alternatives to chemical pesticides. Streptomyces spp. and their metabolites may have great potential as excellent agents for controlling various fungal and bacterial phytopathogens. + select_ontologies: Select ontologies concepts: error_valid_concept: "Error: You must provide a valid concept id" missing_roots: Missing roots diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f044708cb8..a9f0c20135 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -305,7 +305,7 @@ fr: et aériennes, dans le but de remplacer les méthodes actuelles de contrôle chimique et d'éviter l'utilisation extensive de fongicides, qui conduisent souvent à une résistance chez les pathogènes des plantes. En agriculture, les microorganismes promoteurs de croissance des plantes et de biocontrôle ont émergé comme des alternatives sûres aux pesticides chimiques. Les espèces de Streptomyces et leurs métabolites peuvent avoir un grand potentiel en tant qu'agents excellents pour contrôler divers phytopathogènes fongiques et bactériens. - + select_ontologies: Sélectionner des ontologies concepts: error_valid_concept: "Erreur : Vous devez fournir un identifiant de concept valide" missing_roots: Racines manquantes From 3ec8d1c5faba8ffd53e8c2575ecb699b871fc16f Mon Sep 17 00:00:00 2001 From: MUH <58882014+muhammedBkf@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:36:13 +0100 Subject: [PATCH 19/45] Fix : Add language-specific anchors to footer section links and the cookie banner (#760) * Add language-specific anchors to footer section links * Add language-specific anchors to the cookie banner * Refactor language-specific anchors to use I18n default option * Remove anchors from bioportal_config_env.rb.sample; now handled by footer view --- app/views/home/cookies.html.haml | 2 +- app/views/layouts/_footer.html.haml | 4 +++- config/bioportal_config_env.rb.sample | 4 ++-- config/locales/en.yml | 4 ++++ config/locales/fr.yml | 4 ++++ 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/views/home/cookies.html.haml b/app/views/home/cookies.html.haml index d02f687a9b..513df903f7 100644 --- a/app/views/home/cookies.html.haml +++ b/app/views/home/cookies.html.haml @@ -10,4 +10,4 @@ %div = link_button_component(id:'accept-cookie-selector', value: t('cookies_modal.accept_button'), href: cookies_path(cookies: true), size: "slim", variant: 'primary') %div.cookie-privacy-link - = link_to t('cookies_modal.privacy_link'), $FOOTER_LINKS.dig(:sections, :agreements, :privacy_policy) + = link_to t('cookies_modal.privacy_link'), "#{$FOOTER_LINKS.dig(:sections, :agreements, :privacy_policy)}#{t('cookies_modal.privacy_policy_anchor')}" diff --git a/app/views/layouts/_footer.html.haml b/app/views/layouts/_footer.html.haml index 9d89089615..68575a0c2b 100644 --- a/app/views/layouts/_footer.html.haml +++ b/app/views/layouts/_footer.html.haml @@ -20,8 +20,10 @@ %h2 = t("layout.footer."+key.to_s) %div + - section_links.each do |section , link| - %a{:href => link, :target => "_blank"} + - anchor = I18n.t("layout.footer.#{section}_anchor", default: "") + %a{:href => "#{link}#{anchor}", :target => "_blank"} = t("layout.footer."+section.to_s) diff --git a/config/bioportal_config_env.rb.sample b/config/bioportal_config_env.rb.sample index 352805b196..1b46a0ebbf 100644 --- a/config/bioportal_config_env.rb.sample +++ b/config/bioportal_config_env.rb.sample @@ -305,8 +305,8 @@ $FOOTER_LINKS = { }, agreements: { terms: $TERMS_AND_CONDITIONS_LINK, - privacy_policy: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq#h-privacy-policy", - legal_notices: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq#h-legal-notice" + privacy_policy: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq", + legal_notices: "https://doc.jonquetlab.lirmm.fr/share/e6158eda-c109-4385-852c-51a42de9a412/doc/terms-conditions-naDsDo2Zxq" }, about: { about_us: "https://github.com/agroportal/project-management", diff --git a/config/locales/en.yml b/config/locales/en.yml index 12d5bd6b51..fe6724cc51 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -519,8 +519,11 @@ en: issues_and_requests: Issues and Requests agreements: Legal terms: Terms and Conditions + terms_anchor: "#h-agroportal-terms-and-conditions-english" privacy_policy: Privacy Policy + privacy_policy_anchor: "#h-privacy-policy" legal_notices: Legal Notices + legal_notices_anchor: "#h-legal-notice" cite_us: Cite Us acknowledgments: Acknowledgments about: About @@ -1494,6 +1497,7 @@ en: paragraph2: "The cookies are functional and non-optional. By staying on %{portal}, you acknowledge the information was delivered to you." accept_button: "Accept" privacy_link: "Privacy policy" + privacy_policy_anchor: "#h-privacy-policy" taxonomy: groups_and_categories: Groups and Categories description: In AgroPortal, ontologies are organized in groups and tagged with categories. Typically, groups associate ontologies from the same project or organization for better identification of the provenance. Whereas categories are about subjects/topics and enable to classify ontologies. As of 2016, AgroPortal's categories were established in cooperation with FAO AIMS. In 2024, we moved to UNESCO nomenclature for fields of science and technology. Groups and categories, along with other metadata, can be used on the “Browse” page of AgroPortal to filter out the list of ontologies. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a9f0c20135..9eebb36316 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -524,8 +524,11 @@ fr: issues_and_requests: Problèmes et demandes agreements: Légal terms: Termes et conditions + terms_anchor: "#h-conditions-generales-dutilisation-dagroportal-francais" privacy_policy: Politique de confidentialité + privacy_policy_anchor: "#h-vie-privee" legal_notices: Mentions légales + legal_notices_anchor: "#h-mentions-legales" cite_us: Citez-nous acknowledgments: Remerciements about: À propos @@ -1532,6 +1535,7 @@ fr: paragraph2: "Les cookies sont fonctionnels et non facultatifs. En restant sur %{portal}, vous reconnaissez que les informations vous ont été transmises." accept_button: "Accepter" privacy_link: "Vie privé" + privacy_policy_anchor: "#h-vie-privee" taxonomy: groups_and_categories: Groupes et Catégories description: Dans AgroPortal, les ontologies sont organisées en groupes et étiquetées avec des catégories. Typiquement, les groupes associent des ontologies provenant du même projet ou de la même organisation pour une meilleure identification de la provenance. Tandis que les catégories concernent des sujets/thématiques et permettent de classifier les ontologies. En 2016, les catégories d'AgroPortal ont été établies en coopération avec FAO AIMS. En 2024, nous sommes passés à la nomenclature de l'UNESCO pour les domaines des sciences et des technologies. Les groupes et les catégories, ainsi que d'autres métadonnées, peuvent être utilisés sur la page “Parcourir” d'AgroPortal pour filtrer la liste des ontologies. From 5ade60046cad9ccb5289905437af09595939a81f Mon Sep 17 00:00:00 2001 From: MUH <58882014+muhammedBkf@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:41:41 +0100 Subject: [PATCH 20/45] Feature: Add `Details` sub-tab and API cotextual button in Properties, Instances, Schemes, and Collections (#754) * limite taxonomy card ontologies chips number and add a see all button * Properties layout harmonization * Instances layout harmonization * fix: resolve issues related to properties raised in code review * fix: resolve issues related to instances raised in code review * adding subtab "Details" and API cotextual button to `Schemes` * adding subtab "Details" and API cotextual button to `Collections` * Using unless instead of if in sections controllers * Adding new section tab component and using it in Properties, Instances, Schemes and Collections * display first scheme if none are selected * move the created tabs_section component to rails helpers * fix schemes and collections shows not selecting the right object * remove details partials moving the content directly to the show files * remove unnecessary call in the properties show to find the ontology * use the correct partial in the schemes controller show action * put the tab item helper in the component helpers as generic * use the correct collection partial in the show action * remove unnecessary local in the instances partial * clean instance show partial code * remove unnecessary locals in the properties partial call * remove unnecessary locals in the collections show partial call * remove unnecessary locals in the schemes show partial call * Add presence check (not nil and not empty) for ontology details component before rendering --------- Co-authored-by: Syphax bouazzouni Co-authored-by: Bilel KIHAL --- .../taxonomy_card_component.html.haml | 7 ++- app/components/tab_item_component.rb | 8 +-- app/controllers/collections_controller.rb | 5 +- app/controllers/instances_controller.rb | 2 +- app/controllers/ontologies_controller.rb | 3 +- app/controllers/schemes_controller.rb | 4 +- app/helpers/collections_helper.rb | 2 +- app/helpers/components_helper.rb | 9 +++ app/helpers/ontologies_helper.rb | 45 +++++++++++++++ app/views/collections/_collection.html.haml | 13 ----- app/views/collections/_show.html.haml | 14 +++++ app/views/collections/show.html.haml | 1 - app/views/concepts/_show.html.haml | 57 +++++-------------- app/views/instances/_details.html.haml | 27 --------- app/views/instances/_instances.html.haml | 2 +- app/views/instances/_show.html.haml | 22 +++++++ .../sections/_collections.html.haml | 18 +++--- .../ontologies/sections/_schemes.html.haml | 2 +- app/views/properties/_show.html.haml | 31 +++++----- app/views/schemes/_scheme.html.haml | 13 ----- app/views/schemes/_show.html.haml | 11 ++++ app/views/schemes/show.html.haml | 1 - 22 files changed, 155 insertions(+), 142 deletions(-) delete mode 100644 app/views/collections/_collection.html.haml create mode 100644 app/views/collections/_show.html.haml delete mode 100644 app/views/collections/show.html.haml delete mode 100644 app/views/instances/_details.html.haml create mode 100644 app/views/instances/_show.html.haml delete mode 100644 app/views/schemes/_scheme.html.haml create mode 100644 app/views/schemes/_show.html.haml delete mode 100644 app/views/schemes/show.html.haml diff --git a/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml b/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml index 05903f4f4d..ab55aa41de 100644 --- a/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml +++ b/app/components/display/taxonomy_card_component/taxonomy_card_component.html.haml @@ -11,9 +11,12 @@ .description = render TextAreaFieldComponent.new(value: @taxonomy.description) .ontologies-cards - - @taxonomy.ontologies.each do |ontology| + - @taxonomy.ontologies.each_with_index do |ontology, index| + - if index>10 + = render ChipButtonComponent.new(url: "/ontologies?#{@taxonomy.id.split('/')[-2]}=#{@taxonomy.acronym}", text: "...", tooltip:"See all ontologies ...", type: "clickable") + - break = render ChipButtonComponent.new(url: "/ontologies/#{ontology.split('/').last}", text: ontology.split('/').last, tooltip: @ontologies_names[ontology], type: "clickable") - + - if @taxonomy.children .taxonomy-children-reveal{'data-action': "click->reveal-component#toggle", 'data-id': reveal_id} .text diff --git a/app/components/tab_item_component.rb b/app/components/tab_item_component.rb index 88f11f890a..8d5f25c8e4 100644 --- a/app/components/tab_item_component.rb +++ b/app/components/tab_item_component.rb @@ -44,12 +44,8 @@ def page_name end def call - if title && !title.empty? - link_to(title, @path, id: "#{item_id}_tab", class: "#{active_class} tab-link", 'data-json-link': @json_link) - else - link_to(@path, id: "#{item_id}_tab", class: "#{active_class} tab-link", 'data-json-link': @json_link) do - content - end + link_to(@path, id: "#{item_id}_tab", class: "#{active_class} tab-link", 'data-json-link': @json_link) do + (title && !title.empty?) ? title.html_safe : content end end diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index 31b5db7786..c01c0cf879 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -37,9 +37,12 @@ def index end def show - redirect_to(ontology_path(id: params[:ontology_id], p: 'collections', collectionid: params[:id], lang: request_lang)) and return unless turbo_frame_request? + + redirect_to(ontology_path(id: params[:ontology], p: 'collections', collectionid: params[:id], lang: request_lang)) and return unless turbo_frame_request? @collection = get_request_collection + + render partial: "collections/show" end def show_label diff --git a/app/controllers/instances_controller.rb b/app/controllers/instances_controller.rb index 70db773446..acaa005a1f 100644 --- a/app/controllers/instances_controller.rb +++ b/app/controllers/instances_controller.rb @@ -41,7 +41,7 @@ def show redirect_to(ontology_path(id: params[:ontology], p: 'instances', instanceid: params[:id] || params[:instanceid], lang: request_lang)) and return unless turbo_frame_request? - render partial: 'instances/details', layout: nil + render partial: 'show' end private diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 11d0ae2ca5..5a2af639fa 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -192,7 +192,8 @@ def instances def schemes @schemes = get_schemes(@ontology) scheme_id = params[:schemeid] || @submission_latest.URI || nil - @scheme = get_scheme(@ontology, scheme_id) if scheme_id + @scheme = scheme_id ? get_scheme(@ontology, scheme_id) : @schemes.first + render partial: 'ontologies/sections/schemes', layout: 'ontology_viewer' end diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index 7fbbcc973e..8c5c97f1f0 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -32,9 +32,11 @@ def index end def show - redirect_to(ontology_path(id: params[:ontology_id], p: 'schemes', schemeid: params[:id],lang: request_lang)) and return unless turbo_frame_request? + redirect_to(ontology_path(id: params[:ontology], p: 'schemes', schemeid: params[:id],lang: request_lang)) and return unless turbo_frame_request? @scheme = get_request_scheme + + render partial: "schemes/show" end def show_label diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index bb81e39cef..351401a467 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -4,7 +4,7 @@ module CollectionsHelper def get_collections(ontology, add_colors: false) collections = ontology.explore.collections(language: request_lang) generate_collections_colors(collections) if add_colors - collections + collections.sort_by{ |x| x.prefLabel } end def get_collection(ontology, collection_uri) diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 210e4ab57c..43e84004d3 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -1,6 +1,15 @@ module ComponentsHelper include TermsReuses + def tab_item_component(container_tabs:, title:, path:, selected: false, json_link: "", &content) + container_tabs.item(title: title.html_safe, path: path, selected: selected, json_link: json_link) + container_tabs.item_content { capture(&content) } + end + + def alert_component(message, type: "info") + render Display::AlertComponent.new(type: type, message: message) + end + def chips_component(id: , name: , label: , value: , checked: false , tooltip: nil, &block) content_tag(:div, data: { controller: 'tooltip' }, title: tooltip) do check_input(id: id, name: name, value: value, label: label, checked: checked, &block) diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index 33adc9efd5..f37836e9fb 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -530,6 +530,51 @@ def language_selector_hidden_tag(section) data: { controller: "language-change", 'language-change-section-value': section, action: "change->language-change#dispatchLangChangeEvent" } end + def ontology_object_json_link(ontology_acronym, object_type, id) + "#{rest_url}/ontologies/#{ontology_acronym}/#{object_type}/#{escape(id)}?display=all&apikey=#{get_apikey}" + end + + def render_permalink_link + content_tag(:div, class: 'mx-1') do + link_to("#classPermalinkModal", class: "class-permalink nav-link", title: t('concepts.permanent_link_class'), aria: { label: t('concepts.permanent_link_class') }, data: { toggle: "modal", current_purl: @current_purl }) do + content_tag(:i, '', class: "fas fa-link", aria: { hidden: "true" }) + end + end + end + + def render_concepts_json_button(link) + content_tag(:div, class: 'concepts_json_button') do + render RoundedButtonComponent.new(link: link, target: '_blank') + end + end + + + def ontology_object_details_component(frame_id: , ontology_id:, objects_title:, object:, &block) + render TurboFrameComponent.new(id: frame_id, data: {"turbo-frame-target": "frame"}) do + return if !object.present? + return alert_component(object.errors.join) if object.errors + + ontology_object_tabs_component(ontology_id: ontology_id, objects_title: objects_title, object_id: object["@id"]) do |tabs| + tab_item_component(container_tabs: tabs, title: t('concepts.details'), path: '#details', selected: true) do + capture(&block) + end + end + end + end + + def ontology_object_tabs_component(ontology_id:, objects_title:, object_id:, &block) + resource_url = ontology_object_json_link(ontology_id, objects_title, object_id) + render TabsContainerComponent.new(type: 'outline') do |c| + concat(c.pinned_right do + content_tag(:div, '', 'data-concepts-json-target': 'button') do + concat(render_permalink_link) if $PURL_ENABLED + concat(render_concepts_json_button(resource_url)) + end + end) + + capture(c, &block) + end + end def display_complex_text(definitions) diff --git a/app/views/collections/_collection.html.haml b/app/views/collections/_collection.html.haml deleted file mode 100644 index 16503de745..0000000000 --- a/app/views/collections/_collection.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -= turbo_frame_tag 'collection' do - = render ConceptDetailsComponent.new(id:'collection-label', acronym: @ontology.acronym, concept_id: collection.id, - properties: collection.properties, - top_keys: %w[created modified comment note], - bottom_keys: [], - exclude_keys: %w[member]) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t("collections.id")}, {td: link_to_with_actions(collection["@id"], acronym: @ontology.acronym)}) - - t.add_row({th: t("collections.preferred_name")}, {td: display_in_multiple_languages(get_collection_label(collection))}) - - t.add_row({th: t("collections.members_count")}) do |r| - - r.td do - = link_to collection["memberCount"], "/ontologies/" + @ontology.acronym + "/?p=classes&sub_menu=list&concept_collections=" + collection["@id"], 'data-turbo-frame':'_top' - - t.add_row({th: t("collections.type")}, {td: collection["@type"]}) diff --git a/app/views/collections/_show.html.haml b/app/views/collections/_show.html.haml new file mode 100644 index 0000000000..d79a3e47c8 --- /dev/null +++ b/app/views/collections/_show.html.haml @@ -0,0 +1,14 @@ += ontology_object_details_component(frame_id: "collection", ontology_id: @ontology.acronym, objects_title: "collections", object: @collection) do + = render ConceptDetailsComponent.new(id:'collection-label', acronym: @ontology.acronym, concept_id: @collection.id, + properties: @collection.properties, + top_keys: %w[created modified comment note], + bottom_keys: [], + exclude_keys: %w[member]) do |c| + - c.header(stripped: true) do |t| + - t.add_row({th: t("collections.id")}, {td: link_to_with_actions(@collection["@id"], acronym: @ontology.acronym)}) + - t.add_row({th: t("collections.preferred_name")}, {td: display_in_multiple_languages(get_collection_label(@collection))}) + - t.add_row({th: t("collections.members_count")}) do |r| + - r.td do + = link_to @collection["memberCount"], "/ontologies/" + @ontology.acronym + "/?p=classes&sub_menu=list&concept_collections=" + @collection["@id"], 'data-turbo-frame':'_top' + - t.add_row({th: t("collections.type")}, {td: @collection["@type"]}) + diff --git a/app/views/collections/show.html.haml b/app/views/collections/show.html.haml deleted file mode 100644 index 4ef79429cf..0000000000 --- a/app/views/collections/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render partial: 'collection', locals: {collection: @collection} \ No newline at end of file diff --git a/app/views/concepts/_show.html.haml b/app/views/concepts/_show.html.haml index a30f061fdf..8d690b65fd 100644 --- a/app/views/concepts/_show.html.haml +++ b/app/views/concepts/_show.html.haml @@ -4,63 +4,32 @@ = t('concepts.use_jump_to') - else %div{'data-controller': 'concepts-json', 'data-action': 'click->concepts-json#update'} - = render TabsContainerComponent.new(type:'outline') do |c| - - c.pinned_right do - - if $PURL_ENABLED - %div.mx-1 - = link_to("#classPermalinkModal", class: "class-permalink nav-link", title: t('concepts.permanent_link_class'), aria: {label: t('concepts.permanent_link_class')}, data: {toggle: "modal", current_purl: "#{@current_purl}"}) do - %i{class: "fas fa-link", aria: {hidden: "true"}} - %div{'data-concepts-json-target': 'button'} - .concepts_json_button - = render RoundedButtonComponent.new(link: "#{@ontology.id}/classes/#{escape(@concept.id)}?display=all&apikey=#{get_apikey}", target:'_blank') + = ontology_object_tabs_component(ontology_id: @ontology.acronym, objects_title: "classes", object_id: @concept.id) do |c| - apikey = "apikey=#{get_apikey}" - baseClassUrl = "#{@ontology.id}/classes/#{escape(@concept.id)}" - - c.item(title: t('concepts.details'), path: '#details', selected: true, json_link: "#{baseClassUrl}?#{apikey}&display=all") - - - unless skos? - - c.item(id: 'instances', path: '#instances', json_link: "#{baseClassUrl}/instances?#{apikey}") do - = t('concepts.instances') - ( - %span#concept_instances_sorted_list_count - ) - - - c.item(title: t('concepts.visualization'), path: '#visualization') - - - c.item(id: 'notes', path: '#notes', json_link: "#{baseClassUrl}/notes?#{apikey}") do - = t('concepts.notes') - %span#note_count_wrapper - ( - %span#note_count= @notes.length - ) - - c.item(id: 'mappings', path: '#mappings', json_link: "#{baseClassUrl}/mappings?#{apikey}") do - .d-flex - #{t('concepts.mappings')} - ( - = concept_mappings_loader(ontology_acronym: @ontology.acronym, concept_id: @concept.id) - ) - - - if @enable_ontolobridge - - c.item(title: t('concepts.new_term_requests'), path: '#request_term') - - - c.item_content do + - tab_item_component(container_tabs: c, title: t('concepts.details'), path: '#details', selected: true, json_link: "#{baseClassUrl}?#{apikey}&display=all") do = render :partial =>'/concepts/details' - unless skos? - - c.item_content do + - count_span = content_tag(:span, "#{t('concepts.instances')} (#{content_tag(:span, "", id: 'concept_instances_sorted_list_count')})".html_safe) + - tab_item_component(container_tabs: c, title: count_span, path: '#instances', json_link: "#{baseClassUrl}/instances?#{apikey}") do = render :partial =>'instances/instances' , locals: {id: "class-instances-data-table"} - - c.item_content do + + + - tab_item_component(container_tabs: c, title: t('concepts.visualization'), path: '#visualization') do = render :partial =>'/concepts/biomixer' - - c.item_content do + + - count_span = content_tag(:span, "#{t('concepts.notes')} (#{content_tag(:span, @notes.length, id: 'note_count')})".html_safe) + - tab_item_component(container_tabs: c, title: count_span, path: '#notes', json_link: "#{baseClassUrl}/notes?#{apikey}") do = render :partial =>'/notes/list' - - c.item_content do + + - count_span = content_tag(:span, "#{t('concepts.mappings')} (#{content_tag(:span, concept_mappings_loader(ontology_acronym: @ontology.acronym, concept_id: @concept.id))})".html_safe, class: "d-flex") + - tab_item_component(container_tabs: c, title: count_span, path: '#mappings', json_link: "#{baseClassUrl}/mappings?#{apikey}") do = render TurboFrameComponent.new(id:'concept_mappings', src:"/ajax/mappings/get_concept_table?ontologyid=#{@ontology.acronym}&conceptid=#{CGI.escape(@concept.id)}") - - if @enable_ontolobridge - - c.item_content do - = render :partial =>'/concepts/request_term' :javascript jQuery(document).ready(function(){ diff --git a/app/views/instances/_details.html.haml b/app/views/instances/_details.html.haml deleted file mode 100644 index 146dcd9464..0000000000 --- a/app/views/instances/_details.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -= render TurboFrameComponent.new(id: params[:modal]&.to_s.eql?('true') ? modal_frame_id : 'instance_show') do - - if @instance && @instance["@id"] - %div - - ontology_acronym = params[:ontology_id] || @ontology.acronym - - filter_properties = ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type", "http://www.w3.org/2000/01/rdf-schema#label", "http://www.w3.org/2004/02/skos/core#prefLabel"] - - = render ConceptDetailsComponent.new(id:'instance-details', acronym: ontology_acronym, concept_id: @instance["@id"]) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t("instances.id")}, {td: link_to_with_actions(@instance["@id"], acronym: @ontology.acronym) }) - - - label = @instance['label'] || @instance['prefLabel'] - - unless label.blank? - - t.add_row({th: t('instances.label') }, {td: label.join(',').html_safe}) - - - types = @instance.types.reject{|x| x['NamedIndividual']} - - unless types.empty? - - t.add_row({th: t('instances.type') }) do |r| - - r.td do - = types.reject{|x| x['NamedIndividual']}.map {|cls| link_to_class(ontology_acronym,cls)}.join(', ').html_safe - - properties = @instance[:properties].to_h.select{|k,v| !filter_properties.include? k.to_s} - - properties.each do |prop| - - if !prop[1].nil? - - t.add_row({th: link_to_property(prop[0], ontology_acronym)}, {td: prop[1].map { |value| instance_property_value(value , ontology_acronym) }.join(', ').html_safe}) - - - - diff --git a/app/views/instances/_instances.html.haml b/app/views/instances/_instances.html.haml index 78b8ce0c29..9e5ea8eb33 100644 --- a/app/views/instances/_instances.html.haml +++ b/app/views/instances/_instances.html.haml @@ -9,7 +9,7 @@ - if params[:p].eql?('instances') %div#prop_contents{data: {'container-splitter-target': 'container'}} - = render partial: 'instances/details' + = render partial: 'instances/show' diff --git a/app/views/instances/_show.html.haml b/app/views/instances/_show.html.haml new file mode 100644 index 0000000000..71fbc857ca --- /dev/null +++ b/app/views/instances/_show.html.haml @@ -0,0 +1,22 @@ += ontology_object_details_component(frame_id: params['modal'].eql?('true') ? modal_frame_id : "instance_show", ontology_id: @ontology.acronym, objects_title: "instances", object: @instance) do + = render ConceptDetailsComponent.new(id: 'instance-details', acronym: @ontology.acronym, concept_id: @instance["@id"]) do |c| + - c.header(stripped: true) do |t| + - t.add_row({ th: t("instances.id") }, { td: link_to_with_actions(@instance["@id"], acronym: @ontology.acronym) }) + + - label = @instance['label'] || @instance['prefLabel'] + - t.add_row({ th: t('instances.label') }, { td: label.join(', ').html_safe }) unless label.blank? + + - types = @instance.types.reject { |x| x['NamedIndividual'] } + - unless types.empty? + - t.add_row({ th: t('instances.type') }) do |r| + - r.td do + = types.map { |cls| link_to_class(@ontology.acronym, cls) }.join(', ').html_safe + + - filter_properties = %w[http://www.w3.org/1999/02/22-rdf-syntax-ns#type http://www.w3.org/2000/01/rdf-schema#label http://www.w3.org/2004/02/skos/core#prefLabel] + - properties = @instance[:properties].to_h.reject { |k, _| filter_properties.include?(k.to_s) } + - properties.each do |prop, values| + - if values.present? + - t.add_row({ th: link_to_property(prop, @ontology.acronym) }, { td: values.map { |value| instance_property_value(value, @ontology.acronym) }.join(', ').html_safe }) + + + diff --git a/app/views/ontologies/sections/_collections.html.haml b/app/views/ontologies/sections/_collections.html.haml index 280c647603..9463602928 100644 --- a/app/views/ontologies/sections/_collections.html.haml +++ b/app/views/ontologies/sections/_collections.html.haml @@ -1,17 +1,15 @@ = render TurboFrameComponent.new(id: "collections", data: {"turbo-frame-target": "frame"} ) do - - if no_collections? - = no_collections_alert - - else - %div.ont-collections{data:{controller: 'container-splitter'}} - %div#collectionsTree.card.sidebar{data:{'container-splitter-target': 'container'}} + %div.ont-collections{data:{controller: 'container-splitter'}} + %div#collectionsTree.card.sidebar{data:{'container-splitter-target': 'container'}} + - if no_collections? + = no_collections_alert + - else = tree_container_component(id: "collections_sorted_list_view-page-1", placeholder: t('ontologies.sections.collections_search_placeholder', acronym: @ontology.acronym), frame_url: "/ontologies/#{@ontology.acronym}/collections", tree_url: "/ontologies/#{@ontology.acronym}/collections?#{request.original_url.split('?')[1]}") - - %div#collection_contents{data:{'container-splitter-target': 'container'}} - = render TurboFrameComponent.new(id: 'collection') do - - if @collection - = render partial: 'collections/collection', locals: {collection: @collection} + %div#collection_contents{data:{'container-splitter-target': 'container'}} + = render TurboFrameComponent.new(id: 'collection') do + = render partial: 'collections/show' diff --git a/app/views/ontologies/sections/_schemes.html.haml b/app/views/ontologies/sections/_schemes.html.haml index aa01c54aa4..fc91ee21e7 100644 --- a/app/views/ontologies/sections/_schemes.html.haml +++ b/app/views/ontologies/sections/_schemes.html.haml @@ -11,7 +11,7 @@ %div#scheme_contents{data:{'container-splitter-target': 'container'}} = render TurboFrameComponent.new(id:'scheme') do - = render partial: 'schemes/scheme', locals: {scheme: @scheme} + = render partial: 'schemes/show' diff --git a/app/views/properties/_show.html.haml b/app/views/properties/_show.html.haml index f74964f85e..d6ea4ce531 100644 --- a/app/views/properties/_show.html.haml +++ b/app/views/properties/_show.html.haml @@ -1,18 +1,13 @@ -= render TurboFrameComponent.new(id: 'property_show', data: {"turbo-frame-target": "frame"}) do - - if @property - - if @property.errors - = render Display::AlertComponent.new(type:'info', message: @property.errors.join) - - else - - properties = LinkedData::Client::Models::Property.properties_to_hash(@property).first - = render ConceptDetailsComponent.new(id:'property-details', acronym: @acronym, concept_id: @property.id, - properties: @property.properties, - top_keys: [], - bottom_keys: properties.keys.map(&:to_s), - exclude_keys: []) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t('properties.id')}, {td: link_to_with_actions(@property.id, acronym: @acronym)}) - - t.add_row({th: t('properties.type')}, {td: @property.type }) - - t.add_row({th: t('properties.preferred_name')}, {td: display_in_multiple_languages(@property.label)}) unless @property.label.blank? - - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(@property.definition)}) unless @property.definition.blank? - - t.add_row({th: t('properties.domain')}, {td: get_link_for_cls_ajax(@property.domain, @acronym, '_top')}) unless @property.domain.blank? - - t.add_row({th: t('properties.range')}, {td: get_link_for_cls_ajax(@property.range, @acronym, '_top')}) unless @property.range.blank? += ontology_object_details_component(frame_id: "property_show", ontology_id: @acronym, objects_title: "properties", object: @property) do + = render ConceptDetailsComponent.new(id:'property-details', acronym: @acronym, concept_id: @property.id, + properties: @property.properties, + top_keys: [], + bottom_keys: LinkedData::Client::Models::Property.properties_to_hash(@property).first.keys.map(&:to_s), + exclude_keys: []) do |c| + - c.header(stripped: true) do |t| + - t.add_row({th: t('properties.id')}, {td: link_to_with_actions(@property.id, acronym: @acronym)}) + - t.add_row({th: t('properties.type')}, {td: @property.type }) + - t.add_row({th: t('properties.preferred_name')}, {td: display_in_multiple_languages(@property.label)}) unless @property.label.blank? + - t.add_row({th: t('properties.definitions')}, {td: display_in_multiple_languages(@property.definition)}) unless @property.definition.blank? + - t.add_row({th: t('properties.domain')}, {td: get_link_for_cls_ajax(@property.domain, @acronym, '_top')}) unless @property.domain.blank? + - t.add_row({th: t('properties.range')}, {td: get_link_for_cls_ajax(@property.range, @acronym, '_top')}) unless @property.range.blank? \ No newline at end of file diff --git a/app/views/schemes/_scheme.html.haml b/app/views/schemes/_scheme.html.haml deleted file mode 100644 index 5cfac8c316..0000000000 --- a/app/views/schemes/_scheme.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -= turbo_frame_tag 'scheme' do - - if @scheme && !@scheme.empty? - = render ConceptDetailsComponent.new(id:'scheme-label', acronym: @ontology.acronym, concept_id: @scheme.id, - properties: scheme.properties, - top_keys: %w[description comment], - bottom_keys: %w[disjoint subclass is_a has_part], - exclude_keys: []) do |c| - - c.header(stripped: true) do |t| - - t.add_row({th: t('schemes.id')} , {td: link_to_with_actions(scheme["@id"], acronym: @ontology.acronym)}) - - t.add_row({th: t('schemes.preferred_name')} , {td: display_in_multiple_languages(get_scheme_label(scheme))}) - - t.add_row({th: t('schemes.type')} , {td: scheme["@type"]}) - - diff --git a/app/views/schemes/_show.html.haml b/app/views/schemes/_show.html.haml new file mode 100644 index 0000000000..b0af055f4b --- /dev/null +++ b/app/views/schemes/_show.html.haml @@ -0,0 +1,11 @@ += ontology_object_details_component(frame_id: "scheme", ontology_id: @ontology.acronym, objects_title: "schemes", object: @scheme) do + = render ConceptDetailsComponent.new(id:'scheme-label', acronym: @ontology.acronym, concept_id: @scheme.id, + properties: @scheme.properties, + top_keys: %w[description comment], + bottom_keys: %w[disjoint subclass is_a has_part], + exclude_keys: []) do |c| + - c.header(stripped: true) do |t| + - t.add_row({th: t('schemes.id')} , {td: link_to_with_actions(@scheme["@id"], acronym: @ontology.acronym)}) + - t.add_row({th: t('schemes.preferred_name')} , {td: display_in_multiple_languages(get_scheme_label(@scheme))}) + - t.add_row({th: t('schemes.type')} , {td: @scheme["@type"]}) + diff --git a/app/views/schemes/show.html.haml b/app/views/schemes/show.html.haml deleted file mode 100644 index 79eb63ac8f..0000000000 --- a/app/views/schemes/show.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render partial: 'scheme', locals: {scheme: @scheme} \ No newline at end of file From 504d02d3cebb1d30f2a8e17159d613a52d3c1822 Mon Sep 17 00:00:00 2001 From: Bilel KIHAL Date: Thu, 3 Oct 2024 10:10:55 +0200 Subject: [PATCH 21/45] fix infinit login page redirection when trying to access accout page --- app/controllers/users_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a3749d4631..520ab1b861 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,6 +1,6 @@ class UsersController < ApplicationController - before_action :verify_owner, only: [:edit, :show, :subscribe, :un_subscribe] + before_action :verify_owner, only: [:edit, :subscribe, :un_subscribe] before_action :authorize_admin, only: [:index,:subscribe, :un_subscribe] layout :determine_layout From db8e86394596031e36f1c8ff765ca9c875b7b618 Mon Sep 17 00:00:00 2001 From: Bilel KIHAL Date: Mon, 7 Oct 2024 14:39:49 +0200 Subject: [PATCH 22/45] Support coutries as languages --- Gemfile | 3 ++- Gemfile.lock | 4 ++++ app/components/language_field_component.rb | 11 ++++------ app/helpers/collections_helper.rb | 5 ++--- app/helpers/multi_languages_helper.rb | 24 ++++++++++++++++++---- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/Gemfile b/Gemfile index c40e3640c5..8309654a1b 100644 --- a/Gemfile +++ b/Gemfile @@ -94,6 +94,7 @@ gem 'inline_svg' # ISO language codes and flags gem 'flag-icons-rails', '~> 3.4' gem 'iso-639', '~> 0.3.6' +gem 'countries', '~> 5.7' # Custom API client gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'master' @@ -179,4 +180,4 @@ group :test do # Testing framework for Rails gem 'rspec-rails' -end \ No newline at end of file +end diff --git a/Gemfile.lock b/Gemfile.lock index 51f97081af..97facd1e1b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -137,6 +137,8 @@ GEM coderay (1.1.3) color (1.8) concurrent-ruby (1.3.4) + countries (5.7.2) + unaccent (~> 0.3) crack (1.0.0) bigdecimal rexml @@ -523,6 +525,7 @@ GEM railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unaccent (0.4.0) unicode-display_width (2.5.0) uri (0.13.1) version_gem (1.1.4) @@ -568,6 +571,7 @@ DEPENDENCIES capybara chart-js-rails color (~> 1.8) + countries (~> 5.7) dalli debug deepl-rb diff --git a/app/components/language_field_component.rb b/app/components/language_field_component.rb index 7998432a9a..10f215fe51 100644 --- a/app/components/language_field_component.rb +++ b/app/components/language_field_component.rb @@ -3,7 +3,7 @@ class LanguageFieldComponent < ViewComponent::Base - include FlagIconsRails::Rails::ViewHelpers + include FlagIconsRails::Rails::ViewHelpers, MultiLanguagesHelper def initialize(value:, label: nil, auto_label: false, icon: nil) super @@ -11,12 +11,9 @@ def initialize(value:, label: nil, auto_label: false, icon: nil) @lang_code = nil @label = label @icon = icon - - iso = ISO_639.find(value.to_s.split('/').last) - if iso - @lang_code = iso.alpha2 - @label ||= iso.english_name if auto_label - end + @lang_code, label = find_language_code_name(value) + @label ||= label if auto_label + @lang_code = @lang_code.split('-').last if @lang_code end def lang_code diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index bb81e39cef..a577f4190d 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -4,7 +4,7 @@ module CollectionsHelper def get_collections(ontology, add_colors: false) collections = ontology.explore.collections(language: request_lang) generate_collections_colors(collections) if add_colors - collections + collections.sort_by{ |x| helpers.main_language_label(x.prefLabel) } end def get_collection(ontology, collection_uri) @@ -64,7 +64,7 @@ def link_to_collection(collection, selected_collection_id) pref_label_lang, pref_label_html = get_collection_label(collection) tooltip = pref_label_lang.to_s.eql?('@none') ? '' : "data-controller='tooltip' data-tooltip-position-value='right' title='#{pref_label_lang.upcase}'" <<-EOS - @@ -91,4 +91,3 @@ def generate_collections_colors(collections) end end end - diff --git a/app/helpers/multi_languages_helper.rb b/app/helpers/multi_languages_helper.rb index d21d41a302..9e21f2e0ed 100644 --- a/app/helpers/multi_languages_helper.rb +++ b/app/helpers/multi_languages_helper.rb @@ -77,15 +77,31 @@ def content_languages(submission = @submission || @submission_latest) # Transform each language into a select option submission_lang = submission_lang.map do |lang| - lang = lang.split('/').last.upcase - lang = ISO_639.find(lang.to_s.downcase) - next nil unless lang - [lang.alpha2, lang.english_name] + code, name = find_language_code_name(lang) + next nil unless code + [code, name] end.compact [submission_lang, current_lang] end + def find_language_code_name(language) + original_lang = language.to_s.split('/').last.upcase + lang, country = original_lang.split('-') + + if country + lang = ISO3166::Country.find_country_by_alpha2(country) + return nil unless lang + + [original_lang, lang.nationality] + else + lang = ISO_639.find(lang.to_s.downcase) + return nil unless lang + + [lang.alpha2, lang.english_name] + end + end + def content_language_help_text content_tag(:div, style: 'width: 350px;') do concat content_tag(:div, t('language.content_language_help_text_1')) From 47c88488a405053193c5c7f603f9b0d953c1d40c Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:39:05 +0200 Subject: [PATCH 23/45] Feature: Allow multiple parentCategories (#772) * update categories selector in admin page to allow multiple parentCategories * handle category multiple parents in category page * add null safety in nest categories children function --- app/controllers/admin/categories_controller.rb | 2 +- app/controllers/taxonomy_controller.rb | 7 +++---- app/helpers/application_helper.rb | 2 +- app/views/admin/categories/_form.html.haml | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/controllers/admin/categories_controller.rb b/app/controllers/admin/categories_controller.rb index f2c0d3ea00..1e3ba2ed8b 100644 --- a/app/controllers/admin/categories_controller.rb +++ b/app/controllers/admin/categories_controller.rb @@ -122,7 +122,7 @@ def unescape_id end def category_params - params.require(:category).permit(:acronym, :name, :description, :parentCategory, {ontologies:[]}).to_h + params.require(:category).permit(:acronym, :name, :description, {parentCategory: []}, {ontologies:[]}).to_h end def _categories diff --git a/app/controllers/taxonomy_controller.rb b/app/controllers/taxonomy_controller.rb index 06832abe8b..b3afa2542a 100644 --- a/app/controllers/taxonomy_controller.rb +++ b/app/controllers/taxonomy_controller.rb @@ -8,7 +8,6 @@ def index end private - def initialize_taxonomy @groups = LinkedData::Client::Models::Group.all slices = LinkedData::Client::Models::Slice.all @@ -33,13 +32,13 @@ def nest_categories_children(categories) category_index[category[:id]] = category end categories.each do |category| - if category.parentCategory - parent = category_index[category.parentCategory] + category[:parentCategory].each do |parent_id| + parent = category_index[parent_id] parent[:children] ||= [] parent[:children] << category end end - categories.reject! { |category| category.parentCategory } + categories.reject! { |category| category[:parentCategory]&.any? } categories end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c134d5d96c..a349b0c6ce 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -546,6 +546,6 @@ def cancel_button_component(class_name: nil, id: , value:, data: nil) def categories_select(id: nil, name: nil, selected: 'None') categories_for_select = LinkedData::Client::Models::Category.all.map{|x| ["#{x.name} (#{x.acronym})", x.id]}.unshift(["None", '']) - render Input::SelectComponent.new(id: id, name: name, value: categories_for_select, selected: selected) + render Input::SelectComponent.new(id: id, name: name, value: categories_for_select, selected: selected, multiple: true) end end diff --git a/app/views/admin/categories/_form.html.haml b/app/views/admin/categories/_form.html.haml index 75974b5a3a..fff9d1a6cf 100644 --- a/app/views/admin/categories/_form.html.haml +++ b/app/views/admin/categories/_form.html.haml @@ -34,7 +34,7 @@ %th = t('admin.categories.form.parent_category') %td.top - = categories_select(id: 'category_parent_select', name: 'category[parentCategory]', selected: @category&.parentCategory) + = categories_select(id: 'category_parent_select', name: 'category[parentCategory][]', selected: @category&.parentCategory) - unless new_record %tr %th From bbe9fd70bf3c80befd0845a75d47fcf480d0ed49 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Mon, 14 Oct 2024 14:41:40 +0200 Subject: [PATCH 24/45] Fix: browse page white page issue (#774) * ensure that the custom ontology viewer context fix is only applied to it * handle collections sort if all languages mode raising an exception --- app/helpers/collections_helper.rb | 2 +- app/javascript/controllers/turbo_frame_controller.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index 351401a467..72945b3682 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -4,7 +4,7 @@ module CollectionsHelper def get_collections(ontology, add_colors: false) collections = ontology.explore.collections(language: request_lang) generate_collections_colors(collections) if add_colors - collections.sort_by{ |x| x.prefLabel } + collections.sort_by{ |x| main_language_label(x.prefLabel) || '' } if collections end def get_collection(ontology, collection_uri) diff --git a/app/javascript/controllers/turbo_frame_controller.js b/app/javascript/controllers/turbo_frame_controller.js index 6520acb64a..aacc99eca1 100644 --- a/app/javascript/controllers/turbo_frame_controller.js +++ b/app/javascript/controllers/turbo_frame_controller.js @@ -32,7 +32,6 @@ export default class extends Controller { this.urlValue = this.#updatedPageUrl(data) this.frame.src = this.urlValue - } } @@ -44,7 +43,7 @@ export default class extends Controller { if (currentDisplayedUrl.toString().includes(this.urlValue)){ return true - } else if (currentDisplayedUrl.searchParams.get('p') === initUrl.searchParams.get('p')){ + } else if (currentDisplayedUrl.searchParams.has('p') && currentDisplayedUrl.searchParams.get('p') === initUrl.searchParams.get('p')){ // this is a custom fix for only the ontology viewer page, // that use the parameter ?p=section to tell which section is displayed return true From ef836ce19c065d078a747c0721b287b89ea061a9 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:03:54 +0200 Subject: [PATCH 25/45] Feature: Implement federation for the search page (#739) * simplify the header component to use content instead of a new section * update dropdown component to a custom title section instead of text * update browse page to use Dropdown component not bootsrap one * add color option to square badge component * add text and bg colors options to ontology browse card component * add federation helper code and config sample * add portals filters in the browse page * use federation helpers to get federation ontologies information * remove the filter using index code from submission filter as no used * refactor the filter_using_data function to be faster by using an hash * update the browse analytics cache to change depending on portals * add categories and groups ids on hover to know its origin when federated * use the last part of ids for browse counts independently of its origin * remove binding.pry from final federation code * add an error message if one of the external portal is down * display federated results in search page * display federated search results in different colors * add portals param in federated search * add federation checks in search page * merge federated search results * show portals names in federated search result chips in the form 'AgroPortal' instead of 'agroportal' * sort federated search results by string similarity * fix merge development to federated search issues * clean search result component * internationalize: results from other portals in search and browse page * fix performance issue in federated search * merge results using class id and ontology acronym * extract federation enabled into a helper in federated search * refactor search result elem function * add realtime benchmark for search federated search * add portal names and colors in top of federated search results * make sort by string similarity not case sensitive * remove duplicated federation configuration * extract portal button in home page to federation helper, fix the style of it and make it clickable * fix icons colors in federation * display a message in the federated search results when a portal is not responding * disable input chips in federated search page for the portals that are not responding * show tooltip for disabled input chips in the federated search * clean search controller federation code * update ontologies api ruby client to the latest version of federated search * fix federated browse icons colors * add tooltip for portal button in federated search * open external ontologies links in new tab in federated browse * use portal button helper in federated browse * cache federation status call * update chip helpers to support disabled state * use turbo frame for federation input chips, and use federation portals status to disable non working portals * add skelton loading animation for federation input chips * cache federation input chips separately * use cached federation input chips in browse page * fix browse page federation inputs section title style * initialize federation portals input chips in the home page (asynch in the background) * clean federated search aggregator code * move chip skelton to components helper * internationalize portal is not responding message * clean federation portal status cache method * clean federation stimulus controller code * display federated browse errors as warning instead of danger (orange instead of red) * remove related code to fedrated portals status to put it in another PR * use the key "collection" instead of "results" in federated search result hash to maintain the endpoint expected schema * Fix issue when enabling federation and ontologies selector * rename the federation color setter stimulus controller * extract federation errors helpers and a fix a typo in a function name * clean search aggregator by extracting some federation helpers * use again the development branch of the api client * fix some imports and functions calls after the refactor * extract and create federated portal button component * put again custom api documentation line * remove no more used data controller calls * clean the search result component federated code --------- Co-authored-by: Syphax bouazzouni --- Gemfile | 9 ++- Gemfile.lock | 60 +++++++++-------- .../stylesheets/application.css.scss.erb | 1 + app/assets/stylesheets/components/chips.scss | 4 +- .../stylesheets/components/search_result.scss | 3 +- app/assets/stylesheets/federation.scss | 31 +++++++++ app/components/chips_component.rb | 2 +- app/components/display/header_component.rb | 1 + .../display/search_result_component.rb | 27 ++++++-- .../search_result_component.html.haml | 19 ++++-- .../federated_portal_button_component.rb | 13 ++++ ...ederated_portal_button_component.html.haml | 7 ++ ...ated_portal_button_component_controller.js | 18 +++++ .../ontology_browse_card_component.rb | 2 +- .../ontology_browse_card_component.html.haml | 17 ++--- app/controllers/application_controller.rb | 5 ++ app/controllers/concerns/search_aggregator.rb | 66 +++++++++++++++---- app/controllers/home_controller.rb | 2 +- app/controllers/ontologies_controller.rb | 3 - app/controllers/search_controller.rb | 19 ++++-- app/helpers/application_helper.rb | 15 +++-- app/helpers/components_helper.rb | 17 +++-- app/helpers/federation_helper.rb | 36 +++++++++- app/helpers/inputs_helper.rb | 2 +- app/javascript/component_controllers/index.js | 7 +- app/javascript/controllers/index.js | 2 +- .../ontologies/browser/_ontologies.html.haml | 2 +- app/views/ontologies/browser/browse.html.haml | 5 +- app/views/search/index.html.haml | 12 +++- config/bioportal_config_env.rb.sample | 24 +++++-- config/locales/en.yml | 7 +- config/locales/fr.yml | 7 +- 32 files changed, 329 insertions(+), 116 deletions(-) create mode 100644 app/assets/stylesheets/federation.scss create mode 100644 app/components/federated_portal_button_component.rb create mode 100644 app/components/federated_portal_button_component/federated_portal_button_component.html.haml create mode 100644 app/components/federated_portal_button_component/federated_portal_button_component_controller.js diff --git a/Gemfile b/Gemfile index 781e6379e2..b2a3f58053 100644 --- a/Gemfile +++ b/Gemfile @@ -88,12 +88,15 @@ gem 'view_component', '~> 2.72' # Pagination library for Rails gem 'will_paginate', '~> 3.0' +# String similarity, used by federated search to rank results +gem 'string-similarity' + # Render SVG files in Rails views gem 'inline_svg' # ISO language codes and flags -gem 'flag-icons-rails', '~> 3.4' -gem 'iso-639', '~> 0.3.6' +gem "iso-639", "~> 0.3.6" +gem "flag-icons-rails", "~> 3.4" # Custom API client gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'development' @@ -179,4 +182,4 @@ group :test do # Testing framework for Rails gem 'rspec-rails' -end \ No newline at end of file +end diff --git a/Gemfile.lock b/Gemfile.lock index 31fd0e01d0..949528bfb7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git - revision: 670ce6adfe77aeda97974414ab0ed4b6c5dc9469 + revision: 0598e0068e545a5be9bbb501b0155c8bb2d9afb5 branch: development specs: ontologies_api_client (2.2.0) @@ -145,19 +145,20 @@ GEM crass (1.0.6) css_parser (1.17.1) addressable + csv (3.3.0) dalli (3.2.8) date (3.3.4) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) - deepl-rb (2.5.3) + deepl-rb (3.0.2) diff-lcs (1.5.1) docile (1.4.1) domain_name (0.6.20240107) ed25519 (1.3.0) erubi (1.13.0) erubis (2.7.0) - excon (0.111.0) + excon (0.112.0) execjs (2.9.1) faraday (2.0.1) faraday-net_http (~> 2.0) @@ -176,7 +177,7 @@ GEM sass-rails globalid (1.2.1) activesupport (>= 6.1) - graphql (2.3.14) + graphql (2.3.18) base64 fiber-storage graphql-client (0.23.0) @@ -203,7 +204,7 @@ GEM http-accept (1.7.0) http-cookie (1.0.7) domain_name (~> 0.5) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) i18n-tasks (0.9.37) activesupport (>= 4.0.2) @@ -217,7 +218,7 @@ GEM terminal-table (>= 1.5.1) i18n-tasks-csv (1.1) i18n-tasks (~> 0.9) - importmap-rails (2.0.1) + importmap-rails (2.0.3) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) @@ -225,10 +226,11 @@ GEM activesupport (>= 3.0) nokogiri (>= 1.6) io-console (0.7.2) - irb (1.14.0) + irb (1.14.1) rdoc (>= 4.0.0) reline (>= 0.4.2) - iso-639 (0.3.6) + iso-639 (0.3.8) + csv jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -245,7 +247,7 @@ GEM bindata faraday (~> 2.0) faraday-follow_redirects - jwt (2.8.2) + jwt (2.9.3) base64 language_server-protocol (3.17.0.3) launchy (3.0.1) @@ -287,12 +289,13 @@ GEM marcel (1.0.4) matrix (0.4.2) method_source (1.1.0) - mime-types (3.5.2) + mime-types (3.6.0) + logger mime-types-data (~> 3.2015) - mime-types-data (3.2024.0903) + mime-types-data (3.2024.1001) mini_mime (1.1.5) minitest (5.25.1) - msgpack (1.7.2) + msgpack (1.7.3) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.4.1) @@ -302,7 +305,7 @@ GEM time net-http (0.3.2) uri - net-imap (0.4.15) + net-imap (0.4.16) date net-protocol net-pop (0.1.2) @@ -315,9 +318,9 @@ GEM net-ssh (>= 5.0.0, < 8.0.0) net-smtp (0.5.0) net-protocol - net-ssh (7.2.3) + net-ssh (7.3.0) netrc (0.11.0) - newrelic_rpm (9.13.0) + newrelic_rpm (9.14.0) nio4r (2.7.3) nokogiri (1.15.6-x86_64-linux) racc (~> 1.4) @@ -328,7 +331,7 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0) version_gem (~> 1.1) - oj (3.16.5) + oj (3.16.6) bigdecimal (>= 3.0) ostruct (>= 0.2) omniauth (2.1.2) @@ -338,8 +341,8 @@ GEM omniauth-github (2.0.1) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) - omniauth-google-oauth2 (1.1.3) - jwt (>= 2.0) + omniauth-google-oauth2 (1.2.0) + jwt (>= 2.9) oauth2 (~> 2.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) @@ -369,7 +372,7 @@ GEM psych (5.1.2) stringio public_suffix (5.1.1) - puma (5.6.8) + puma (5.6.9) nio4r (~> 2.0) racc (1.8.1) rack (2.2.9) @@ -429,14 +432,14 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.3.7) - rouge (4.3.0) + rexml (3.3.8) + rouge (4.4.0) rspec-core (3.13.1) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.1) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (7.0.1) @@ -458,7 +461,7 @@ GEM rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.2) + rubocop-ast (1.32.3) parser (>= 3.3.1.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) @@ -510,24 +513,24 @@ GEM ostruct stimulus-rails (1.3.4) railties (>= 6.0.0) + string-similarity (2.1.0) stringio (3.1.1) temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - terser (1.2.3) + terser (1.2.4) execjs (>= 0.3.0, < 3) thor (1.3.2) tilt (2.4.0) time (0.4.0) date timeout (0.4.1) - turbo-rails (2.0.6) + turbo-rails (2.0.10) actionpack (>= 6.0.0) - activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) uri (0.13.1) version_gem (1.1.4) view_component (2.83.0) @@ -539,7 +542,7 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webmock (3.23.1) + webmock (3.24.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -620,6 +623,7 @@ DEPENDENCIES simplecov-cobertura sprockets-rails stimulus-rails + string-similarity terser turbo-rails tzinfo-data diff --git a/app/assets/stylesheets/application.css.scss.erb b/app/assets/stylesheets/application.css.scss.erb index c5b6723fb3..52b88a0a6d 100755 --- a/app/assets/stylesheets/application.css.scss.erb +++ b/app/assets/stylesheets/application.css.scss.erb @@ -60,6 +60,7 @@ @import "tools"; @import "portal_configuration"; @import "taxonomy"; +@import "federation"; /* Bootstrap and Font Awesome */ @import "bootstrap"; diff --git a/app/assets/stylesheets/components/chips.scss b/app/assets/stylesheets/components/chips.scss index ac1a2a939f..267fbb00db 100644 --- a/app/assets/stylesheets/components/chips.scss +++ b/app/assets/stylesheets/components/chips.scss @@ -32,7 +32,7 @@ } .chips-container.disabled div label > span, .chips-container div label span:has(span.disabled){ - background-color: #f8f9fa !important; + background-color: #f8f9fa !important; } .chips-container div label input[type="checkbox"]:checked ~ span{ @@ -42,4 +42,4 @@ .chips-container div label input[type="checkbox"]:checked ~ span .chips-check-icon{ display:unset; -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/components/search_result.scss b/app/assets/stylesheets/components/search_result.scss index fc99288618..f5d8510458 100644 --- a/app/assets/stylesheets/components/search_result.scss +++ b/app/assets/stylesheets/components/search_result.scss @@ -41,7 +41,7 @@ display: flex; justify-content: center; align-items: center; - border-radius: 4px; + border-radius: 5px; background-color: var(--light-color); padding: 5px 13px; margin-right: 10px; @@ -52,6 +52,7 @@ } .search-result-component .actions .button svg{ width: 16px; + height: 16px; } .search-result-component .actions .button svg path{ diff --git a/app/assets/stylesheets/federation.scss b/app/assets/stylesheets/federation.scss new file mode 100644 index 0000000000..6bb893e251 --- /dev/null +++ b/app/assets/stylesheets/federation.scss @@ -0,0 +1,31 @@ +.federation-portal-button{ + display: flex; + justify-content: center; + align-items: center; + border-radius: 5px; + background-color: var(--light-color); + padding: 5px 13px; + margin-right: 0px; + height: 100%; +} + + +.federation-portal-button:hover{ + cursor: pointer; +} + +.federation-portal-button svg{ + width: 16px; + height: 16px; +} +.federation-portal-button svg path{ + fill: var(--primary-color); +} + +.federation-portal-button .text{ + color: var(--primary-color); + margin-left: 8px; +} +.federation-portal-button.icon-right .text{ + margin:0 8px; +} \ No newline at end of file diff --git a/app/components/chips_component.rb b/app/components/chips_component.rb index 624681f812..b92080d79c 100644 --- a/app/components/chips_component.rb +++ b/app/components/chips_component.rb @@ -13,4 +13,4 @@ def initialize(id:nil, name:, label: nil, value: nil, checked: false, tooltip: def checked? @checked end -end \ No newline at end of file +end diff --git a/app/components/display/header_component.rb b/app/components/display/header_component.rb index d8b8ecebd0..48ae752754 100644 --- a/app/components/display/header_component.rb +++ b/app/components/display/header_component.rb @@ -5,6 +5,7 @@ class Display::HeaderComponent < ViewComponent::Base include ComponentsHelper + def initialize(text: nil, tooltip: nil) super @text = text diff --git a/app/components/display/search_result_component.rb b/app/components/display/search_result_component.rb index 18d56a288e..e5be88256b 100644 --- a/app/components/display/search_result_component.rb +++ b/app/components/display/search_result_component.rb @@ -2,10 +2,13 @@ class Display::SearchResultComponent < ViewComponent::Base include UrlsHelper include ModalHelper include MultiLanguagesHelper + include FederationHelper + include ComponentsHelper 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) + + def initialize(number: 0,title: nil, ontology_acronym: nil ,uri: nil, definition: nil, link: nil, is_sub_component: false, portal_name: nil, portal_color: nil, portal_light_color: nil, other_portals: []) @title = title @uri = uri @definition = definition @@ -13,6 +16,10 @@ def initialize(number: 0,title: nil, ontology_acronym: nil ,uri: nil, definition @is_sub_component = is_sub_component @ontology_acronym = ontology_acronym @number = number.to_s + @portal_name = portal_name + @portal_color = portal_color + @portal_light_color = portal_light_color + @other_portals = other_portals end def sub_component_class @@ -61,12 +68,22 @@ def visualize_button end def reveal_ontologies_button(text,id,icon) - content_tag(:div, class: 'button icon-right', 'data-action': "click->reveal-component#toggle", 'data-id': id) do - inline_svg_tag(icon) + - content_tag(:div, class: 'text') do + content_tag(:div, class: 'button icon-right', 'data-action': "click->reveal-component#toggle", 'data-id': id, style: @portal_color ? "background-color: #{@portal_light_color} !important" : '') do + inline_svg_tag(icon, class: "federated-icon-#{@portal_name}") + + content_tag(:div, class: 'text', style: @portal_color ? "color: #{@portal_color} !important" : '') do text end + - inline_svg_tag("icons/arrow-down.svg") + inline_svg_tag("icons/arrow-down.svg", class: "federated-icon-#{@portal_name}") end end + + def external_class? + !@portal_name.nil? + end + + def all_federated_portals + out = Array(@other_portals) + out.prepend({name: @portal_name, color: @portal_color, light_color: @portal_light_color, link: @link}) if external_class? + out + end end 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 index 9d4d48bc08..4771756fd2 100644 --- a/app/components/display/search_result_component/search_result_component.html.haml +++ b/app/components/display/search_result_component/search_result_component.html.haml @@ -1,20 +1,25 @@ .search-result-component{class: sub_component_class, 'data-controller': 'reveal-component'} - %a.title{href: @link} - = @title + %a.title{href: @link, style: @portal_color ? "color: #{@portal_color} !important" : '', target: @portal_color ? "_blank" : ''} + .d-flex.align-items-center + = @title + = inline_svg_tag 'icons/external-link.svg', class: "ml-1 federated-icon-#{@portal_name} #{@portal_color ? '' : 'd-none'}" + - if @uri .uri = @uri - if @definition - = display_in_multiple_languages(@definition) .actions - = details_button - = visualize_button - = mappings_button + - unless external_class? + = details_button + = visualize_button + = mappings_button - if subresults? = reveal_ontologies_button("#{subresults.size} #{t('search.result_component.more_from_ontology')}", sub_ontologies_id, 'icons/three-dots.svg') - if reuses? = reveal_ontologies_button("#{t('search.result_component.reuses_in')} #{reuses.size} ontologies", reuses_id, 'icons/reuses.svg') + - all_federated_portals.each do |p| + = portal_button(name: p[:name], color: p[:color], light_color: p[:light_color], link: p[:link], tooltip: "Source #{p[:name].humanize.gsub("portal", "Portal")}") - if subresults? .more-from-ontology.d-none{id: sub_ontologies_id} .vertical-line @@ -28,4 +33,4 @@ .search-result-sub-components - reuses.each do |reuse| .search-result-sub-component - = reuse \ No newline at end of file + = reuse diff --git a/app/components/federated_portal_button_component.rb b/app/components/federated_portal_button_component.rb new file mode 100644 index 0000000000..4d8a1e886c --- /dev/null +++ b/app/components/federated_portal_button_component.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class FederatedPortalButtonComponent < ViewComponent::Base + attr_reader :name, :tooltip, :link, :color, :light_color + + def initialize(name:, link:, color:, tooltip:, light_color:) + @name = name + @tooltip = tooltip + @link = link + @color = color + @light_color = light_color + end +end diff --git a/app/components/federated_portal_button_component/federated_portal_button_component.html.haml b/app/components/federated_portal_button_component/federated_portal_button_component.html.haml new file mode 100644 index 0000000000..e45a6f0520 --- /dev/null +++ b/app/components/federated_portal_button_component/federated_portal_button_component.html.haml @@ -0,0 +1,7 @@ +%span{'data-controller': 'federation-portals-colors', + 'data-federation-portals-colors-color-value': color, + 'data-federation-portals-colors-portal-name-value': name.downcase} +%a{ href: link, target: '_blank', 'data-controller' => 'tooltip', title: tooltip, class: 'federation-portal-button button icon-right', style: color ? "background-color: #{light_color} !important" : '' } + = inline_svg_tag('logos/ontoportal.svg', class: "federated-icon-#{name.downcase}") + %div{ class: 'text', style: color ? "color: #{color} !important" : '' } + = name.humanize.gsub("portal", "Portal") diff --git a/app/components/federated_portal_button_component/federated_portal_button_component_controller.js b/app/components/federated_portal_button_component/federated_portal_button_component_controller.js new file mode 100644 index 0000000000..dd6d4a708d --- /dev/null +++ b/app/components/federated_portal_button_component/federated_portal_button_component_controller.js @@ -0,0 +1,18 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static values = { + color: String, + portalName: String, + } + connect() { + this.#initIconsStyle() + } + + #initIconsStyle(){ + const style = document.createElement('style'); + style.innerHTML = `.federated-icon-${this.portalNameValue} path { fill: ${this.colorValue} !important; }\n`; + document.head.appendChild(style); + } + + } diff --git a/app/components/ontology_browse_card_component.rb b/app/components/ontology_browse_card_component.rb index dfc2c05d0e..24d15ab661 100644 --- a/app/components/ontology_browse_card_component.rb +++ b/app/components/ontology_browse_card_component.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class OntologyBrowseCardComponent < ViewComponent::Base - include ApplicationHelper, OntologiesHelper, FederationHelper + include ApplicationHelper, OntologiesHelper, FederationHelper, ComponentsHelper def initialize(ontology: nil, onto_link: nil, text_color: nil, bg_light_color: nil, portal_name: nil) super diff --git a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml index 6a08ccc836..51f7986ea0 100644 --- a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml +++ b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml @@ -3,11 +3,12 @@ .d-flex .browse-ontology-description .browse-ontology-title-bar - %a.browse-ontology-title{:href => onto_link, data: {'turbo': 'false'} , style: style_text} + %a.browse-ontology-title{:href => onto_link, data: {'turbo': 'false'} , style: style_text, target: external_ontology? ? '_blank' : ''} = ontology[:name]+" ("+ontology[:acronym]+")" = private_ontology_icon(ontology[:private]) - if external_ontology? - = render Display::InfoTooltipComponent.new(text: "Federated ontology from #{ontology[:sources].map{|x| link_to(x,x)}.join(', ')}", icon: 'external-link.svg') + %div{class: "federated-icon-#{@portal_name&.downcase} d-inline"} + = render Display::InfoTooltipComponent.new(text: "Federated ontology from #{ontology[:sources].map{|x| link_to(x,x)}.join(', ')}", icon: 'external-link.svg') - if session[:user]&.admin? - ontology_status = status_string(ontology) = render Display::InfoTooltipComponent.new(text: ontology_status, icon: submission_status_icons(ontology_status)) @@ -77,14 +78,8 @@ - ontology[:sources].each do |id| - config = ontology_portal_config(id)&.last || internal_portal_config(id) || {} - unless config.blank? - %div.mx-1{title: content_tag(:div, "Source #{config[:name]}"), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} - = render ChipButtonComponent.new(type: "clickable" , style: style_bg) do - = link_to ontoportal_ui_link(id), target: '_top' do - %span.d-inline - %span.mr-1 - = inline_svg 'logo-white.svg', width: "20", height: "20" - %span - = config[:name] + %span{style: "padding: 3px 0; margin-right: 0.25rem;"} + = portal_button(name: config[:name], color: @text_color, light_color: @bg_light_color, link: ontoportal_ui_link(id), tooltip: "Source #{config[:name]}") - if session[:user]&.admin? %div.mx-1{title: content_tag(:div, debug(ontology), style: 'height: 300px; overflow: scroll'), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} @@ -102,4 +97,4 @@ .two .browse-sket-column-three .one - .two \ No newline at end of file + .two diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 291e2ae33d..b9f6f31d72 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -175,6 +175,7 @@ def bp_config_json def rest_url helpers.rest_url end + def request_portals helpers.request_portals end @@ -440,6 +441,10 @@ def json_link(url, optional_params) optional_params_str = filtered_params.map { |param, value| "#{param}=#{value}" }.join("&") return base_url + optional_params_str + "&apikey=#{$API_KEY}" end + + def set_federated_portals + RequestStore.store[:federated_portals] = params[:portals]&.split(',') + end private def not_found_record(exception) diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index d50d369a8f..a45f0237ee 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -1,5 +1,6 @@ module SearchAggregator - include UrlsHelper, MultiLanguagesHelper + include UrlsHelper, MultiLanguagesHelper, FederationHelper + require 'string-similarity' extend ActiveSupport::Concern BLACKLIST_FIX_STR = [ "https://", @@ -27,11 +28,16 @@ module SearchAggregator 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| + search_results = grouped_results.map do |group| format_search_result(group, all_ontologies) end + + search_results = merge_sort_federated_results(query, search_results) if federation_enabled? + + search_results end def format_search_result(result, ontologies) @@ -40,16 +46,21 @@ def format_search_result(result, ontologies) 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 + 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 merge_sort_federated_results(query, search_results) + search_results = merge_federated_results(search_results) + sort_results_by_string_similarity(query, search_results) + end + def search_concept_label(label) label = language_hash(label) @@ -59,20 +70,25 @@ def search_concept_label(label) pref_lab.downcase.include?(@search_query.downcase) || @search_query.downcase.include?(pref_lab.downcase) end.first || label.first end - + label end - def search_result_elem(class_object, ontology_acronym, title) + def search_result_elem(class_object, ontology_acronym, title) label = search_concept_label(class_object.prefLabel) + request_lang = helpers.request_lang&.eql?("ALL") ? '' : "&language=#{helpers.request_lang}" - { + result = { uri: class_object.id.to_s, - title: title.nil? || title.empty? ? "#{label} - #{ontology_acronym}" : "#{label} - #{title}", + title: title.to_s.empty? ? "#{label} - #{ontology_acronym}" : "#{label} - #{title}", ontology_acronym: ontology_acronym, - link: "/ontologies/#{ontology_acronym}?p=classes&conceptid=#{escape(class_object.id)}#{helpers.request_lang&.eql?("ALL") ? '' : "&language="+helpers.request_lang.to_s}", - definition: class_object.definition + link: "/ontologies/#{ontology_acronym}?p=classes&conceptid=#{escape(class_object.id)}#{request_lang}", + definition: class_object.definition, } + + result.merge!(class_federation_configuration(class_object)) if federation_enabled? + + result end @@ -228,5 +244,27 @@ def blacklist_cls_id_components(cls_id, blacklist_words) stripped_id end -end + def merge_federated_results(search_results) + search_results.each do |element| + element[:root][:other_portals] = [] + element[:reuses].reject! do |reuse| + if (element[:root][:ontology_acronym] == reuse[:root][:ontology_acronym]) && (element[:root][:uri] == reuse[:root][:uri]) + portal_name = reuse[:root][:portal_name] + link = reuse[:root][:link] + element[:root][:other_portals] << {name: portal_name, color: federated_portal_color(portal_name), light_color: federated_portal_light_color(portal_name), link: link} + true + else + false + end + end + end + end + + def sort_results_by_string_similarity(query, search_results) + search_results = search_results.sort_by do |entry| + root_similarity = String::Similarity.cosine(query.downcase, entry[:root][:title].split('-').first.gsub(" ", "").downcase) + -root_similarity + end + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index d7e4beeb31..bc9531e562 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -4,7 +4,7 @@ class HomeController < ApplicationController layout :determine_layout - include FairScoreHelper + include FairScoreHelper, FederationHelper def index @analytics = helpers.ontologies_analytics diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 5a2af639fa..4b3008a37e 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -571,7 +571,4 @@ def search_first_instance_id return !results.blank? ? results.first[:name] : nil end - def set_federated_portals - RequestStore.store[:federated_portals] = params[:portals]&.split(',') - end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 841f0f948a..b6a7c5ede4 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,7 +1,7 @@ require 'uri' class SearchController < ApplicationController - include SearchAggregator, SearchContent + include SearchAggregator, SearchContent, FederationHelper skip_before_action :verify_authenticity_token @@ -13,14 +13,25 @@ def index @advanced_options_open = false @search_results = [] @json_url = json_link("#{rest_url}/search", {}) + params[:portals] = params[:portals]&.join(',') return if @search_query.empty? params[:pagesize] = "150" - results = LinkedData::Client::Models::Class.search(@search_query, params).collection + set_federated_portals + + params[:ontologies] = nil if federation_enabled? + + @time = Benchmark.realtime do + results = LinkedData::Client::Models::Class.search(@search_query, params) + @federation_errors = federation_error(results) if federation_error?(results) + results = results[:collection] + + + @search_results = aggregate_results(@search_query, results) + end @advanced_options_open = !search_params_empty? - @search_results = aggregate_results(@search_query, results) @json_url = json_link("#{rest_url}/search", params.permit!.to_h) end @@ -141,7 +152,7 @@ def search_params [ :ontologies, :categories, :also_search_properties, :also_search_obsolete, :also_search_views, - :require_exact_match, :require_definition + :require_exact_match, :require_definition, :portals ] end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a349b0c6ce..e061a0a05e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,6 +11,11 @@ module ApplicationHelper include ModalHelper, MultiLanguagesHelper, UrlsHelper + def url_to_endpoint(url) + uri = URI.parse(url) + endpoint = uri.path.sub(/^\//, '') + endpoint + end RESOLVE_NAMESPACE = {:omv => "http://omv.ontoware.org/2005/05/ontology#", :skos => "http://www.w3.org/2004/02/skos/core#", :owl => "http://www.w3.org/2002/07/owl#", :rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", :rdfs => "http://www.w3.org/2000/01/rdf-schema#", :metadata => "http://data.bioontology.org/metadata/", :metadata_def => "http://data.bioontology.org/metadata/def/", :dc => "http://purl.org/dc/elements/1.1/", :xsd => "http://www.w3.org/2001/XMLSchema#", @@ -23,17 +28,16 @@ module ApplicationHelper :oboInOwl => "http://www.geneontology.org/formats/oboInOwl#", :idot => "http://identifiers.org/idot/", :sd => "http://www.w3.org/ns/sparql-service-description#", :cclicense => "http://creativecommons.org/licenses/", 'skos-xl' => "http://www.w3.org/2008/05/skos-xl#"} - def url_to_endpoint(url) - uri = URI.parse(url) - endpoint = uri.path.sub(/^\//, '') - endpoint - end def search_json_link(link = @json_url, style: '') custom_style = "font-size: 50px; line-height: 0.5; margin-left: 6px; #{style}".strip render IconWithTooltipComponent.new(icon: "json.svg",link: link, target: '_blank', title: t('fair_score.go_to_api'), size:'small', style: custom_style) end + def portal_name_from_uri(uri) + URI.parse(uri).hostname.split('.').first + end + def resolve_namespaces RESOLVE_NAMESPACE end @@ -155,7 +159,6 @@ def at_slice? !@subdomain_filter.nil? && !@subdomain_filter[:active].nil? && @subdomain_filter[:active] == true end - def add_comment_button(parent_id, parent_type) if session[:user].nil? link_to t('application.add_comment'), login_index_path(redirect: request.url), class: "secondary-button regular-button slim" diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 43e84004d3..e43d546db5 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -1,6 +1,10 @@ module ComponentsHelper include TermsReuses + def portal_button(name: nil , color: nil , light_color: nil, link: nil, tooltip: nil) + render FederatedPortalButtonComponent.new(name: name, color: color, link: link, tooltip: tooltip, light_color: light_color) + end + def tab_item_component(container_tabs:, title:, path:, selected: false, json_link: "", &content) container_tabs.item(title: title.html_safe, path: path, selected: selected, json_link: json_link) container_tabs.item_content { capture(&content) } @@ -38,7 +42,7 @@ def check_resolvability_container(url) end end end - + def search_page_input_component(name:, value: nil, placeholder: , button_icon: 'icons/search.svg', type: 'text', &block) content_tag :div, class: 'search-page-input-container', data: { controller: 'reveal' } do search_input = content_tag :div, class: 'search-page-input' do @@ -72,7 +76,7 @@ def paginated_list_component(id:, results:, next_page_url:, child_url:, child_tu end end) end - + concepts = c.collection if concepts && !concepts.empty? concepts.each do |concept| @@ -113,7 +117,7 @@ def copy_link_to_clipboard(url, show_content: false) end - def generated_link_to_clipboard(url, acronym) + def generated_link_to_clipboard(url, acronym) url = "#{$UI_URL}/ontologies/#{acronym}/#{link_last_part(url)}" content_tag(:span, id: "generate_portal_link", style: 'display: inline-block;') do render ClipboardComponent.new(icon: 'icons/copy_link.svg', title: "#{t("components.copy_portal_uri", portal_name: portal_name)} #{link_to(url)}", message: url, show_content: false) @@ -132,7 +136,7 @@ def htaccess_tag(acronym) def link_to_with_actions(link_to_tag, acronym: nil, url: nil, copy: true, check_resolvability: true, generate_link: true, generate_htaccess: false) tag = link_to_tag url = link_to_tag if url.nil? - + tag += content_tag(:span, class: 'mx-1') do concat copy_link_to_clipboard(url) if copy concat generated_link_to_clipboard(url, acronym) if generate_link @@ -145,11 +149,11 @@ def link_to_with_actions(link_to_tag, acronym: nil, url: nil, copy: true, check_ def tree_component(root, selected, target_frame:, sub_tree: false, id: nil, auto_click: false, submission: nil, &child_data_generator) root.children.sort! { |a, b| (a.prefLabel || a.id).downcase <=> (b.prefLabel || b.id).downcase } - + render TreeViewComponent.new(id: id, sub_tree: sub_tree, auto_click: auto_click) do |tree_child| root.children.each do |child| children_link, data, href = child_data_generator.call(child) - + if children_link.nil? || data.nil? || href.nil? raise ArgumentError, t('components.error_block') end @@ -288,5 +292,4 @@ def form_cancel_button end end - end diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index afd60dfa00..572e26db3e 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -44,16 +44,14 @@ def ontology_portal_config(id) def ontology_portal_name(id) portal_key, _ = ontology_portal_config(id) - portal_key ? federated_portal_name(portal_key) : 'not found' + portal_key ? federated_portal_name(portal_key) : nil end - def ontology_portal_color(id) portal_key, _ = ontology_portal_config(id) federated_portal_color(portal_key) if portal_key end - def ontoportal_ui_link(id) portal_key, config = ontology_portal_config(id) return nil unless portal_key @@ -94,4 +92,36 @@ def request_portals_names content_tag(:span, federated_portal_name(name), style: color ? "color: #{color}" : "", class: color ? "" : "text-primary") end.compact end + + def federation_enabled? + params[:portals] + end + + def federation_error?(response) + !response[:errors].blank? + end + + def federation_error(response) + federation_errors = response[:errors].map{|e| ontology_portal_name(e.split(' ').last.gsub('search', ''))} + federation_errors.map{ |p| "#{p} #{t('federation.not_responding')} " }.join(' ') + end + + def class_federation_configuration(class_object) + is_external = federation_external_class?(class_object) + portal_name = is_external ? helpers.portal_name_from_uri(class_object.links['ui']) : nil + + result = { + portal_name: portal_name, + portal_color: is_external ? federated_portal_color(portal_name) : nil, + portal_light_color: is_external ? federated_portal_light_color(portal_name) : nil + } + result[:link] = class_object.links['ui'] if is_external + result + end + + def federation_external_class?(class_object) + !class_object.links['self'].include?($REST_URL) + end + + end diff --git a/app/helpers/inputs_helper.rb b/app/helpers/inputs_helper.rb index cd4782b6f3..94b677ccd2 100644 --- a/app/helpers/inputs_helper.rb +++ b/app/helpers/inputs_helper.rb @@ -82,4 +82,4 @@ def attribute_error(attr) def input_error_message(name) attribute_error(method_name(name)) end -end \ No newline at end of file +end diff --git a/app/javascript/component_controllers/index.js b/app/javascript/component_controllers/index.js index c0d3bcd20f..bd6df841a2 100644 --- a/app/javascript/component_controllers/index.js +++ b/app/javascript/component_controllers/index.js @@ -25,6 +25,7 @@ import Table_component_controller from '../../components/table_component/table_c import clipboard_component_controller from '../../components/clipboard_component/clipboard_component_controller' import range_slider_component_controller from '../../components/input/range_slider_component/range_slider_component_controller' import RDFHighlighter from '../../components/display/rdf_highlighter_component/rdf_highlighter_component_controller' +import FederationController from "../../components/federated_portal_button_component/federated_portal_button_component_controller" application.register("rdf-highlighter", RDFHighlighter) application.register('turbo-modal', TurboModalController) @@ -35,10 +36,12 @@ application.register('subscribe-notes', Ontology_subscribe_button_component_cont 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('alert-component', alert_component_controller) application.register('progress-pages', Progress_pages_component_controller) application.register('reveal-component', Reveal_component_controller) application.register('table-component', Table_component_controller) application.register('clipboard', clipboard_component_controller) -application.register('range-slider', range_slider_component_controller) \ No newline at end of file + +application.register('range-slider', range_slider_component_controller) +application.register("federation-portals-colors", FederationController) diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 24f39bf453..fa09b85416 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -105,4 +105,4 @@ import MappingsController from "./mappings_visualization_controller" application.register('mappings', MappingsController) import ConceptsJsonButtonController from "./concepts_json_button_controller.js" -application.register('concepts-json', ConceptsJsonButtonController) \ No newline at end of file +application.register('concepts-json', ConceptsJsonButtonController) diff --git a/app/views/ontologies/browser/_ontologies.html.haml b/app/views/ontologies/browser/_ontologies.html.haml index 3ca8685bc0..cadbb85168 100644 --- a/app/views/ontologies/browser/_ontologies.html.haml +++ b/app/views/ontologies/browser/_ontologies.html.haml @@ -8,7 +8,7 @@ #{t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies, portals: request_portals_names.join(', ').html_safe).html_safe} (#{sprintf("%.2f", @time)}s) - unless @errors.blank? %div.my-1 - = render Display::AlertComponent.new(type: 'danger') do + = render Display::AlertComponent.new(type: 'warning') do - @errors.each do |e| %div = e.errors || e diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index fd9777e3a8..34d195e55a 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -71,8 +71,8 @@ = turbo_frame_tag "count_#{key}_#{link_last_part(object["id"])}", busy: true %span.show-if-loading = render LoaderComponent.new(small:true) - %div{data:{controller: "show-filter-count browse-filters", action: "change->show-filter-count#updateCount change->browse-filters#dispatchFilterEvent"}, id: "portals_filter_container"} - = render DropdownContainerComponent.new(id: "browse-portal-filter", is_open: !request_portals.empty?, title: "Results from external portals") do + .browse-federation-input-chip-container + = render DropdownContainerComponent.new(id: "browse-portal-filter", is_open: !request_portals.empty?, title: t('federation.results_from_external_portals')) do .browse-filter-checks-container.px-1 - federated_portals.each do |key, config| = group_chip_component(name: "portals", object: { "acronym" => config[:name], "value" => key }, checked: request_portals.include?(key.to_s), title: '') @@ -91,4 +91,3 @@ - list.loader do = browser_counter_loader - ontologies_browse_skeleton - diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index 28ecbf2eed..d5f117cf13 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -15,7 +15,12 @@ = t("search.advanced_options.ontologies") .field = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) - + .filter-container + .title + = t('federation.results_from_external_portals') + .field.d-flex + - federated_portals.each do |key, config| + = group_chip_component(name: "portals[]", object: { "acronym" => config[:name], "value" => key }, checked: request_portals.include?(key.to_s), title: '') .right .filter-container .title @@ -35,7 +40,7 @@ .search-page-options{class: @search_results.empty? ? 'justify-content-end': ''} - unless @search_results.empty? .search-page-number-of-results - = "#{t('search.match_in')} #{@search_results.length} #{t('search.ontologies')}" + = "#{t('search.match_in')} #{@search_results.length} #{t('search.ontologies')} #{t('federation.from')} #{request_portals_names.join(', ').html_safe} (#{sprintf("%.2f", @time)}s)".html_safe %div.d-flex .search-page-json.mx-4.mt-1 = search_json_link @@ -49,6 +54,8 @@ =inline_svg_tag 'icons/hide.svg' .text = t('search.hide_advanced_options') + - unless @federation_errors.blank? + = render Display::AlertComponent.new(message: @federation_errors, type: "warning") - if @search_results .search-page-results-container - number = 0 @@ -69,4 +76,3 @@ .browse-empty-illustration %img{:src => "#{asset_path("empty-box.svg")}"} %p No result was found - \ No newline at end of file diff --git a/config/bioportal_config_env.rb.sample b/config/bioportal_config_env.rb.sample index 1b46a0ebbf..90dad17f3c 100644 --- a/config/bioportal_config_env.rb.sample +++ b/config/bioportal_config_env.rb.sample @@ -211,14 +211,12 @@ $HOME_PAGE_LOGOS = [ } ] - -# Federation configuration $PORTALS_INSTANCES = [ { name: 'AgroPortal', api: 'https://data.agroportal.lirmm.fr', ui: 'https://agroportal.lirmm.fr/', - color: '#349696', + color: '#3CB371', apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', 'light-color': '#F1F6FA', }, @@ -233,14 +231,17 @@ $PORTALS_INSTANCES = [ { name: 'SIFR BioPortal', ui: 'https://bioportal.lirmm.fr/', + api: 'https://data.bioportal.lirmm.fr/', + apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', color: '#74a9cb', + 'light-color': '#E9F2FA', }, { name: 'EcoPortal', ui: 'https://ecoportal.lifewatch.eu/', api: 'https://data.ecoportal.lifewatch.eu/', apikey: "43a437ba-a437-4bf0-affd-ab520e584719", - color: '#0d508a', + color: '#2076C9', 'light-color': '#E9F2FA', }, { @@ -256,26 +257,37 @@ $PORTALS_INSTANCES = [ { name: 'IndustryPortal', ui: 'http://industryportal.enit.fr', + api: 'https://data.industryportal.enit.fr/', + apikey: '019adb70-1d64-41b7-8f6e-8f7e5eb54942', color: '#1c0f5d', + 'light-color': '#F0F5F6', }, { name: 'EarthPortal', ui: 'https://earthportal.eu/', api: 'https://data.earthportal.eu/', apikey: "c9147279-954f-41bd-b068-da9b0c441288", - color: '#1e2251', + color: '#404696', 'light-color': '#F0F5F6' }, + { + name: 'TestPortal', + ui: 'https://testportal.lirmm.fr/', + api: 'https://data.testportal.lirmm.fr/', + color: '#74a9cb', + apikey: '1de0a270-29c5-4dda-b043-7c3580628cd5', + }, { name: 'BiodivPortal', ui: 'https://biodivportal.gfbio.org/', api: 'https://data.biodivportal.gfbio.org/', apikey: "47a57aa3-7b54-4f34-b695-dbb5f5b7363e", - color: '#33691B', + color: '#349696', 'light-color': '#EBF5F5', } ] + $ONTOPORTAL_WEBSITE_LINK = "https://ontoportal.org/" $ONTOPORTAL_GITHUB_REPO = "https://github.com/ontoportal" diff --git a/config/locales/en.yml b/config/locales/en.yml index fe6724cc51..e99e3420c6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1501,4 +1501,9 @@ en: taxonomy: groups_and_categories: Groups and Categories description: In AgroPortal, ontologies are organized in groups and tagged with categories. Typically, groups associate ontologies from the same project or organization for better identification of the provenance. Whereas categories are about subjects/topics and enable to classify ontologies. As of 2016, AgroPortal's categories were established in cooperation with FAO AIMS. In 2024, we moved to UNESCO nomenclature for fields of science and technology. Groups and categories, along with other metadata, can be used on the “Browse” page of AgroPortal to filter out the list of ontologies. - show_sub_categories: Show sub categories \ No newline at end of file + show_sub_categories: Show sub categories + + federation: + results_from_external_portals: Results from external portals + from: from + not_responding: is not responding. \ No newline at end of file diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9eebb36316..d5b33a5d7c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1539,4 +1539,9 @@ fr: taxonomy: groups_and_categories: Groupes et Catégories description: Dans AgroPortal, les ontologies sont organisées en groupes et étiquetées avec des catégories. Typiquement, les groupes associent des ontologies provenant du même projet ou de la même organisation pour une meilleure identification de la provenance. Tandis que les catégories concernent des sujets/thématiques et permettent de classifier les ontologies. En 2016, les catégories d'AgroPortal ont été établies en coopération avec FAO AIMS. En 2024, nous sommes passés à la nomenclature de l'UNESCO pour les domaines des sciences et des technologies. Les groupes et les catégories, ainsi que d'autres métadonnées, peuvent être utilisés sur la page “Parcourir” d'AgroPortal pour filtrer la liste des ontologies. - show_sub_categories: Afficher les sous-catégories \ No newline at end of file + show_sub_categories: Afficher les sous-catégories + federation: + results_from_external_portals: Résultats provenant de portails externes + from: de + not_responding: ne répond pas. + From 7eb8d766bf5b32337b036ae2b5b88035ef4f9463 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:24:30 +0200 Subject: [PATCH 26/45] Feature: Add federation portals status check (#769) * cache federation status call * update chip helpers to support disabled state * use turbo frame for federation input chips, and use federation portals status to disable non working portals * add skelton loading animation for federation input chips * cache federation input chips separately * use cached federation input chips in browse page * fix browse page federation inputs section title style * initialize federation portals input chips in the home page (asynch in the background) * clean federated search aggregator code * move chip skelton to components helper * internationalize portal is not responding message * clean federation portal status cache method * clean federation stimulus controller code * display federated browse errors as warning instead of danger (orange instead of red) * use federation portal status in home page portals configuration * fix issue in federation portal status method * clean portal_config_tooltip method to make it more readable * consider the portal as down if the api of it is not present in the config file * put federation portal status vue in a helper, and remove the html file of it * internationalize portals status message * extract federation_input_chips to a helper * put init portals status in home page logic in a helper for clarity * change home/federation_portals_status root to status/:portal_name * use dig in federation_portal_status to ensure to not raise an exception * move init portal status helper to federation file and fix class name * move federation helpers from application helper to federation file and fix variables calls * add loading state to the chips component and use it in federation status * rename federation chip component helper name to prevent conflicts --------- Co-authored-by: Syphax bouazzouni --- app/assets/stylesheets/browse.scss | 19 +++++++ app/assets/stylesheets/components/chips.scss | 37 +++++++++++++ app/components/chips_component.rb | 12 +++- .../chips_component/chips_component.html.haml | 20 ++++--- app/controllers/concerns/search_aggregator.rb | 1 - app/controllers/home_controller.rb | 9 +++ app/helpers/application_helper.rb | 1 + app/helpers/components_helper.rb | 18 ++++-- app/helpers/federation_helper.rb | 55 +++++++++++++++++++ app/helpers/home_helper.rb | 14 ++++- app/helpers/inputs_helper.rb | 4 +- app/views/home/index.html.haml | 3 +- app/views/ontologies/browser/browse.html.haml | 5 +- app/views/search/index.html.haml | 3 +- config/locales/en.yml | 3 +- config/locales/fr.yml | 2 +- config/routes.rb | 1 + 17 files changed, 181 insertions(+), 26 deletions(-) diff --git a/app/assets/stylesheets/browse.scss b/app/assets/stylesheets/browse.scss index 2dc96db265..a55292bde0 100644 --- a/app/assets/stylesheets/browse.scss +++ b/app/assets/stylesheets/browse.scss @@ -451,6 +451,25 @@ margin-top: 10px; } +.browse-federation-input-chips{ + display: flex; + flex-wrap: wrap; + margin: 0 15px 15px 15px; + div { + flex-grow: 1; + } + + label{ + display: grid; + } +} +.browse-federation-input-chip-container{ + p{ + font-size: 14px; + font-weight: 400; + color: #666666; + } +} @media only screen and (max-width: 1250px) { .browse-first-row { diff --git a/app/assets/stylesheets/components/chips.scss b/app/assets/stylesheets/components/chips.scss index 267fbb00db..13c15630d2 100644 --- a/app/assets/stylesheets/components/chips.scss +++ b/app/assets/stylesheets/components/chips.scss @@ -32,6 +32,7 @@ } .chips-container.disabled div label > span, .chips-container div label span:has(span.disabled){ + opacity: 60%; background-color: #f8f9fa !important; } @@ -43,3 +44,39 @@ .chips-container div label input[type="checkbox"]:checked ~ span .chips-check-icon{ display:unset; } + +.chips-container.loading div { + cursor: default; + opacity: 0.6; +} + +.chips-container .skeleton { + width: 80px; + height: 36px; + background-color: #e0e0e0; + border-radius: 5px; + animation: shimmer 4s infinite; + position: relative; + overflow: hidden; +} + +@keyframes shimmer { + 0% { + background-position: -200px 0; + } + 100% { + background-position: 200px 0; + } +} + +.chips-container .skeleton::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.6) 50%, rgba(255, 255, 255, 0.2) 100%); + animation: shimmer 4s infinite; + border-radius: 5px; +} diff --git a/app/components/chips_component.rb b/app/components/chips_component.rb index b92080d79c..101643a865 100644 --- a/app/components/chips_component.rb +++ b/app/components/chips_component.rb @@ -1,16 +1,26 @@ class ChipsComponent < ViewComponent::Base renders_one :count - def initialize(id:nil, name:, label: nil, value: nil, checked: false, tooltip: nil) + def initialize(id:nil, name:, label: nil, value: nil, checked: false, tooltip: nil, disabled: false, loading: false) @id = id || name @name = name @value = value || 'true' @checked = checked @label = label || @value @tooltip = tooltip + @disabled = disabled + @loading = loading end def checked? @checked end + + def disabled_class_name + @disabled ? 'disabled' : '' + end + + def loading_class_name + @loading ? 'loading' : '' + end end diff --git a/app/components/chips_component/chips_component.html.haml b/app/components/chips_component/chips_component.html.haml index f08d5a33dd..74acecd425 100644 --- a/app/components/chips_component/chips_component.html.haml +++ b/app/components/chips_component/chips_component.html.haml @@ -1,9 +1,13 @@ -.chips-container{class: @disabled ? 'disabled' : '', 'data-controller': 'tooltip', title: @tooltip} +.chips-container{class: "#{disabled_class_name} #{loading_class_name}", 'data-controller': 'tooltip', title: @tooltip} %div - %label{:for => "chips-#{@id}-check"} - %input{:id => "chips-#{@id}-check", :name => @name, :type => "checkbox", :value => @value, checked: checked?, disabled: @disabled} - %span - = inline_svg_tag 'check.svg', class: 'chips-check-icon' - %div - = @label - = count + - if @loading + %label + %span.skeleton + - else + %label{:for => "chips-#{@id}-check"} + %input{:id => "chips-#{@id}-check", :name => @name, :type => "checkbox", :value => @value, checked: checked?, disabled: @disabled} + %span + = inline_svg_tag 'check.svg', class: 'chips-check-icon' + %div + = @label + = count diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index a45f0237ee..e8a72a74ed 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -91,7 +91,6 @@ def search_result_elem(class_object, ontology_acronym, title) result end - def ontology_name_acronym(ontologies, selected_acronym) ontology = ontologies.select { |x| x.acronym.eql?(selected_acronym.split('/').last) }.first "#{ontology.name} (#{ontology.acronym})" if ontology diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index bc9531e562..772d6e52fc 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -166,6 +166,15 @@ def annotator_recommender_form end end + def federation_portals_status + @name = params[:name] + @acronym = params[:acronym] + @key = params[:portal_name] + @checked = params[:checked].eql?('true') + @portal_up = federation_portal_status(portal_name: @key.downcase.to_sym) + render inline: helpers.federation_chip_component(@key, @name, @acronym, @checked, @portal_up) + end + private # Dr. Musen wants 5 specific groups to appear first, sorted by order of importance. diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e061a0a05e..81a97e01fc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -551,4 +551,5 @@ def categories_select(id: nil, name: nil, selected: 'None') categories_for_select = LinkedData::Client::Models::Category.all.map{|x| ["#{x.name} (#{x.acronym})", x.id]}.unshift(["None", '']) render Input::SelectComponent.new(id: id, name: name, value: categories_for_select, selected: selected, multiple: true) end + end diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index e43d546db5..6d69ad2ad3 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -14,19 +14,19 @@ def alert_component(message, type: "info") render Display::AlertComponent.new(type: type, message: message) end - def chips_component(id: , name: , label: , value: , checked: false , tooltip: nil, &block) + def chips_component(id: , name: , label: , value: , checked: false , tooltip: nil, disabled: false, &block) content_tag(:div, data: { controller: 'tooltip' }, title: tooltip) do - check_input(id: id, name: name, value: value, label: label, checked: checked, &block) + check_input(id: id, name: name, value: value, label: label, checked: checked, disabled: disabled, &block) end end - def group_chip_component(id: nil, name: , object: , checked: , value: nil, title: nil, &block) + def group_chip_component(id: nil, name: , object: , checked: , value: nil, title: nil, disabled: false, &block) title ||= object["name"] value ||= (object["value"] || object["acronym"] || object["id"]) chips_component(id: id || value, name: name, label: object["acronym"], checked: checked, - value: value, tooltip: title, &block) + value: value, tooltip: title, disabled: disabled, &block) end alias :category_chip_component :group_chip_component @@ -292,4 +292,14 @@ def form_cancel_button end end + def chips_skelton + content_tag(:div, class: 'chips-container loading') do + content_tag(:div) do + content_tag(:label) do + content_tag(:span, '', class: 'skeleton') + end + end + end + end + end diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index 572e26db3e..d1e9e9f755 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -124,4 +124,59 @@ def federation_external_class?(class_object) end + def federation_portal_status(portal_name: nil) + Rails.cache.fetch("federation_portal_up_#{portal_name}", expires_in: 2.hours) do + portal_api = federated_portals&.dig(portal_name,:api) + return false unless portal_api + portal_up = false + begin + response = Faraday.new(url: portal_api) do |f| + f.adapter Faraday.default_adapter + f.request :url_encoded + f.options.timeout = 20 + f.options.open_timeout = 20 + end.head + portal_up = response.success? + rescue StandardError => e + Rails.logger.error("Error checking portal status for #{portal_name}: #{e.message}") + end + portal_up + end + end + + def federation_chip_component(key, name, acronym, checked, portal_up) + render TurboFrameComponent.new(id:"federation_portals_status_#{key}") do + content_tag(:div, style: "cursor: default;") do + title = "#{!portal_up ? "#{key.humanize.gsub('portal', 'Portal')} #{t('federation.not_responding')}" : ''}" + group_chip_component(name: name, + object: { "acronym" => acronym, "value" => key }, + checked: checked, + title: title , + disabled: !portal_up) + end + end + end + + def federation_input_chips(name: nil) + federated_portals.map do |key, config| + turbo_frame_component = TurboFrameComponent.new( + id: "federation_portals_status_#{key}", + src: "status/#{key}?name=#{name}&acronym=#{config[:name]}&checked=#{request_portals.include?(key.to_s)}" + ) + + content_tag :div do + render(turbo_frame_component) do |container| + container.loader do + render ChipsComponent.new(name: '', loading: true, tooltip: t('federation.check_status', portal: key.to_s.humanize.gsub('portal', 'Portal'))) + end + end + end + end.join.html_safe + end + + def init_federation_portals_status + content_tag(:div, class: 'd-none') do + federation_input_chips + end + end end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index 0a5498b01d..2ddcdc5275 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -23,12 +23,21 @@ def format_number_abbreviated(number) end def portal_config_tooltip(portal_name, &block) - title = render TurboFrameComponent.new(id: "portal_config_tooltip_#{portal_name&.downcase}", src: "/config?portal=#{portal_name&.downcase}", style: "width: 600px !important; max-height: 300px; overflow: scroll") + portal_id = portal_name&.downcase + title = if federation_portal_status(portal_name: portal_id) + render( + TurboFrameComponent.new( + id: "portal_config_tooltip_#{portal_id}", + src: "/config?portal=#{portal_id}", + style: "width: 600px !important; max-height: 300px; overflow: scroll" + ) + ) + end render Display::InfoTooltipComponent.new(text: title, interactive: true) do capture(&block) end end - + def discover_ontologies_button render Buttons::RegularButtonComponent.new(id: 'discover-ontologies-button', value: t('home.discover_ontologies_button'), variant: "secondary", state: "regular", href: "/ontologies") do |btn| btn.icon_right do @@ -43,4 +52,5 @@ def home_ontoportal_description content_tag(:div, t('home.ontoportal_description', ontoportal_link: ontoportal_link, github_link: github_link).html_safe, style: "margin-bottom: 20px") end + end diff --git a/app/helpers/inputs_helper.rb b/app/helpers/inputs_helper.rb index 94b677ccd2..242de76990 100644 --- a/app/helpers/inputs_helper.rb +++ b/app/helpers/inputs_helper.rb @@ -28,8 +28,8 @@ def number_input(name: , label: '', value: ) value: value) end - def check_input(id:, name:, value:, label: '', checked: false, &block) - render ChipsComponent.new(name: name, id: id, label: label, value: value, checked: checked) do |c| + def check_input(id:, name:, value:, label: '', checked: false, disabled: false, &block) + render ChipsComponent.new(name: name, id: id, label: label, value: value, checked: checked, disabled: disabled) do |c| if block_given? capture(c, &block) end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index fd32acc016..28a0a25504 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -230,6 +230,8 @@ %a{href:logo[:url], target: "_blanc"} %img{src: asset_path(logo[:img_src])} += init_federation_portals_status + :javascript function submitAnnotator(){ document.getElementById("annotator_submit").click() @@ -237,4 +239,3 @@ function submitRecommender(){ document.getElementById("recommender_submit").click() } - diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index 34d195e55a..baca3ce7b8 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -73,9 +73,8 @@ = render LoaderComponent.new(small:true) .browse-federation-input-chip-container = render DropdownContainerComponent.new(id: "browse-portal-filter", is_open: !request_portals.empty?, title: t('federation.results_from_external_portals')) do - .browse-filter-checks-container.px-1 - - federated_portals.each do |key, config| - = group_chip_component(name: "portals", object: { "acronym" => config[:name], "value" => key }, checked: request_portals.include?(key.to_s), title: '') + .px-1.browse-federation-input-chips + = federation_input_chips(name: "portals") .browse-second-row .browse-search-bar diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index d5f117cf13..1fe45b7ae8 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -19,8 +19,7 @@ .title = t('federation.results_from_external_portals') .field.d-flex - - federated_portals.each do |key, config| - = group_chip_component(name: "portals[]", object: { "acronym" => config[:name], "value" => key }, checked: request_portals.include?(key.to_s), title: '') + = federation_input_chips(name: "portals[]") .right .filter-container .title diff --git a/config/locales/en.yml b/config/locales/en.yml index e99e3420c6..cc3d73bb4e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1506,4 +1506,5 @@ en: federation: results_from_external_portals: Results from external portals from: from - not_responding: is not responding. \ No newline at end of file + not_responding: is not responding. + check_status: Checking %{portal} availability diff --git a/config/locales/fr.yml b/config/locales/fr.yml index d5b33a5d7c..b54f2d4430 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1544,4 +1544,4 @@ fr: results_from_external_portals: Résultats provenant de portails externes from: de not_responding: ne répond pas. - + check_status: Vérification de la disponibilité de %{portal} diff --git a/config/routes.rb b/config/routes.rb index b28c26ec02..b3760280fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -131,6 +131,7 @@ end get '' => 'home#index' + get 'status/:portal_name', to: 'home#federation_portals_status' match 'sparql_proxy', to: 'admin#sparql_endpoint', via: [:get, :post] From e8df1678acfb6a1061d822edc1c81abf79244946 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 23 Oct 2024 02:49:42 +0200 Subject: [PATCH 27/45] Fix: federated browse page not updating the categories when portals changed (#779) * sort by name when we do federated browse * extract categories and groups inputs sections into a turbo frame * use switch input helper in the browse page for more clarity * create dropdown component helper and add small argument to loader helper * use current_user_admin? helper instead of session[:user]&.admin? * move some of the browse page code to helpers * fix the bug of mixing the section filters because this.element was the container not the filters section blocks * update the categories section in browse page if request_portal changed --------- Co-authored-by: Syphax --- app/controllers/ontologies_controller.rb | 15 ++- app/helpers/components_helper.rb | 10 +- app/helpers/ontologies_helper.rb | 108 +++++++++++++----- .../controllers/browse_filters_controller.js | 15 ++- app/views/ontologies/browser/browse.html.haml | 54 +++------ 5 files changed, 131 insertions(+), 71 deletions(-) diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 4b3008a37e..81dfe0bc9a 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -49,15 +49,28 @@ def ontologies_filter if @page.page.eql?(1) streams = [prepend("ontologies_list_view-page-#{@page.page}", partial: 'ontologies/browser/ontologies')] + streams += @count_objects.map do |section, values_count| values_count.map do |value, count| replace("count_#{section}_#{link_last_part(value)}") do helpers.turbo_frame_tag("count_#{section}_#{link_last_part(value)}") do - helpers.content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") + helpers.content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") end end end end.flatten + + unless request_portals.empty? + streams += [ + replace('categories_refresh_for_federation') do + key = "categories" + objects, checked_values, _ = @filters[key.to_sym] + helpers.browse_filter_section_body(checked_values: checked_values, + key: key, objects: objects, + counts: @count_objects[key.to_sym]) + end + ] + end else streams = [replace("ontologies_list_view-page-#{@page.page}", partial: 'ontologies/browser/ontologies')] end diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 6d69ad2ad3..5c43b24d8a 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -1,6 +1,12 @@ module ComponentsHelper include TermsReuses + def dropdown_component(id: ,title: nil, tooltip:nil , is_open: false, &block) + render DropdownContainerComponent.new(id: id, title: title, tooltip: tooltip, is_open: is_open) do |d| + capture(d, &block) if block_given? + end + end + def portal_button(name: nil , color: nil , light_color: nil, link: nil, tooltip: nil) render FederatedPortalButtonComponent.new(name: name, color: color, link: link, tooltip: tooltip, light_color: light_color) end @@ -183,8 +189,8 @@ def chart_component(title: '', type:, labels:, datasets:, index_axis: 'x', show_ content_tag(:canvas, nil, data: data) end - def loader_component(type = 'pulsing') - render LoaderComponent.new(type: type) + def loader_component(type:'pulsing', small: false ) + render LoaderComponent.new(type: type, small: small) end def info_tooltip(text, interactive: true) diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index f37836e9fb..f3a7ae05f7 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -71,32 +71,10 @@ def ontology_alternative_names(submission = @submission_latest) end) end end + def private_ontology_icon(is_private) raw(content_tag(:i, '', class: 'fas fa-key', title: t('ontologies.private_ontology'))) if is_private end - def browse_filter_section_label(key) - labels = { - categories: t('ontologies.categories'), - groups: t('ontologies.groups'), - hasFormalityLevel: t('ontologies.formality_levels'), - isOfType: t('ontologies.ontology_types'), - naturalLanguage: t('ontologies.natural_languages') - } - - labels[key] || key.to_s.underscore.humanize.capitalize - end - - def browser_counter_loader - content_tag(:div, class: "browse-desc-text", style: "margin-bottom: 15px;") do - content_tag(:div, class: "d-flex align-items-center") do - str = content_tag(:span, t('ontologies.showing')) - str += content_tag(:span, "", class: "p-1 p-2", style: "color: #a7a7a7;") do - render LoaderComponent.new(small: true) - end - str - end - end - end def ontologies_browse_skeleton(pagesize = 5) pagesize.times do @@ -815,7 +793,6 @@ def n_triples_to_table(n_triples_string) end end - private def submission_languages(submission = @submission) Array(submission&.naturalLanguage).map { |natural_language| natural_language["iso639"] && natural_language.split('/').last }.compact @@ -825,12 +802,87 @@ def id_to_acronym(id) id.split('/').last end - def browse_taxonomy_tooltip(texonomy) - content_tag(:div, class: 'd-flex') do - content_tag(:div, "See more information about #{texonomy} in ", class: 'mr-1') + - content_tag(:a, 'here', href: "/#{texonomy}", target: '_blank') + def browse_taxonomy_tooltip(taxonomy_type) + return nil unless taxonomy_type.eql?("categories") || taxonomy_type.eql?("groups") + + content_tag(:div, class: '') do + content_tag(:span, "See more information about #{taxonomy_type} in ", class: 'mr-1') + + content_tag(:a, 'here', href: "/#{taxonomy_type}", target: '_blank') + end + end + + def browse_chip_filter(key:, object:, values:, countable: true, count: nil) + title = (key.to_s.eql?("categories") || key.to_s.eql?("groups")) ? nil : '' + checked = values.any? { |obj| [link_last_part(object["id"]), link_last_part(object["value"])].include?(obj) } + + group_chip_component(name: key, object: object, checked: checked, title: title) do |c| + c.count { browse_chip_count_badge(key: key, id: object["id"], count: count) } if countable end end + def browse_chip_count_badge(id:, key:, count: nil) + content_tag :span, class: 'badge badge-light ml-1' do + turbo_frame_tag("count_#{key}_#{link_last_part(id)}", busy: true) + + if count || count == 0 + content_tag(:span, count.to_s, class: "hide-if-loading #{count.zero? ? 'disabled' : ''}") + else + content_tag(:span, class: 'show-if-loading') do + loader_component(small: true, type: nil) + end + end + end + end + + def browse_filter_section_label(key) + labels = { + categories: t('ontologies.categories'), + groups: t('ontologies.groups'), + hasFormalityLevel: t('ontologies.formality_levels'), + isOfType: t('ontologies.ontology_types'), + naturalLanguage: t('ontologies.natural_languages') + } + + labels[key] || key.to_s.underscore.humanize.capitalize + end + + def browse_filter_section_header(key: nil, count: nil, title: nil) + render Display::HeaderComponent.new(tooltip: key ? browse_taxonomy_tooltip(key.to_s) : nil) do + content_tag(:span, class: "browse-filter-title-bar") do + concat title || browse_filter_section_label(key) + + concat content_tag(:span, count, class: "badge badge-primary mx-1", + "data-show-filter-count-target": "countSpan", + style: "#{count&.positive? ? '' : 'display: none;'}") + end + + end + end + + def browse_filter_section_body(checked_values: , key:, objects:, countable: true, counts: nil) + output = content_tag(:div, class: "browse-filter-checks-container px-3") do + Array(objects).map do |object| + count = counts ? counts[link_last_part(object["id"])] || 0 : nil + concat browse_chip_filter(key: key, object: object, values: checked_values, countable: countable, count: count) + end + end + + if key.to_s.include?("categories") + turbo_frame_tag('categories_refresh_for_federation') { output.html_safe } + else + output + end + end + + def browser_counter_loader + content_tag(:div, class: "browse-desc-text", style: "margin-bottom: 15px;") do + content_tag(:div, class: "d-flex align-items-center") do + str = content_tag(:span, t('ontologies.showing')) + str += content_tag(:span, "", class: "p-1 p-2", style: "color: #a7a7a7;") do + render LoaderComponent.new(small: true) + end + str + end + end + end end diff --git a/app/javascript/controllers/browse_filters_controller.js b/app/javascript/controllers/browse_filters_controller.js index 74272dc26a..51eff387bb 100644 --- a/app/javascript/controllers/browse_filters_controller.js +++ b/app/javascript/controllers/browse_filters_controller.js @@ -2,6 +2,7 @@ import {Controller} from "@hotwired/stimulus" import debounce from "debounce" // Connects to data-controller="browse-filters" export default class extends Controller { + static targets = ['sort'] initialize() { this.dispatchInputEvent = debounce(this.dispatchInputEvent.bind(this), 700); @@ -46,11 +47,16 @@ export default class extends Controller { filter = "private_only" break; default: - checks = this.#getSelectedChecks().map(x => x.value) + checks = this.#getSelectedChecks(event).map(x => x.value) filter = event.target.name } - this.#dispatchEvent(filter, checks) + event.stopPropagation() + } + + federationChange(event){ + this.sortTarget.value = "ontology_name" + this.sortTarget.dispatchEvent(new Event('change', { bubbles: true })) } @@ -63,11 +69,10 @@ export default class extends Controller { data: data }, bubbles: true }); - this.element.dispatchEvent(customEvent); } - #getSelectedChecks() { - return Array.from(this.element.querySelectorAll('input:checked')) + #getSelectedChecks(event) { + return Array.from(event.currentTarget.querySelectorAll('input:checked')) } } diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index baca3ce7b8..5a5a122080 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -33,48 +33,32 @@ %div{data: { controller: "turbo-frame history browse-filters" , "turbo-frame-url-value": "/ontologies_filter?page=1&#{request.original_url.split('?').last}", action: "change->browse-filters#dispatchFilterEvent changed->history#updateURL changed->turbo-frame#updateFrame"}} .browse-sub-container - .browse-first-row{data:{controller: "browse-filters", action: "change->browse-filters#dispatchFilterEvent changed->history#updateURL"}} + .browse-first-row %div.pt-1 = upload_ontology_button %div{style:'margin-top: 30px'} %p.browse-filters-title= t("ontologies.filters") - - if session[:user]&.admin? + - if current_user_admin? %div.browse-filter.admin-border - = render SwitchInputComponent.new(id:'filter-private', name:'private_only', checked: @show_private_only) do - = t("ontologies.browser.show_private_ontology") + = switch_input(id:'filter-private', name:'private_only', checked: @show_private_only, label: t("ontologies.browser.show_private_ontology")) %div.browse-filter - = render SwitchInputComponent.new(id:'filter-views', name:'views', checked: @show_views) do - = t("ontologies.browser.show_ontology_views") - = render SwitchInputComponent.new(id:'filter-retired', name:'retired',checked: @show_retired) do - = t("ontologies.browser.show_retired_ontologies") + = switch_input(id:'filter-views', name:'views', checked: @show_views, label: t("ontologies.browser.show_ontology_views")) + = switch_input(id:'filter-retired', name:'retired',checked: @show_retired , label: t("ontologies.browser.show_retired_ontologies")) - @filters.each do |key, values| - - if session[:user]&.admin? || key != :missingStatus - .browse-filter{data:{controller: "show-filter-count browse-filters", action: "change->show-filter-count#updateCount change->browse-filters#dispatchFilterEvent"}, id: "#{key}_filter_container", style: "#{"border-color: var(--admin-color);" if key == :missingStatus}"} - .browse-filter-title-bar{"data-target" => "#browse-#{key}-filter", "data-toggle" => "collapse"} - %p - = browse_filter_section_label(key) - %span.badge.badge-primary{"data-show-filter-count-target":"countSpan", style: "#{values[2] && values[2].positive? ? '' : 'display: none;'}"} - = values[2] - .d-flex.align-items-center - - if key.eql?(:categories) || key.eql?(:groups) - .mr-2 - = render Display::InfoTooltipComponent.new(text: browse_taxonomy_tooltip(key.to_s)) - = inline_svg_tag 'arrow-down.svg' - .collapse{id: "browse-#{key}-filter", class: "#{values[2].positive? ? 'show': ''}"} - .browse-filter-checks-container - - values.first.each do |object| - - title = (key.eql?(:categories) || key.eql?(:groups)) ? nil : '' - = group_chip_component(name: key, object: object, checked: values[1].any?(link_last_part(object["id"])) || values[1].any?(link_last_part(object["value"])) , title: title) do |c| - - c.count do - %span.badge.badge-light.ml-1 - = turbo_frame_tag "count_#{key}_#{link_last_part(object["id"])}", busy: true - %span.show-if-loading - = render LoaderComponent.new(small:true) - .browse-federation-input-chip-container - = render DropdownContainerComponent.new(id: "browse-portal-filter", is_open: !request_portals.empty?, title: t('federation.results_from_external_portals')) do - .px-1.browse-federation-input-chips - = federation_input_chips(name: "portals") + %div{ id: "#{key}_filter_container", data:{controller: "browse_filters show-filter-count", + action: "change->show-filter-count#updateCount + change->browse-filters#dispatchFilterEvent"}} + - objects, checked_values, count = values + = dropdown_component(id: "browse-#{key}-filter", is_open: count.positive?) do |d| + - d.title { browse_filter_section_header(key: key, count: count)} + = browse_filter_section_body(key: key, checked_values: checked_values, objects: objects) + + %div{ data:{action: "change->browse-filters#federationChange"}} + = dropdown_component(id: "browse-portal-filter", is_open: !request_portals.empty?) do |d| + - d.title { browse_filter_section_header(title: t('federation.results_from_external_portals'))} + .px-1.browse-federation-input-chips + = federation_input_chips(name: "portals") .browse-second-row .browse-search-bar @@ -83,7 +67,7 @@ .browse-search-filters %select#format.browse-format-filter{:name => "format"} = options_for_select(@formats, @selected_format) - %select#Sort_by.browse-sort-by-filter{:name => "Sort_by"} + %select#Sort_by.browse-sort-by-filter{name: "Sort_by", 'data-browse-filters-target': "sort"} = options_for_select(@sorts_options, @sort_by) .browse-ontologies = render TurboFrameComponent.new(id: "ontologies_list_view-page-1" , src: "/ontologies_filter?page=1&#{request.original_url.split('?').last}", data:{"turbo-frame-target":"frame", "turbo-frame-url-value": "/ontologies_filter"}) do |list| From ea3526a69dc8d930e075ec56db088d22789bf889 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 23 Oct 2024 04:38:58 +0200 Subject: [PATCH 28/45] Feature: Add federation results counts grouped by portal (#778) * add text for api json button in search page, and show it only when the search is performed * count federated search results count by portal * show separate portals federated search result counts * show separate federated browse ontologies counts for each portal * Fix color issue in browse page portal name button * fix federated browse counts function to handle all cases * limit search page results count sentence width * fix api json button non clickable in search page * Fix external ontology icon when the ontology is not external in the browse page * display ontology sources buttons in browse page for internal ontologies that are present in other portals * use helpers for re-used ui components in the search UI page * extract same helper for the error message alert UI component * harmonize federation result text code * fix text_with_icon helper style * remove no more used :from local text key * complete match_in local text * move urls related helpers from application_helper to urls_helpers file * update search aggregator and result component to save ontology id * extract common logic to count ontology id from search and browse results * clean url helpers usages --------- Co-authored-by: Syphax --- app/assets/stylesheets/search.scss | 2 + .../display/search_result_component.rb | 4 +- .../ontology_browse_card_component.rb | 2 +- .../ontology_browse_card_component.html.haml | 12 ++-- app/controllers/concerns/search_aggregator.rb | 8 +-- app/controllers/concerns/submission_filter.rb | 4 +- app/controllers/ontologies_controller.rb | 4 +- app/controllers/search_controller.rb | 1 + app/helpers/application_helper.rb | 49 +-------------- app/helpers/components_helper.rb | 18 +----- app/helpers/federation_helper.rb | 63 +++++++++++++++---- app/helpers/instances_helper.rb | 2 +- app/helpers/mappings_helper.rb | 2 +- app/helpers/urls_helper.rb | 40 ++++++++++++ .../ontologies/browser/_ontologies.html.haml | 18 +++--- app/views/ontologies/browser/browse.html.haml | 2 +- app/views/recommender/index.html.haml | 4 +- app/views/search/index.html.haml | 60 +++++++++--------- config/locales/en.yml | 4 +- config/locales/fr.yml | 4 +- 20 files changed, 160 insertions(+), 143 deletions(-) diff --git a/app/assets/stylesheets/search.scss b/app/assets/stylesheets/search.scss index e66312f7e2..5dbea30de7 100644 --- a/app/assets/stylesheets/search.scss +++ b/app/assets/stylesheets/search.scss @@ -69,6 +69,7 @@ .search-page-advanced-button .text{ margin-left: 10px; color: var(--primary-color); + margin-top: 2px; } .search-page-advanced-button .icon svg path{ @@ -76,6 +77,7 @@ } .search-page-number-of-results{ color: #888888; + max-width: 800px; } .search-page-result-element{ diff --git a/app/components/display/search_result_component.rb b/app/components/display/search_result_component.rb index e5be88256b..4b112b60f8 100644 --- a/app/components/display/search_result_component.rb +++ b/app/components/display/search_result_component.rb @@ -8,13 +8,13 @@ class Display::SearchResultComponent < ViewComponent::Base 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, portal_name: nil, portal_color: nil, portal_light_color: nil, other_portals: []) + def initialize(number: 0,title: nil, ontology_id: nil ,uri: nil, definition: nil, link: nil, is_sub_component: false, portal_name: nil, portal_color: nil, portal_light_color: nil, other_portals: []) @title = title @uri = uri @definition = definition @link = link @is_sub_component = is_sub_component - @ontology_acronym = ontology_acronym + @ontology_acronym = ontology_id&.split('/')&.last @number = number.to_s @portal_name = portal_name @portal_color = portal_color diff --git a/app/components/ontology_browse_card_component.rb b/app/components/ontology_browse_card_component.rb index 24d15ab661..cb40202109 100644 --- a/app/components/ontology_browse_card_component.rb +++ b/app/components/ontology_browse_card_component.rb @@ -17,7 +17,7 @@ def ontology end def external_ontology? - !internal_ontology?(@ontology[:id]) || (Array(@ontology[:sources]).size > 1) + !internal_ontology?(@ontology[:id]) end def onto_link diff --git a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml index 51f7986ea0..4f02e889f4 100644 --- a/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml +++ b/app/components/ontology_browse_card_component/ontology_browse_card_component.html.haml @@ -74,12 +74,12 @@ %span.mx-1{data:{controller:'tooltip'}, title: t('components.view_of_the_ontology', ontology: ontology[:viewOfOnt].split('/').last )} = render ChipButtonComponent.new(type: "clickable", text: t('components.view'), style: style_bg) - - if external_ontology? - - ontology[:sources].each do |id| - - config = ontology_portal_config(id)&.last || internal_portal_config(id) || {} - - unless config.blank? - %span{style: "padding: 3px 0; margin-right: 0.25rem;"} - = portal_button(name: config[:name], color: @text_color, light_color: @bg_light_color, link: ontoportal_ui_link(id), tooltip: "Source #{config[:name]}") + + - ontology[:sources]&.each do |id| + - config = ontology_portal_config(id)&.last || internal_portal_config(id) || {} + - unless config.blank? + %span{style: "padding: 3px 0; margin-right: 0.25rem;"} + = portal_button(name: config[:name], color: config[:color], light_color: config[:"light-color"], link: ontoportal_ui_link(id), tooltip: "Source #{config[:name]}") - if session[:user]&.admin? %div.mx-1{title: content_tag(:div, debug(ontology), style: 'height: 300px; overflow: scroll'), data:{controller: 'tooltip', 'tooltip-interactive-value': 'true'}} diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index e8a72a74ed..71f22ad423 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -44,7 +44,7 @@ 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 + ontology = result.links['ontology'] { root: search_result_elem(result, ontology, ontology_name_acronym(ontologies, ontology)), descendants: same_ont.map { |x| search_result_elem(x, ontology, '') }, @@ -74,14 +74,14 @@ def search_concept_label(label) label end - def search_result_elem(class_object, ontology_acronym, title) + def search_result_elem(class_object, ontology_id, title) label = search_concept_label(class_object.prefLabel) request_lang = helpers.request_lang&.eql?("ALL") ? '' : "&language=#{helpers.request_lang}" - + ontology_acronym = link_last_part(ontology_id) result = { uri: class_object.id.to_s, title: title.to_s.empty? ? "#{label} - #{ontology_acronym}" : "#{label} - #{title}", - ontology_acronym: ontology_acronym, + ontology_id: ontology_id, link: "/ontologies/#{ontology_acronym}?p=classes&conceptid=#{escape(class_object.id)}#{request_lang}", definition: class_object.definition, } diff --git a/app/controllers/concerns/submission_filter.rb b/app/controllers/concerns/submission_filter.rb index b37beeb8d4..2097fbb7e7 100644 --- a/app/controllers/concerns/submission_filter.rb +++ b/app/controllers/concerns/submission_filter.rb @@ -56,7 +56,9 @@ def submissions_paginate_filter(params) count = @page.page.eql?(1) ? count_objects(submissions) : {} - [@page.collection, @page.totalCount, count, filter_params] + federation_counts = federated_browse_counts(submissions) + + [@page.collection, @page.totalCount, count, filter_params, federation_counts] end def ontologies_with_filters_url(filters, page: 1, count: false) diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 81dfe0bc9a..041c1e6361 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -44,7 +44,7 @@ def index def ontologies_filter @time = Benchmark.realtime do - @ontologies, @count, @count_objects, @request_params = submissions_paginate_filter(params) + @ontologies, @count, @count_objects, @request_params, @federation_counts = submissions_paginate_filter(params) end if @page.page.eql?(1) @@ -206,7 +206,7 @@ def schemes @schemes = get_schemes(@ontology) scheme_id = params[:schemeid] || @submission_latest.URI || nil @scheme = scheme_id ? get_scheme(@ontology, scheme_id) : @schemes.first - + render partial: 'ontologies/sections/schemes', layout: 'ontology_viewer' end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index b6a7c5ede4..680d58cce8 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -30,6 +30,7 @@ def index @search_results = aggregate_results(@search_query, results) + @federation_counts = federated_search_counts(@search_results) end @advanced_options_open = !search_params_empty? @json_url = json_link("#{rest_url}/search", params.permit!.to_h) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 81a97e01fc..0150c3b6a3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -11,11 +11,7 @@ module ApplicationHelper include ModalHelper, MultiLanguagesHelper, UrlsHelper - def url_to_endpoint(url) - uri = URI.parse(url) - endpoint = uri.path.sub(/^\//, '') - endpoint - end + RESOLVE_NAMESPACE = {:omv => "http://omv.ontoware.org/2005/05/ontology#", :skos => "http://www.w3.org/2004/02/skos/core#", :owl => "http://www.w3.org/2002/07/owl#", :rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#", :rdfs => "http://www.w3.org/2000/01/rdf-schema#", :metadata => "http://data.bioontology.org/metadata/", :metadata_def => "http://data.bioontology.org/metadata/def/", :dc => "http://purl.org/dc/elements/1.1/", :xsd => "http://www.w3.org/2001/XMLSchema#", @@ -59,18 +55,6 @@ def get_apikey end end - def rest_hostname - extract_hostname(REST_URI) - end - - def extract_hostname(url) - begin - uri = URI.parse(url) - uri.hostname - rescue URI::InvalidURIError - url - end - end def omniauth_providers_info $OMNIAUTH_PROVIDERS @@ -84,11 +68,6 @@ def omniauth_token_provider(strategy) omniauth_provider_info(strategy.to_sym).keys.first end - def encode_param(string) - CGI.escape(string) - end - - def current_user session[:user] end @@ -102,9 +81,6 @@ def child_id(child) child.id.to_s.split('/').last end - - - # Create a popup button with a ? inside to display help when hovered def help_tooltip(content, html_attribs = {}, icon = 'fas fa-question-circle', css_class = nil, text = nil) html_attribs["title"] = content attribs = [] @@ -145,15 +121,6 @@ def onts_for_select onts_for_select end - def link_last_part(url) - return "" if url.nil? - - if url.include?('#') - url.split('#').last - else - url.split('/').last - end - end def at_slice? !@subdomain_filter.nil? && !@subdomain_filter[:active].nil? && @subdomain_filter[:active] == true @@ -186,14 +153,6 @@ def add_proposal_button(parent_id, parent_type) end - def link?(str) - # Regular expression to match strings starting with "http://" or "https://" - link_pattern = /\Ahttps?:\/\// - str = str&.strip - # Check if the string matches the pattern - !!(str =~ link_pattern) - end - def subscribe_button(ontology_id) return if ontology_id.nil? render TurboFrameComponent.new(id: 'subscribe_button', src: ontology_subscriptions_path(ontology_id: ontology_id.split('/').last), class: 'ml-1') do |t| @@ -363,10 +322,6 @@ def help_path(anchor: nil) "#{Rails.configuration.settings.links[:help]}##{anchor}" end - def uri?(url) - url =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/ - end - def extract_label_from(uri) label = uri.to_s.chomp('/').chomp('#') index = label.index('#') @@ -506,7 +461,7 @@ def insert_sample_text_button(text) end end - def empty_state(text) + def empty_state(text = t('no_result_was_found')) content_tag(:div, class:'browse-empty-illustration') do inline_svg_tag('empty-box.svg') + content_tag(:p, text) diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 5c43b24d8a..c135804790 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -34,8 +34,6 @@ def group_chip_component(id: nil, name: , object: , checked: , value: nil, title checked: checked, value: value, tooltip: title, disabled: disabled, &block) end - alias :category_chip_component :group_chip_component - def rdf_highlighter_container(format, content) render Display::RdfHighlighterComponent.new(format: format, text: content) @@ -107,7 +105,6 @@ def paginated_list_component(id:, results:, next_page_url:, child_url:, child_tu end end - def resolvability_check_tag(url) content_tag(:span, check_resolvability_container(url), style: 'display: inline-block;', onClick: "window.open('#{check_resolvability_url(url: url)}', '_blank');") end @@ -122,7 +119,6 @@ def copy_link_to_clipboard(url, show_content: false) end end - def generated_link_to_clipboard(url, acronym) url = "#{$UI_URL}/ontologies/#{acronym}/#{link_last_part(url)}" content_tag(:span, id: "generate_portal_link", style: 'display: inline-block;') do @@ -138,7 +134,6 @@ def htaccess_tag(acronym) end end - def link_to_with_actions(link_to_tag, acronym: nil, url: nil, copy: true, check_resolvability: true, generate_link: true, generate_htaccess: false) tag = link_to_tag url = link_to_tag if url.nil? @@ -274,14 +269,12 @@ def properties_dropdown(id, title, tooltip, properties, is_open: false, &block) end end - def regular_button(id, value, variant: "secondary", state: "regular", size: "slim", &block) render Buttons::RegularButtonComponent.new(id:id, value: value, variant: variant, state: state, size: size) do |btn| capture(btn, &block) if block_given? end end - def form_save_button render Buttons::RegularButtonComponent.new(id: 'save-button', value: t('components.save_button'), variant: "primary", size: "slim", type: "submit") do |btn| btn.icon_left do @@ -298,14 +291,9 @@ def form_cancel_button end end - def chips_skelton - content_tag(:div, class: 'chips-container loading') do - content_tag(:div) do - content_tag(:label) do - content_tag(:span, '', class: 'skeleton') - end - end + def text_with_icon(text:, icon:) + content_tag(:div, class: 'd-flex align-items-center icon') do + inline_svg_tag(icon, height: '18', weight: '18') + content_tag(:div, class: 'text') {text} end end - end diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index d1e9e9f755..6104d89456 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -36,25 +36,24 @@ def federated_portal_light_color(key) config[:'light-color'] if config end - def ontology_portal_config(id) rest_url = id.split('/')[0..-3].join('/') - federated_portals.select{|_, config| config[:api].start_with?(rest_url)}.first + federated_portals.select { |_, config| config[:api].start_with?(rest_url) }.first end def ontology_portal_name(id) - portal_key, _ = ontology_portal_config(id) + portal_key, _ = ontology_portal_config(id) portal_key ? federated_portal_name(portal_key) : nil end def ontology_portal_color(id) - portal_key, _ = ontology_portal_config(id) + portal_key, _ = ontology_portal_config(id) federated_portal_color(portal_key) if portal_key end def ontoportal_ui_link(id) - portal_key, config = ontology_portal_config(id) - return nil unless portal_key + portal_key, config = ontology_portal_config(id) + return nil unless portal_key ui_link = config[:ui] api_link = config[:api] @@ -75,8 +74,8 @@ def request_portals [portal_name] + portals end - def request_portals_names - request_portals.map do |x| + def request_portals_names(counts, time) + output = request_portals.map do |x| config = federated_portal_config(x) if config @@ -89,8 +88,10 @@ def request_portals_names next nil end - content_tag(:span, federated_portal_name(name), style: color ? "color: #{color}" : "", class: color ? "" : "text-primary") - end.compact + content_tag(:span, "#{federated_portal_name(name)} (#{counts[federated_portal_name(name).downcase]})", style: color ? "color: #{color}" : "", class: color ? "" : "text-primary") + end.compact.join(", ") + + "#{output} in #{sprintf("%.2f", time)}s" end def federation_enabled? @@ -102,8 +103,18 @@ def federation_error?(response) end def federation_error(response) - federation_errors = response[:errors].map{|e| ontology_portal_name(e.split(' ').last.gsub('search', ''))} - federation_errors.map{ |p| "#{p} #{t('federation.not_responding')} " }.join(' ') + federation_errors = response[:errors].map { |e| ontology_portal_name(e.split(' ').last.gsub('search', '')) } + federation_errors.map { |p| "#{p} #{t('federation.not_responding')} " }.join(' ') + end + + def alert_message_if_federation_error(errors, &block) + return if errors.blank? + + content_tag(:div, class: 'my-1') do + render Display::AlertComponent.new(type: 'warning') do + capture(&block) + end + end end def class_federation_configuration(class_object) @@ -179,4 +190,32 @@ def init_federation_portals_status federation_input_chips end end + def federated_search_counts(search_results) + ids = search_results.map do |result| + result.dig(:root, :ontology_id) || rest_url + end + counts_ontology_ids_by_portal_name(ids) + end + + def federated_browse_counts(ontologies) + ids = ontologies.map { |ontology| ontology[:id] } + counts_ontology_ids_by_portal_name(ids) + end + + private + + def counts_ontology_ids_by_portal_name(portals_ids) + counts = Hash.new(0) + current_portal, *federation_portals = request_portals + portals_ids.each do |id| + counts[current_portal.downcase] += 1 if id.include?(current_portal.to_s.downcase) + + federation_portals.each do |portal| + portal_api = federated_portals[portal.downcase.to_sym][:api] + counts[portal.downcase] += 1 if id.include?(portal_api) + end + end + + counts + end end diff --git a/app/helpers/instances_helper.rb b/app/helpers/instances_helper.rb index db5170b233..79e5b17586 100644 --- a/app/helpers/instances_helper.rb +++ b/app/helpers/instances_helper.rb @@ -57,7 +57,7 @@ def link_to_property(property, ontology_acronym) end def instance_property_value(property, ontology_acronym) - if uri?(property) + if link?(property) instance, types = get_instance_and_type(property, ontology_acronym) return link_to_instance(instance, ontology_acronym) unless instance.empty? end diff --git a/app/helpers/mappings_helper.rb b/app/helpers/mappings_helper.rb index afc85907a8..bfe583a41b 100644 --- a/app/helpers/mappings_helper.rb +++ b/app/helpers/mappings_helper.rb @@ -141,7 +141,7 @@ def get_mappings_target target_ontology = ontology_to target = concept_to_id else - if helpers.uri?(ontology_to) + if helpers.link?(ontology_to) target_ontology = LinkedData::Client::Models::Ontology.find(ontology_to) else target_ontology = LinkedData::Client::Models::Ontology.find_by_acronym(ontology_to).first diff --git a/app/helpers/urls_helper.rb b/app/helpers/urls_helper.rb index 5e26db110b..b193f0a084 100644 --- a/app/helpers/urls_helper.rb +++ b/app/helpers/urls_helper.rb @@ -1,4 +1,40 @@ module UrlsHelper + def url_to_endpoint(url) + uri = URI.parse(url) + endpoint = uri.path.sub(/^\//, '') + endpoint + end + def rest_hostname + extract_hostname(REST_URI) + end + + def extract_hostname(url) + begin + uri = URI.parse(url) + uri.hostname + rescue URI::InvalidURIError + url + end + end + + def link?(str) + # Regular expression to match strings starting with "http://" or "https://" + link_pattern = /\Ahttps?:\/\// + str = str&.strip + # Check if the string matches the pattern + !!(str =~ link_pattern) + end + + def link_last_part(url) + return "" if url.nil? + + if url.include?('#') + url.split('#').last + else + url.split('/').last + end + end + def escape(string) CGI.escape(string) if string end @@ -6,4 +42,8 @@ def escape(string) def unescape(string) CGI.unescape(string) if string end + + def encode_param(string) + escape(string) + end end diff --git a/app/views/ontologies/browser/_ontologies.html.haml b/app/views/ontologies/browser/_ontologies.html.haml index cadbb85168..b9ba32c32e 100644 --- a/app/views/ontologies/browser/_ontologies.html.haml +++ b/app/views/ontologies/browser/_ontologies.html.haml @@ -5,13 +5,13 @@ - if @page.page.eql?(1) = content_tag(:p, class: "browse-desc-text", style: "margin-bottom: 12px !important;") do - #{t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies, portals: request_portals_names.join(', ').html_safe).html_safe} (#{sprintf("%.2f", @time)}s) - - unless @errors.blank? - %div.my-1 - = render Display::AlertComponent.new(type: 'warning') do - - @errors.each do |e| - %div - = e.errors || e + = t("ontologies.showing_ontologies_size", ontologies_size: @count, analytics_size: @total_ontologies, portals: request_portals_names(@federation_counts, @time)).html_safe + + = alert_message_if_federation_error(@errors) do + - @errors.each do |e| + %div + = e.errors || e + - ontologies = c.collection - ontologies.each do |ontology| - config = ontology_portal_config(ontology[:id])&.last || {} @@ -24,6 +24,4 @@ - c.loader do - ontologies_browse_skeleton - c.error do - .browse-empty-illustration - %img{:src => "#{asset_path("empty-box.svg")}"} - %p No result was found \ No newline at end of file + = empty_state diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index 5a5a122080..e912fe3234 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -1,7 +1,7 @@ .browse-center .browse-container .container.align-alert - - if session[:user]&.admin? + - if current_user_admin? %div{style:'width: 70%;'} = render Display::AlertComponent.new(type: 'info') do %span.d-flex.align-items-center diff --git a/app/views/recommender/index.html.haml b/app/views/recommender/index.html.haml index 5ac33ba53c..57df61d9db 100644 --- a/app/views/recommender/index.html.haml +++ b/app/views/recommender/index.html.haml @@ -78,7 +78,7 @@ - btn.icon_left do = inline_svg_tag "edit.svg" - if @results && @results.empty? - = empty_state(t('no_result_was_found')) + = empty_state - unless @results.nil? || @results.empty? .recommender-page-results .title @@ -122,5 +122,3 @@ = render Buttons::RegularButtonComponent.new(id:'recommender_go_annotator', value: t('recommender.call_annotator'), variant: "secondary", href: "/annotator?text=#{params[:input]}&ontologies=#{params[:ontologies]}", size: "slim", target: '_blank', state: "regular") do |btn| - btn.icon_right do = inline_svg_tag "arrow-right-outlined.svg" - - diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index 1fe45b7ae8..e109c8f180 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -39,39 +39,37 @@ .search-page-options{class: @search_results.empty? ? 'justify-content-end': ''} - unless @search_results.empty? .search-page-number-of-results - = "#{t('search.match_in')} #{@search_results.length} #{t('search.ontologies')} #{t('federation.from')} #{request_portals_names.join(', ').html_safe} (#{sprintf("%.2f", @time)}s)".html_safe + = t('search.match_in', results_size: @search_results.length, portals: request_portals_names(@federation_counts, @time)).html_safe %div.d-flex - .search-page-json.mx-4.mt-1 - = search_json_link + .search-page-advanced-button.mr-4{class: @search_results.blank? ? 'd-none' : ''} + %a.d-flex{href: @json_url, target: '_blank'} + = text_with_icon(text: "API JSON", icon: 'json.svg') + .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 - = t('search.show_advanced_options') + = text_with_icon(text: t('search.show_advanced_options'), icon: 'icons/settings.svg') + .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 - = t('search.hide_advanced_options') - - unless @federation_errors.blank? - = render Display::AlertComponent.new(message: @federation_errors, type: "warning") - - 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| - - number = number + 1 - - c.reuse(r[:root].merge(is_sub_component: true, number: number)) do |b| - - r[:descendants].each { |dd| b.subresult(dd.merge(is_sub_component: true))} + = text_with_icon(text: t('search.hide_advanced_options'), icon: 'icons/hide.svg') + + + + + = alert_message_if_federation_error(@federation_errors) {@federation_errors} + + .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| + - number = number + 1 + - c.reuse(r[:root].merge(is_sub_component: true, number: number)) do |b| + - r[:descendants].each { |dd| b.subresult(dd.merge(is_sub_component: true))} - if @search_results.empty? && !@search_query.empty? - .browse-empty-illustration - %img{:src => "#{asset_path("empty-box.svg")}"} - %p No result was found + = empty_state diff --git a/config/locales/en.yml b/config/locales/en.yml index cc3d73bb4e..4f9ee4c48b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -921,8 +921,7 @@ en: classes_with_definitions: Classes with definitions show_advanced_options: Show options hide_advanced_options: Hide options - match_in: Match in - ontologies: ontologies + match_in: Match in %{results_size} ontologies from %{portals} result_component: details: Details visualize: Vizualize @@ -1505,6 +1504,5 @@ en: federation: results_from_external_portals: Results from external portals - from: from not_responding: is not responding. check_status: Checking %{portal} availability diff --git a/config/locales/fr.yml b/config/locales/fr.yml index b54f2d4430..0b319d391e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -937,8 +937,7 @@ fr: classes_with_definitions: Classes avec définitions show_advanced_options: Afficher les options hide_advanced_options: Cacher les options - match_in: Correspondance dans - ontologies: ontologies + match_in: Correspondance dans %{results_size} ontologies de %{portals} result_component: details: Détails visualize: Visualiser @@ -1542,6 +1541,5 @@ fr: show_sub_categories: Afficher les sous-catégories federation: results_from_external_portals: Résultats provenant de portails externes - from: de not_responding: ne répond pas. check_status: Vérification de la disponibilité de %{portal} From dc448c982ca6e1b58291d7fac2cb641646aeeaa7 Mon Sep 17 00:00:00 2001 From: Bilel KIHAL Date: Wed, 23 Oct 2024 19:03:16 +0200 Subject: [PATCH 29/45] fix issue after merging federation PRs --- app/helpers/federation_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index 6104d89456..1cb1a2279d 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -1,4 +1,5 @@ module FederationHelper + include ApplicationHelper def federated_portals $FEDERATED_PORTALS ||= LinkedData::Client.settings.federated_portals From c52b5c8f5a75d7c8893da8ce1aba45d32062a912 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Thu, 24 Oct 2024 02:59:36 +0200 Subject: [PATCH 30/45] Feature: Add canonical portal for duplicate ontologies (#770) * method to get the canonical portal for an ontology using pullLocation * display federated ontologies in browse page based on the canonical portal * clean canonical portal function in federation helper * extract canonical portal ontology choice in a function for clarity * display search page results based on the canonical portal of the ontology * move federation canonical to federation helper * clean swap portal attributes function in federation helper * fix and clean apply canonical portal function * extract count portals into a separate function from external_canonical_ontology_portal function + apply_canonical_portal function * put again search canonical logic the search_aggregator file and refactor the code * do the search canonical logic only if enabled * fix rest_hostname raising an exception that REST_URI does not exist --------- Co-authored-by: Syphax bouazzouni --- Gemfile | 2 +- app/controllers/concerns/search_aggregator.rb | 44 ++++++++++++++++++- app/controllers/concerns/submission_filter.rb | 8 +--- app/helpers/federation_helper.rb | 29 ++++++++++++ app/helpers/urls_helper.rb | 2 +- 5 files changed, 75 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index b2a3f58053..38db8f1d81 100644 --- a/Gemfile +++ b/Gemfile @@ -100,8 +100,8 @@ gem "flag-icons-rails", "~> 3.4" # Custom API client gem 'ontologies_api_client', git: 'https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git', branch: 'development' - # Ruby 2.7.8 pinned gems (to remove when migrating to Ruby >= 3.0) + gem 'ffi', '~> 1.16.3' gem 'net-ftp', '~> 0.2.0', require: false gem 'net-http', '~> 0.3.2' diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index 71f22ad423..24cc2b292c 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -31,11 +31,15 @@ def aggregate_results(query, results) all_ontologies = LinkedData::Client::Models::Ontology.all(include: 'acronym,name', include_views: true, display_links: false, display_context: false) + search_results = grouped_results.map do |group| format_search_result(group, all_ontologies) end - search_results = merge_sort_federated_results(query, search_results) if federation_enabled? + if federation_enabled? + search_results = merge_sort_federated_results(query, search_results) if federation_enabled? + search_results = swap_canonical_portal_results_first(search_results) + end search_results end @@ -251,7 +255,13 @@ def merge_federated_results(search_results) if (element[:root][:ontology_acronym] == reuse[:root][:ontology_acronym]) && (element[:root][:uri] == reuse[:root][:uri]) portal_name = reuse[:root][:portal_name] link = reuse[:root][:link] - element[:root][:other_portals] << {name: portal_name, color: federated_portal_color(portal_name), light_color: federated_portal_light_color(portal_name), link: link} + element[:root][:other_portals] << { + name: portal_name, + color: federated_portal_color(portal_name), + light_color: federated_portal_light_color(portal_name), + link: link, + ontology_id: reuse[:root][:ontology_id] + } true else false @@ -260,6 +270,36 @@ def merge_federated_results(search_results) end end + def swap_canonical_portal_results_first(search_results) + all_submissions = LinkedData::Client::Models::OntologySubmission.all(include: 'pullLocation', include_views: true, display_links: false, display_context: false) + + search_results.each do |result| + next if result[:root][:portal_name].nil? || result[:root][:other_portals].blank? + + result_ontology_ids = [result[:root][:ontology_id]] + result[:root][:other_portals].map { |p| p[:ontology_id] } + + result_submissions = all_submissions.select do |submission| + result_ontology_ids.any? { |ontology_id| submission.id.include?(ontology_id) } + end + + canonical_portal = most_referred_portal(result_submissions) + is_internal_ontology = result[:root][:portal_name].eql?(canonical_portal.to_s) + + next if canonical_portal.nil? || is_internal_ontology + + canonical_portal_result = result[:root][:other_portals].find { |r| r[:name] == canonical_portal.to_s } + swap_portal_attributes(result[:root], canonical_portal_result) if canonical_portal_result + end + search_results + end + + + def swap_portal_attributes(root_portal, new_portal) + [:link, :portal_name, :portal_color, :portal_light_color].each do |attribute| + root_portal[attribute], new_portal[attribute] = new_portal[attribute], root_portal[attribute] + end + end + def sort_results_by_string_similarity(query, search_results) search_results = search_results.sort_by do |entry| root_similarity = String::Similarity.cosine(query.downcase, entry[:root][:title].split('-').first.gsub(" ", "").downcase) diff --git a/app/controllers/concerns/submission_filter.rb b/app/controllers/concerns/submission_filter.rb index 2097fbb7e7..34ec133480 100644 --- a/app/controllers/concerns/submission_filter.rb +++ b/app/controllers/concerns/submission_filter.rb @@ -70,18 +70,14 @@ def ontologies_with_filters_url(filters, page: 1, count: false) def merge_by_acronym(submissions) merged_submissions = [] submissions.group_by { |x| x[:ontology]&.acronym }.each do |acronym, ontologies| - if ontologies.size.eql?(1) - ontology = ontologies.first - else - ontology = ontologies.select { |x| helpers.internal_ontology?(x[:id]) }.first || ontologies.first - end - + ontology = canonical_ontology(ontologies) ontology[:sources] = ontologies.map { |x| x[:id] } merged_submissions << ontology end merged_submissions end + def filter_submissions(ontologies, query:, status:, show_views:, private_only:, languages:, page_size:, formality_level:, is_of_type:, groups:, categories:, formats:) submissions = LinkedData::Client::Models::OntologySubmission.all(include: BROWSE_ATTRIBUTES.join(','), also_include_views: true, display_links: false, display_context: false) diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index 1cb1a2279d..135cae2883 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -135,6 +135,18 @@ def federation_external_class?(class_object) !class_object.links['self'].include?($REST_URL) end + def canonical_ontology(ontologies) + if ontologies.size.eql?(1) + ontologies.first + else + internal_ontology = ontologies.select { |x| helpers.internal_ontology?(x[:id]) }.first + if internal_ontology + internal_ontology + else + external_canonical_ontology_portal(ontologies) + end + end + end def federation_portal_status(portal_name: nil) Rails.cache.fetch("federation_portal_up_#{portal_name}", expires_in: 2.hours) do @@ -191,6 +203,7 @@ def init_federation_portals_status federation_input_chips end end + def federated_search_counts(search_results) ids = search_results.map do |result| result.dig(:root, :ontology_id) || rest_url @@ -219,4 +232,20 @@ def counts_ontology_ids_by_portal_name(portals_ids) counts end + + def external_canonical_ontology_portal(ontologies) + canonical_portal = most_referred_portal(ontologies) + ontologies.select{|o| o[:id].include?(canonical_portal.to_s)}.first + end + + def most_referred_portal(ontology_submissions) + portal_counts = Hash.new(0) + ontology_submissions.each do |submission| + federated_portals.keys.each do |portal| + portal_counts[portal] += 1 if submission[:pullLocation]&.include?(portal.to_s) + end + end + portal_counts.max_by { |_, count| count }&.first + end + end diff --git a/app/helpers/urls_helper.rb b/app/helpers/urls_helper.rb index b193f0a084..2c149e29d4 100644 --- a/app/helpers/urls_helper.rb +++ b/app/helpers/urls_helper.rb @@ -5,7 +5,7 @@ def url_to_endpoint(url) endpoint end def rest_hostname - extract_hostname(REST_URI) + extract_hostname($REST_URL) end def extract_hostname(url) From 18a4cac1b817a524012c5e936e918525b4de887c Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Thu, 24 Oct 2024 11:04:29 +0200 Subject: [PATCH 31/45] Fix: slices homepage metrics count (#775) * create metrics helper module and implement portal_metrics that handle slices also * update home controller and home index to use the portal_metrics helper * add mappings sliced count * add projects sliced count * use the slice name in figures section title if enabled * fix mappings metrics count when not sliced to includes interportal and external mappings --- app/controllers/application_controller.rb | 17 ------ app/controllers/home_controller.rb | 23 ++------ app/helpers/application_helper.rb | 5 ++ app/helpers/metrics_helper.rb | 69 +++++++++++++++++++++++ app/views/home/index.html.haml | 16 +++--- 5 files changed, 86 insertions(+), 44 deletions(-) create mode 100644 app/helpers/metrics_helper.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b9f6f31d72..7af57ee341 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -387,23 +387,6 @@ def get_apikey() return apikey end - def total_mapping_count - total_count = 0 - - begin - stats = LinkedData::Client::HTTP.get("#{REST_URI}/mappings/statistics/ontologies") - unless stats.blank? - stats = stats.to_h.compact - # Some of the mapping counts are erroneously stored as strings - stats.transform_values!(&:to_i) - total_count = stats.values.sum - end - rescue - LOG.add :error, e.message - end - - return total_count - end def determine_layout if Rails.env.appliance? diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 772d6e52fc..2d20328b74 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,31 +3,15 @@ class HomeController < ApplicationController layout :determine_layout - - include FairScoreHelper, FederationHelper + include FairScoreHelper, FederationHelper,MetricsHelper def index @analytics = helpers.ontologies_analytics - # Calculate BioPortal summary statistics - - @ont_count = if @analytics.empty? - LinkedData::Client::Models::Ontology.all.size - else - @analytics.keys.size - end - metrics = LinkedData::Client::Models::Metrics.all - metrics = metrics.each_with_object(Hash.new(0)) do |h, sum| - h.to_hash.slice(:classes, :properties, :individuals).each { |k, v| sum[k] += v } - end @slices = LinkedData::Client::Models::Slice.all - @cls_count = metrics[:classes] - @individuals_count = metrics[:individuals] - @prop_count = metrics[:properties] - @map_count = total_mapping_count - @projects_count = LinkedData::Client::Models::Project.all.length - @users_count = LinkedData::Client::Models::User.all.length + @metrics = portal_metrics(@analytics) + @upload_benefits = [ t('home.benefit1'), @@ -166,6 +150,7 @@ def annotator_recommender_form end end + def federation_portals_status @name = params[:name] @acronym = params[:acronym] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0150c3b6a3..1ad824206b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -372,6 +372,11 @@ def portal_name $SITE end + def current_slice_name + name = @subdomain_filter[:name] + name.blank? ? nil : name + end + def navitems items = [["/ontologies", t('layout.header.browse')], ["/mappings", t('layout.header.mappings')], diff --git a/app/helpers/metrics_helper.rb b/app/helpers/metrics_helper.rb new file mode 100644 index 0000000000..5eb39444ce --- /dev/null +++ b/app/helpers/metrics_helper.rb @@ -0,0 +1,69 @@ +module MetricsHelper + + def portal_metrics(analytics) + ontologies_acronym = if analytics.empty? + LinkedData::Client::Models::Ontology.all.map { |x| x.acronym } + else + analytics.keys + end + + metrics = ontologies_metrics(ontologies_acronym) + + ont_count = ontologies_acronym.size + cls_count = metrics[:classes] + individuals_count = metrics[:individuals] + prop_count = metrics[:properties] + map_count = total_mapping_count(ontologies_acronym) + projects_count = projects_count(ontologies_acronym) + users_count = LinkedData::Client::Models::User.all.length + + { + ontologies_count: ont_count, + class_count: cls_count, + individuals_count: individuals_count, + properties_count: prop_count, + mappings_count: map_count, + projects_count: projects_count, + users_count: users_count + } + end + + def ontologies_metrics(ontologies_acronym = []) + metrics = LinkedData::Client::Models::Metrics.all + + metrics.each_with_object(Hash.new(0)) do |h, sum| + acronym = h.submission&.first&.split('/')&.dig(-3) + next nil if acronym.nil? + next nil unless ontologies_acronym.blank? || ontologies_acronym.include?(acronym) + + h.to_hash.slice(:classes, :properties, :individuals).each { |k, v| sum[k] += v } + end + end + + private + + def projects_count(ontologies_acronym = []) + projects = LinkedData::Client::Models::Project.all + projects.select! { |p| ontologies_acronym.intersection(p.ontologyUsed.map{|x| x.split('/').last}).any? } unless ontologies_acronym.empty? + projects.size + end + + def total_mapping_count(ontologies_acronym = []) + total_count = 0 + begin + stats = LinkedData::Client::HTTP.get(MappingStatistics::MAPPING_STATISTICS_URL) + unless stats.blank? + stats = stats.to_h.compact + # Some of the mapping counts are erroneously stored as strings + stats.select!{ |acronym, count| ontologies_acronym.include?(acronym.to_s) } if helpers.at_slice? + stats.transform_values!(&:to_i) + total_count = stats.values.sum + end + rescue StandardError => e + LOG.add :error, e.message + end + + total_count + end + +end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 28a0a25504..253e0546ad 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -127,7 +127,7 @@ .home-section %h4 - = t('home.agroportal_figures', site: portal_name) + = t('home.agroportal_figures', site: current_slice_name || portal_name) %hr.home-section-line/ .home-statistics-container .home-statistics @@ -135,43 +135,43 @@ %hr/ %div %h4 - = format_number_abbreviated(@ont_count) + = format_number_abbreviated(@metrics[:ontologies_count]) %p= t("home.ontologies") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@cls_count) + = format_number_abbreviated(@metrics[:class_count]) %p= t("home.classes") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@individuals_count) + = format_number_abbreviated(@metrics[:individuals_count]) %p= t("home.individuals") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@prop_count) + = format_number_abbreviated(@metrics[:properties_count]) %p= t("home.properties") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@projects_count) + = format_number_abbreviated(@metrics[:projects_count]) %p= t("home.projects") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@map_count) + = format_number_abbreviated(@metrics[:mappings_count]) %p= t("home.mappings") .home-statistics-item %hr/ %div %h4 - = format_number_abbreviated(@users_count) + = format_number_abbreviated(@metrics[:users_count]) %p= t("home.users") .home-statistics-link.justify-content-end{style: @analytics.empty? && "visibility: hidden"} = link_to t("home.see_details"),'/statistics', target: "_blank" From 227f5633dc38954153bc00b256dfc3fc54a4d814 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Fri, 25 Oct 2024 07:37:35 +0200 Subject: [PATCH 32/45] Fix: put back category chip component alias to group chip component (#783) --- app/helpers/components_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index c135804790..a63ba8edb5 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -34,6 +34,7 @@ def group_chip_component(id: nil, name: , object: , checked: , value: nil, title checked: checked, value: value, tooltip: title, disabled: disabled, &block) end + alias :category_chip_component :group_chip_component def rdf_highlighter_container(format, content) render Display::RdfHighlighterComponent.new(format: format, text: content) From 817c1f5f1d9cdd97862ca14e9caca292908a011f Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:56:24 +0200 Subject: [PATCH 33/45] Fix: Summary page scroll up auto multiple times (#781) * fix summary page scroll up auto multiple times * clean center tree view code and use it for: 'classes', 'properties', 'schemes', 'collections', 'instances' * revert adding useVisibility, and extract tree_view_pages to a constant --- .../controllers/simple_tree_controller.js | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/app/javascript/controllers/simple_tree_controller.js b/app/javascript/controllers/simple_tree_controller.js index 858bfde2c8..4cece829a7 100644 --- a/app/javascript/controllers/simple_tree_controller.js +++ b/app/javascript/controllers/simple_tree_controller.js @@ -1,4 +1,7 @@ import { Controller } from '@hotwired/stimulus' + +const TREE_VIEW_PAGES = ['classes', 'properties', 'schemes', 'collections', 'instances'] + // Connects to data-controller="simple-tree" export default class extends Controller { @@ -7,17 +10,7 @@ export default class extends Controller { } connect () { - setTimeout(() => { - let activeElem = this.element.querySelector('.tree-link.active'); - if (activeElem) { - activeElem.scrollIntoView({ block: 'center' }); - window.scrollTo({top: 0,}); - if (this.autoClickValue) { - activeElem.click(); - } - } - this.#onClickTooManyChildrenInit(); - }, 0); + this.#centerTreeView() } select (event) { @@ -33,6 +26,29 @@ export default class extends Controller { event.target.nextElementSibling.nextElementSibling.classList.toggle('hidden') } + #centerTreeView() { + setTimeout(() => { + const location = window.location.href; + + const isTreeViewPage = TREE_VIEW_PAGES.some(param => location.includes(`p=${param}`)); + + if (isTreeViewPage) { + const activeElem = this.element.querySelector('.tree-link.active'); + + if (activeElem) { + activeElem.scrollIntoView({ block: 'center' }); + window.scrollTo({ top: 0 }); + + if (this.autoClickValue) { + activeElem.click(); + } + } + + this.#onClickTooManyChildrenInit(); + } + }, 0); + } + #onClickTooManyChildrenInit () { jQuery('.too_many_children_override').live('click', (event) => { event.preventDefault() From a69658dadf13116adaa3c76dcc65977fc38a4240 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:31:23 +0200 Subject: [PATCH 34/45] Feature: Add enable/disable federation option (#780) * add enable/disable federation * hide federation inputs in browse page when federation is disabled * hide federation inputs in search page when federation is disabled * fix federation enabled function name in ontologies controller * put @federation_enabled variable in the right place in search controller index * rename federation_portals_enabled? to federated_request? * remove the usage of "@federation_enabled" to use directly the helper * fix a bug of the url of federation status, to add "/" to sure to always go to the correct route --------- Co-authored-by: Syphax --- app/controllers/concerns/search_aggregator.rb | 6 +++--- app/controllers/search_controller.rb | 2 +- app/helpers/federation_helper.rb | 9 +++++++-- app/views/ontologies/browser/browse.html.haml | 11 ++++++----- app/views/search/index.html.haml | 11 ++++++----- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/app/controllers/concerns/search_aggregator.rb b/app/controllers/concerns/search_aggregator.rb index 24cc2b292c..078c89c5c9 100644 --- a/app/controllers/concerns/search_aggregator.rb +++ b/app/controllers/concerns/search_aggregator.rb @@ -36,8 +36,8 @@ def aggregate_results(query, results) format_search_result(group, all_ontologies) end - if federation_enabled? - search_results = merge_sort_federated_results(query, search_results) if federation_enabled? + if federated_request? + search_results = merge_sort_federated_results(query, search_results) search_results = swap_canonical_portal_results_first(search_results) end @@ -90,7 +90,7 @@ def search_result_elem(class_object, ontology_id, title) definition: class_object.definition, } - result.merge!(class_federation_configuration(class_object)) if federation_enabled? + result.merge!(class_federation_configuration(class_object)) if federated_request? result end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 680d58cce8..9a7efa1514 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -21,7 +21,7 @@ def index set_federated_portals - params[:ontologies] = nil if federation_enabled? + params[:ontologies] = nil if federated_request? @time = Benchmark.realtime do results = LinkedData::Client::Models::Class.search(@search_query, params) diff --git a/app/helpers/federation_helper.rb b/app/helpers/federation_helper.rb index 135cae2883..d961712bc0 100644 --- a/app/helpers/federation_helper.rb +++ b/app/helpers/federation_helper.rb @@ -95,10 +95,15 @@ def request_portals_names(counts, time) "#{output} in #{sprintf("%.2f", time)}s" end - def federation_enabled? + def federated_request? params[:portals] end + def federation_enabled? + !federated_portals.blank? + end + + def federation_error?(response) !response[:errors].blank? end @@ -185,7 +190,7 @@ def federation_input_chips(name: nil) federated_portals.map do |key, config| turbo_frame_component = TurboFrameComponent.new( id: "federation_portals_status_#{key}", - src: "status/#{key}?name=#{name}&acronym=#{config[:name]}&checked=#{request_portals.include?(key.to_s)}" + src: "/status/#{key}?name=#{name}&acronym=#{config[:name]}&checked=#{request_portals.include?(key.to_s)}" ) content_tag :div do diff --git a/app/views/ontologies/browser/browse.html.haml b/app/views/ontologies/browser/browse.html.haml index e912fe3234..d6632fbc27 100644 --- a/app/views/ontologies/browser/browse.html.haml +++ b/app/views/ontologies/browser/browse.html.haml @@ -54,11 +54,12 @@ - d.title { browse_filter_section_header(key: key, count: count)} = browse_filter_section_body(key: key, checked_values: checked_values, objects: objects) - %div{ data:{action: "change->browse-filters#federationChange"}} - = dropdown_component(id: "browse-portal-filter", is_open: !request_portals.empty?) do |d| - - d.title { browse_filter_section_header(title: t('federation.results_from_external_portals'))} - .px-1.browse-federation-input-chips - = federation_input_chips(name: "portals") + - if federation_enabled? + %div{ data:{action: "change->browse-filters#federationChange"}} + = dropdown_component(id: "browse-portal-filter", is_open: !request_portals.empty?) do |d| + - d.title { browse_filter_section_header(title: t('federation.results_from_external_portals'))} + .px-1.browse-federation-input-chips + = federation_input_chips(name: "portals") .browse-second-row .browse-search-bar diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index e109c8f180..a53fccdbea 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -15,11 +15,12 @@ = t("search.advanced_options.ontologies") .field = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) - .filter-container - .title - = t('federation.results_from_external_portals') - .field.d-flex - = federation_input_chips(name: "portals[]") + - if federation_enabled? + .filter-container + .title + = t('federation.results_from_external_portals') + .field.d-flex + = federation_input_chips(name: "portals[]") .right .filter-container .title From d83e3049aeeaf66bcac042b5522c7076afc5a8b0 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:32:36 +0200 Subject: [PATCH 35/45] Fix: Jump to infinit loading after search federation work (#782) * fix jump to infinit loading * search result as an object instead of hash in search controller --- app/controllers/search_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 9a7efa1514..e0d15048da 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -26,7 +26,7 @@ def index @time = Benchmark.realtime do results = LinkedData::Client::Models::Class.search(@search_query, params) @federation_errors = federation_error(results) if federation_error?(results) - results = results[:collection] + results = results.collection @search_results = aggregate_results(@search_query, results) @@ -48,6 +48,7 @@ def json_search params.delete("ontologies") end search_page = LinkedData::Client::Models::Class.search(params[:q], params) + @results = search_page.collection response = "" From fea06125af71bfa1a918cb2c7e61f3b798bebae2 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Sat, 26 Oct 2024 11:01:39 +0200 Subject: [PATCH 36/45] Fix: Include views in ontologies selector of search page (#784) * include views in ontologies selector * refactored onts_for_select to have the option to include views and by default no --------- Co-authored-by: Syphax --- app/helpers/application_helper.rb | 4 ++-- app/views/search/index.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1ad824206b..3f69732afd 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -106,8 +106,8 @@ def error_message_alert end end - def onts_for_select - ontologies ||= LinkedData::Client::Models::Ontology.all(include: "acronym,name") + def onts_for_select(include_views: false) + ontologies ||= LinkedData::Client::Models::Ontology.all({include: "acronym,name", include_views: include_views}) onts_for_select = [['', '']] ontologies.each do |ont| next if ( ont.acronym.nil? or ont.acronym.empty? ) diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index a53fccdbea..c193b6cd0e 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -14,8 +14,8 @@ .title = t("search.advanced_options.ontologies") .field - = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(',')) - - if federation_enabled? + = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(','), ontologies: onts_for_select(include_views: true)) + - if federation_enabled? .filter-container .title = t('federation.results_from_external_portals') From e3a2ba2fdd0c9b615f946ae4815786f05c09c009 Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 28 Oct 2024 11:42:14 +0100 Subject: [PATCH 37/45] Update Gemfile.lock --- Gemfile.lock | 30 +++++++++++++++--------------- app/views/search/index.html.haml | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 03c7f75383..3bb8c68812 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: https://github.com/ontoportal-lirmm/ontologies_api_ruby_client.git - revision: 0598e0068e545a5be9bbb501b0155c8bb2d9afb5 + revision: 7423b46ff6fa7e5ef0f1d36548f7c04466939f71 branch: development specs: ontologies_api_client (2.2.0) @@ -160,8 +160,8 @@ GEM ed25519 (1.3.0) erubi (1.13.0) erubis (2.7.0) - excon (0.112.0) - execjs (2.9.1) + excon (1.0.0) + execjs (2.10.0) faraday (2.0.1) faraday-net_http (~> 2.0) ruby2_keywords (>= 0.0.4) @@ -179,7 +179,7 @@ GEM sass-rails globalid (1.2.1) activesupport (>= 6.1) - graphql (2.3.18) + graphql (2.3.19) base64 fiber-storage graphql-client (0.23.0) @@ -241,8 +241,8 @@ GEM railties (>= 3.2.16) jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (2.7.2) - json-jwt (1.16.6) + json (2.7.4) + json-jwt (1.16.7) activesupport (>= 4.2) aes_key_wrap base64 @@ -266,7 +266,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.6.1) - loofah (2.22.0) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) lookbook (1.5.5) @@ -307,7 +307,7 @@ GEM time net-http (0.3.2) uri - net-imap (0.4.16) + net-imap (0.4.17) date net-protocol net-pop (0.1.2) @@ -377,7 +377,7 @@ GEM puma (5.6.9) nio4r (~> 2.0) racc (1.8.1) - rack (2.2.9) + rack (2.2.10) rack-protection (3.2.0) base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) @@ -434,9 +434,9 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.3.8) + rexml (3.3.9) rouge (4.4.0) - rspec-core (3.13.1) + rspec-core (3.13.2) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) @@ -453,7 +453,7 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.1) - rubocop (1.66.1) + rubocop (1.67.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -507,7 +507,7 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sshkit (1.23.1) + sshkit (1.23.2) base64 net-scp (>= 1.1.2) net-sftp (>= 2.1.2) @@ -527,13 +527,13 @@ GEM time (0.4.0) date timeout (0.4.1) - turbo-rails (2.0.10) + turbo-rails (2.0.11) actionpack (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unaccent (0.4.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) uri (0.13.1) version_gem (1.1.4) view_component (2.83.0) diff --git a/app/views/search/index.html.haml b/app/views/search/index.html.haml index c193b6cd0e..a87850f86e 100644 --- a/app/views/search/index.html.haml +++ b/app/views/search/index.html.haml @@ -15,7 +15,7 @@ = t("search.advanced_options.ontologies") .field = ontologies_selector(id:'search_page_ontologies' ,name: 'ontologies[]', selected: params[:ontologies]&.split(','), ontologies: onts_for_select(include_views: true)) - - if federation_enabled? + - if federation_enabled? .filter-container .title = t('federation.results_from_external_portals') From 8bf8a9d8d3bafb3080c85fa54fce7ec8a1415f04 Mon Sep 17 00:00:00 2001 From: Syphax Date: Mon, 28 Oct 2024 11:46:50 +0100 Subject: [PATCH 38/45] Fix: disabled federation browse page state --- app/controllers/concerns/submission_filter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/submission_filter.rb b/app/controllers/concerns/submission_filter.rb index 34ec133480..c039147280 100644 --- a/app/controllers/concerns/submission_filter.rb +++ b/app/controllers/concerns/submission_filter.rb @@ -47,7 +47,7 @@ def submissions_paginate_filter(params) submissions = filter_submissions(@ontologies, **params) - submissions = merge_by_acronym(submissions) + submissions = merge_by_acronym(submissions) if federation_enabled? submissions = sort_submission_by(submissions, @sort_by, @search) From e5387e5f902f896b29dcfe80d0711100ac29473b Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:38:39 +0100 Subject: [PATCH 39/45] Fix: project page ontologies not selected (#790) * fix the issue in the projects page when the ontologies are private * fix ontologies selector name in projects from * pass the correct project ontologies value in project ontologies selector * pass an empty array if used ontologies of a group is nil --- app/controllers/projects_controller.rb | 14 +++++++------- app/views/projects/_form.html.haml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 490072b4bd..5816730466 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -26,13 +26,13 @@ def show redirect_to projects_path return end - + @project = projects.first @ontologies_used = [] onts_used = @project.ontologyUsed onts_used.each do |ont_used| ont = LinkedData::Client::Models::Ontology.find(ont_used) - unless ont.nil? + unless ont.nil? || ont.errors @ontologies_used << Hash["name", ont.name, "acronym", ont.acronym] end end @@ -62,7 +62,7 @@ def edit @project = projects.first @user_select_list = LinkedData::Client::Models::User.all.map {|u| [u.username, u.id]} @user_select_list.sort! {|a,b| a[1].downcase <=> b[1].downcase} - @usedOntologies = @project.ontologyUsed || [] + @usedOntologies = @project.ontologyUsed&.map{|o| o.split('/').last} @ontologies = LinkedData::Client::Models::Ontology.all end @@ -76,7 +76,7 @@ def create @project = LinkedData::Client::Models::Project.new(values: project_params) @project_saved = @project.save - + # Project successfully created. if response_success?(@project_saved) flash[:notice] = t('projects.project_successfully_created') @@ -160,10 +160,10 @@ def destroy def project_params p = params.require(:project).permit(:name, :acronym, :institution, :contacts, { creator:[] }, :homePage, :description, { ontologyUsed:[] }) - + p[:creator]&.reject!(&:blank?) - p[:ontologyUsed]&.reject!(&:blank?) - p.to_h + p[:ontologyUsed] ||= [] + p = p.to_h end def flash_error(msg) diff --git a/app/views/projects/_form.html.haml b/app/views/projects/_form.html.haml index cd3eea4235..55bdb61edc 100644 --- a/app/views/projects/_form.html.haml +++ b/app/views/projects/_form.html.haml @@ -53,5 +53,5 @@ %div#ontology_picker_project{style: "padding-top: 2em;"} - selected_ontologies = @project.ontologyUsed && @project.ontologyUsed.map {|id| id.split('/').last } || [] - locals = { sel_text: t('projects.form.select_ontologies'), selected_ontologies: selected_ontologies, form_object: :project, form_attribute: "ontologyUsed" } - = ontologies_selector(id:'projects_page_ontologies_selector' ,name: 'ontologies') + = ontologies_selector(id:'projects_page_ontologies_selector' ,name: 'project[ontologyUsed][]', selected: @usedOntologies) From 1aa8d3850eb5f87b39d84b13f78edaa14f8abfa2 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:40:04 +0100 Subject: [PATCH 40/45] fix collection tree issue by including MultiLanguagesHelper to make sure main_language_label is defined in collections helper (#793) --- app/helpers/collections_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index b366a1401c..9a80d24111 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -1,4 +1,5 @@ module CollectionsHelper + include MultiLanguagesHelper def get_collections(ontology, add_colors: false) From 70f0e4daa445de6006baa15f420c5874b40d1bd7 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:45:32 +0100 Subject: [PATCH 41/45] Fix: ontologies selector localization text and add "[view]" of the views in the list (#791) * fix ontologies selector weird results sentence * show [view] beside ontology views in ontologies selector --- app/helpers/application_helper.rb | 4 ++-- .../ontologies_selector/ontologies_selector_results.html.haml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3f69732afd..acf5335c1c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -107,14 +107,14 @@ def error_message_alert end def onts_for_select(include_views: false) - ontologies ||= LinkedData::Client::Models::Ontology.all({include: "acronym,name", include_views: include_views}) + ontologies ||= LinkedData::Client::Models::Ontology.all({include: "acronym,name,viewOf", include_views: include_views}) onts_for_select = [['', '']] ontologies.each do |ont| next if ( ont.acronym.nil? or ont.acronym.empty? ) acronym = ont.acronym name = ont.name abbreviation = acronym.empty? ? "" : "(#{acronym})" - ont_label = "#{name.strip} #{abbreviation}" + ont_label = "#{name.strip} #{abbreviation}#{ont.viewOf ? ' [view]' : ''}" onts_for_select << [ont_label, acronym] end onts_for_select.sort! { |a,b| a[0].downcase <=> b[0].downcase } diff --git a/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml b/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml index 2aeafa23df..9bd25a770a 100644 --- a/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml +++ b/app/views/ontologies/ontologies_selector/ontologies_selector_results.html.haml @@ -2,7 +2,7 @@ .ontologies-selector-results .horizontal-line .results-number - = t("ontologies.showing_ontologies_size", ontologies_size: @ontologies.length, analytics_size: @total_ontologies_number) + = t("ontologies.showing_ontologies_size", ontologies_size: @ontologies.length, analytics_size: @total_ontologies_number, portals: portal_name) %span.select-all{'data-action': 'click->ontologies-selector#selectall'} = t('ontologies_selector.select_all') .ontologies From 1e4b80996ec39f9f70191d9f45043ac7c6596887 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Wed, 30 Oct 2024 15:57:12 +0100 Subject: [PATCH 42/45] Fix: update select_language_label helper to select f no english found any language remaining over the not tagged (#795) * update select_language_label helper to select f no english found any language remaining over the not tagged * fix tests by fixing a dependency change * fix login system tests after securing the user delete action in the API --- Gemfile.lock | 2 +- app/components/tree_link_component.rb | 46 ++++++++++++++-------- app/helpers/multi_languages_helper.rb | 2 +- test/helpers/application_test_helpers.rb | 50 +++++++++++++++--------- test/system/login_flows_test.rb | 2 - test/system/submission_flows_test.rb | 16 +++----- 6 files changed, 69 insertions(+), 49 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3bb8c68812..73328d8412 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -160,7 +160,7 @@ GEM ed25519 (1.3.0) erubi (1.13.0) erubis (2.7.0) - excon (1.0.0) + excon (0.112.0) execjs (2.10.0) faraday (2.0.1) faraday-net_http (~> 2.0) diff --git a/app/components/tree_link_component.rb b/app/components/tree_link_component.rb index 73da902f20..fd23706d96 100644 --- a/app/components/tree_link_component.rb +++ b/app/components/tree_link_component.rb @@ -2,26 +2,19 @@ class TreeLinkComponent < ViewComponent::Base include MultiLanguagesHelper, ModalHelper, ApplicationHelper - def initialize(child:, href:, children_href: , selected: false , data: {}, muted: false, target_frame: nil, open_in_modal: false, is_reused: nil) + + def initialize(child:, href:, children_href:, selected: false, data: {}, muted: false, target_frame: nil, open_in_modal: false, is_reused: nil) + super + @child = child @active_style = selected ? 'active' : '' - #@icons = child.relation_icon(node) @muted_style = muted ? 'text-muted' : '' @href = href @children_link = children_href - label = (@child.prefLabel || @child.label) rescue @child.id - if label.nil? - @pref_label_html = link_last_part(child.id) - else - pref_label_lang, @pref_label_html = select_language_label(label) - pref_label_lang = pref_label_lang.to_s.upcase - @tooltip = pref_label_lang.eql?("@NONE") ? "" : pref_label_lang - if child.obsolete? - @pref_label_html = "#{@pref_label_html}".html_safe - end - end - @data ||= { controller: 'tooltip', 'tooltip-position-value': 'right', turbo: true, 'turbo-frame': target_frame, action: 'click->simple-tree#select'} + @pref_label_html, @tooltip = node_label(child) + + @data ||= { controller: 'tooltip', 'tooltip-position-value': 'right', turbo: true, 'turbo-frame': target_frame, action: 'click->simple-tree#select' } @data.merge!(data) do |_, old, new| "#{old} #{new}" @@ -32,7 +25,6 @@ def initialize(child:, href:, children_href: , selected: false , data: {}, muted @is_reused = is_reused end - # This gives a very hacky short code to use to uniquely represent a class # based on its parent in a tree. Used for unique ids in HTML for the tree view def short_uuid @@ -49,7 +41,7 @@ def open? end def border_left - !@child.hasChildren ? 'pl-3 tree-border-left' : '' + !@child.hasChildren ? 'pl-3 tree-border-left' : '' end def li_id @@ -75,4 +67,26 @@ def open_children_link end + private + + def node_label(child) + label = begin + child.prefLabel || child.label + rescue + child.id + end + + if label.nil? + pref_label_html = link_last_part(child.id) + else + pref_label_lang, pref_label_html = select_language_label(label) + pref_label_lang = pref_label_lang.to_s.upcase + tooltip = pref_label_lang.eql?("@NONE") ? "" : pref_label_lang + + pref_label_html = "#{pref_label_html}".html_safe if child.obsolete? + end + + [pref_label_html, tooltip] + end + end diff --git a/app/helpers/multi_languages_helper.rb b/app/helpers/multi_languages_helper.rb index 9e21f2e0ed..6e127b94e0 100644 --- a/app/helpers/multi_languages_helper.rb +++ b/app/helpers/multi_languages_helper.rb @@ -153,7 +153,7 @@ def select_language_label(concept_label, platform_languages = %i[en fr]) end end - concept_value || concept.to_a.first + concept_value || concept.reject { |k| k.to_s.eql?('@none') }.first || concept.first end def main_language_label(label) diff --git a/test/helpers/application_test_helpers.rb b/test/helpers/application_test_helpers.rb index 975c220eac..28530711f7 100644 --- a/test/helpers/application_test_helpers.rb +++ b/test/helpers/application_test_helpers.rb @@ -16,17 +16,27 @@ module Users def sign_in_as(username) user = fixtures(:users)[username] logged_in_user = LinkedData::Client::Models::User.authenticate(user.username, user.password) - if logged_in_user && !logged_in_user.errors - logged_in_user = create_user(user) - end + logged_in_user = create_user(user) if logged_in_user && !logged_in_user.errors logged_in_user end def create_user(user, admin: false) - admin_user = LinkedData::Client::Models::User.authenticate('admin', 'password') if admin + admin_user = LinkedData::Client::Models::User.authenticate('admin', 'password') existent_user = LinkedData::Client::Models::User.find_by_username(user.username).first - existent_user.delete if existent_user + conn = Faraday.new(url: LinkedData::Client.settings.rest_url) do |faraday| + faraday.request :url_encoded + faraday.response :logger + faraday.adapter Faraday.default_adapter + faraday.headers = { + "Accept" => "application/json", + "Authorization" => "apikey token=#{admin_user.apikey}", + "User-Agent" => "NCBO API Ruby Client v0.1.0" + } + + end + + conn.delete("/users/#{user.username}") if existent_user values = user.to_h values[:role] = ["ADMINISTRATOR"] if admin @@ -34,17 +44,6 @@ def create_user(user, admin: false) if admin # Overwrite the normal ".save" to accept creating admin user - conn = Faraday.new(url: LinkedData::Client.settings.rest_url) do |faraday| - faraday.request :url_encoded - faraday.response :logger - faraday.adapter Faraday.default_adapter - faraday.headers = { - "Accept" => "application/json", - "Authorization" => "apikey token=#{admin_user.apikey}", - "User-Agent" => "NCBO API Ruby Client v0.1.0" - } - - end conn.post(existent_user.class.collection_path, existent_user.to_hash.to_json, 'Content-Type' => 'application/json') else existent_user.save @@ -61,7 +60,22 @@ def delete_users(users = LinkedData::Client::Models::User.all) end def delete_user(user) - LinkedData::Client::Models::User.find_by_username(user.username).first&.delete + admin_user = LinkedData::Client::Models::User.authenticate('admin', 'password') + existent_user = LinkedData::Client::Models::User.find_by_username(user.username).first + + conn = Faraday.new(url: LinkedData::Client.settings.rest_url) do |faraday| + faraday.request :url_encoded + faraday.response :logger + faraday.adapter Faraday.default_adapter + faraday.headers = { + "Accept" => "application/json", + "Authorization" => "apikey token=#{admin_user.apikey}", + "User-Agent" => "NCBO API Ruby Client v0.1.0" + } + + end + + conn.delete("/users/#{user.username}") if existent_user end end @@ -133,4 +147,4 @@ def delete_agents(agents = LinkedData::Client::Models::Agent.all) Array(agents).each { |g| g.delete } end end -end \ No newline at end of file +end diff --git a/test/system/login_flows_test.rb b/test/system/login_flows_test.rb index d6ff0deb49..8bed283d20 100644 --- a/test/system/login_flows_test.rb +++ b/test/system/login_flows_test.rb @@ -22,8 +22,6 @@ class LoginFlowsTest < ApplicationSystemTestCase new_user = @user_john delete_user(new_user) - LinkedData::Client::Models::User.find_by_username(new_user.username).first&.delete - fill_in 'user_firstName', with: new_user.firstName fill_in 'user_lastName', with: new_user.lastName fill_in 'user_username', with: new_user.username diff --git a/test/system/submission_flows_test.rb b/test/system/submission_flows_test.rb index 6045308d04..a0c06a4f47 100644 --- a/test/system/submission_flows_test.rb +++ b/test/system/submission_flows_test.rb @@ -46,7 +46,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text cat.acronym.titleize end - assert_text @new_submission.URI assert_text @new_submission.description assert_text @new_submission.pullLocation @@ -103,7 +102,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase click_on "Licensing" submission_licensing_edit_fill(ontology_2, submission_2) - # Persons and organizations tab click_on "Persons and organizations" submission_agent_edit_fill(submission_2) @@ -148,7 +146,10 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text submission_2.URI assert_text submission_2.versionIRI + + wait_for '.submission-status' assert_selector '.submission-status', text: submission_2.version + assert_selector ".flag-icon-fr" # todo fix this submission_2.identifier.each do |id| assert_text id @@ -258,7 +259,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text "rdfs" assert_text "dct" - open_dropdown "#configuration" submission_2.keyClasses.each do |key| @@ -341,10 +341,8 @@ class SubmissionFlowsTest < ApplicationSystemTestCase sleep 0.5 click_button 'Back' - fill_ontology(ontology_2, submission_2, add_submission: true) - assert_selector 'h2', text: 'Ontology submitted successfully!' click_on current_url.gsub("/ontologies/success/#{existent_ontology.acronym}", '') + ontology_path(existent_ontology.acronym) @@ -362,7 +360,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text submission_2.description assert_text submission_2.pullLocation - # check assert_selector '.fas.fa-key' if submission_2.status.eql?('private') @@ -380,7 +377,6 @@ class SubmissionFlowsTest < ApplicationSystemTestCase assert_text group.name end - open_dropdown "#dates" assert_date submission_2.modificationDate assert_date existent_submission.released @@ -483,7 +479,6 @@ def submission_agent_edit_fill(submission) list_inputs "#submissioncontact_from_group_input", "submission[contact]", submission.contact - agent1 = fixtures(:agents)[:agent1] agent2 = fixtures(:agents)[:agent2] @@ -624,7 +619,6 @@ def fill_ontology(new_ontology, new_submission, add_submission: false) list_checks new_ontology.hasDomain.map(&:acronym), @categories.map(&:acronym) list_checks new_ontology.group.map(&:acronym), @groups.map(&:acronym) - click_button 'Next' # Page 2 @@ -644,9 +638,9 @@ def fill_ontology(new_ontology, new_submission, add_submission: false) # Page 3 if add_submission - date_picker_fill_in 'submission[modificationDate]', new_submission.modificationDate + date_picker_fill_in 'submission[modificationDate]', new_submission.modificationDate else - date_picker_fill_in 'submission[released]', new_submission.released + date_picker_fill_in 'submission[released]', new_submission.released end list_inputs "#submissioncontact_from_group_input", "submission[contact]", new_submission.contact From d67ba52615725d5768353877ca29a535090c2dbb Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:00:49 +0100 Subject: [PATCH 43/45] Fix: clean concepts controller code to improve tree view performance (#794) --- app/controllers/concepts_controller.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/concepts_controller.rb b/app/controllers/concepts_controller.rb index 196a61f97d..1063c953b4 100644 --- a/app/controllers/concepts_controller.rb +++ b/app/controllers/concepts_controller.rb @@ -45,14 +45,15 @@ def index @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology]).first @ob_instructions = helpers.ontolobridge_instructions_template(@ontology) - @submission = @ontology.explore.latest_submission(include: 'all') + @submission = @ontology.explore.latest_submission(include:'uriRegexPattern,preferredNamespaceUri') + + @concept = @ontology.explore.single_class({dispay: 'prefLabel'}, params[:id]) - @concept = @ontology.explore.single_class({full: true}, params[:id]) concept_not_found(params[:id]) if @concept.nil? @schemes = params[:concept_schemes].split(',') @concept.children = @concept.explore.children(pagesize: 750, concept_schemes: Array(@schemes).join(','), language: request_lang, display: 'prefLabel,obsolete,hasChildren').collection || [] - @concept.children.sort! { |x, y| (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase } unless @concept.children.empty? + render turbo_stream: [ replace(helpers.child_id(@concept) + '_open_link') { TreeLinkComponent.tree_close_icon }, replace(helpers.child_id(@concept) + '_childs') do From e616842389ae436b47d09f95fe7cc5bfbf2911e2 Mon Sep 17 00:00:00 2001 From: Syphax bouazzouni Date: Wed, 30 Oct 2024 17:38:56 +0100 Subject: [PATCH 44/45] Fix: Disable slices not hiding the slices section in the home page (#797) * update select_language_label helper to select f no english found any language remaining over the not tagged * hide the slices section in the home page if slices disabled --- app/helpers/application_helper.rb | 3 +++ app/views/home/index.html.haml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index acf5335c1c..6d565bdb22 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -121,6 +121,9 @@ def onts_for_select(include_views: false) onts_for_select end + def slices_enabled? + $ENABLE_SLICES.eql?(true) + end def at_slice? !@subdomain_filter.nil? && !@subdomain_filter[:active].nil? && @subdomain_filter[:active] == true diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 253e0546ad..8bc39eba97 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -176,7 +176,7 @@ .home-statistics-link.justify-content-end{style: @analytics.empty? && "visibility: hidden"} = link_to t("home.see_details"),'/statistics', target: "_blank" - - if @slices + - if slices_enabled? .home-section .home-section-title .text From f1773c1c09e3014d488906c047215c75bdd3c961 Mon Sep 17 00:00:00 2001 From: Bilel Kihal <61744974+Bilelkihal@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:54:09 +0100 Subject: [PATCH 45/45] Feature: update concept mappings table UI (#798) * fix mappings table style * hide relations column when it's completely empty * use icons and Abbreviations types and sources * hide actions column if no mapping is of type REST * update create new mapping button style * remove concept mapping table padding * remove relation column and remove Action title * make the column width for the mapping to and ontology larger * move some mappings helper to the mapping controller as used only there * move the logic from the mapping table view to helpers * fix link text component icon size * change mappings action icons colors to primary color * pass button component in create new mapping instead of using only the css of it * internationalize create new mapping button text * internationalize mapping types description * fix interportal mapping issue when the interportal_hash is empty * limit mapping type tooltip by 300px width --------- Co-authored-by: Syphax --- app/assets/stylesheets/mappings.scss | 25 +++ app/components/link_text_component.rb | 2 +- app/controllers/mappings_controller.rb | 54 +++++- app/helpers/mappings_helper.rb | 174 +++++++++--------- .../mappings/_concept_mappings.html.haml | 17 +- app/views/mappings/_mapping_table.html.haml | 12 +- app/views/mappings/_show_line.html.haml | 57 ++---- config/locales/en.yml | 7 + config/locales/fr.yml | 8 + 9 files changed, 202 insertions(+), 154 deletions(-) diff --git a/app/assets/stylesheets/mappings.scss b/app/assets/stylesheets/mappings.scss index 89a7ebffbe..20ddbb6940 100644 --- a/app/assets/stylesheets/mappings.scss +++ b/app/assets/stylesheets/mappings.scss @@ -320,6 +320,28 @@ div#map_from_concept_details_table, div#map_to_concept_details_table { #concept_mappings_table { width: 100%; + word-break: break-word; + font-size: 13px; +} +.mappings-table-mapping-to{ + width: 45%; +} +.mappings-table-mapping-to .chip-button-component-container{ + text-wrap: wrap !important; +} +.mappings-table-mapping-to a{ + background-color: unset; + line-height: unset; + padding: unset; + font-weight: unset; + font-size: unset; +} +.mappings-table-icon{ + width: 18px; + height: 18px; +} +.mappings-table-icon path{ + fill: var(--gray-color); } .summary-mappings-tab-table { @@ -341,4 +363,7 @@ div#map_from_concept_details_table, div#map_to_concept_details_table { padding: 10px; outline: none; margin-left: 0 !important; +} +.mappings-table-actions svg path{ + fill: var(--primary-color); } \ No newline at end of file diff --git a/app/components/link_text_component.rb b/app/components/link_text_component.rb index 47ea0f6dfc..36dc79ba33 100644 --- a/app/components/link_text_component.rb +++ b/app/components/link_text_component.rb @@ -10,7 +10,7 @@ def initialize(text:, icon: nil, target: nil) end def call - svg_icon = !@icon&.empty? ? inline_svg(@icon) : '' + svg_icon = !@icon&.empty? ? inline_svg(@icon, width: '14px', height: '14px') : '' extra_span = @text == t('mappings.upload_mappings') ? '' : "#{svg_icon}" "#{@text}#{extra_span}".html_safe end diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index b1bcfc8c32..df582acb4e 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -342,4 +342,56 @@ def valid_values?(values) end errors end -end \ No newline at end of file + + def set_mapping_target(concept_to_id:, ontology_to:, mapping_type: ) + case mapping_type + when 'interportal' + @map_to_interportal, @map_to_interportal_ontology = ontology_to.match(%r{(.*)/ontologies/(.*)}).to_a[1..] + @map_to_interportal_class = concept_to_id + when 'external' + @map_to_external_ontology = ontology_to + @map_to_external_class = concept_to_id + else + @map_to_bioportal_ontology_id = ontology_to + @map_to_bioportal_full_id = concept_to_id + end + end + + def get_mappings_target_params + mapping_type = Array(params[:mapping_type]).first + external = true + case mapping_type + when 'interportal' + ontology_to = "#{params[:map_to_interportal]}/ontologies/#{params[:map_to_interportal_ontology]}" + concept_to_id = params[:map_to_interportal_class] + when 'external' + ontology_to = params[:map_to_external_ontology] + concept_to_id = params[:map_to_external_class] + else + ontology_to = params[:map_to_bioportal_ontology_id] + concept_to_id = params[:map_to_bioportal_full_id] + external = false + end + [ontology_to, concept_to_id, external] + end + + def get_mappings_target + ontology_to, concept_to_id, external_mapping = get_mappings_target_params + target = '' + if external_mapping + target_ontology = ontology_to + target = concept_to_id + else + if helpers.link?(ontology_to) + target_ontology = LinkedData::Client::Models::Ontology.find(ontology_to) + else + target_ontology = LinkedData::Client::Models::Ontology.find_by_acronym(ontology_to).first + end + if target_ontology + target = target_ontology.explore.single_class(concept_to_id).id + target_ontology = target_ontology.id + end + end + [target_ontology, target, external_mapping] + end +end diff --git a/app/helpers/mappings_helper.rb b/app/helpers/mappings_helper.rb index bfe583a41b..c1e503b547 100644 --- a/app/helpers/mappings_helper.rb +++ b/app/helpers/mappings_helper.rb @@ -2,43 +2,85 @@ module MappingsHelper # Used to replace the full URI by the prefixed URI RELATIONSHIP_PREFIX = { - "http://www.w3.org/2004/02/skos/core#" => "skos:", - "http://www.w3.org/2000/01/rdf-schema#" => "rdfs:", - "http://www.w3.org/2002/07/owl#" => "owl:", - "http://www.w3.org/1999/02/22-rdf-syntax-ns#" => "rdf:", - "http://purl.org/linguistics/gold/" => "gold:", - "http://lemon-model.net/lemon#" => "lemon:" + 'http://www.w3.org/2004/02/skos/core#' => 'skos:', + 'http://www.w3.org/2000/01/rdf-schema#' => 'rdfs:', + 'http://www.w3.org/2002/07/owl#' => 'owl:', + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' => 'rdf:', + 'http://purl.org/linguistics/gold/' => 'gold:', + 'http://lemon-model.net/lemon#' => 'lemon:' } INTERPORTAL_HASH = $INTERPORTAL_HASH - - # a little method that returns true if the URIs array contain a gold:translation or gold:freeTranslation - def translation?(relation_array) - if relation_array.kind_of?(Array) - relation_array.map!(&:downcase) - if relation_array.include? "http://purl.org/linguistics/gold/translation" - true - elsif relation_array.include? "http://purl.org/linguistics/gold/freetranslation" - true - else - false + def mapping_links(mapping, concept) + target_concept = mapping.classes.select do |c| + c.id != concept.id && c.links['ontology'] != concept.links['ontology'] + end.first + target_concept ||= mapping.classes.last + process = mapping.process || {} + + if inter_portal_mapping?(target_concept) + cls_link = ajax_to_inter_portal_cls(target_concept) + ont_name = target_concept.links['ontology'] + ont_link = link_to ont_name, get_inter_portal_ui_link(ont_name, process['name']), target: '_blank' + source_tooltip = 'Internal-portal' + elsif internal_mapping?(target_concept) + begin + ont = target_concept.explore.ontology + ont_name = ont.acronym + ont_link = link_to ont_name, ontology_path(ont_name), 'data-turbo-frame': '_top' + rescue + ont_name = target_concept.links['ontology'] || target_concept.id + ont_link = ont_name end + cls_link = raw(get_link_for_cls_ajax(target_concept.id, ont_name, '_top')) + source_tooltip = 'Internal' else - false + cls_label = ExternalLinkTextComponent.new(text: target_concept.links['self']).call + cls_link = raw("#{cls_label}") + ont_name = target_concept.links['ontology'] + ont_link = link_to ExternalLinkTextComponent.new(text: ont_name).call, target_concept.links['ontology'], + target: '_blank' + source_tooltip = 'External' end + + [cls_link, ont_link, source_tooltip] + end + + def mapping_prefixed_relations(mapping) + process = mapping.process || {} + Array(process[:relation]).each { |relation| get_prefixed_uri(relation) } + end + + def mapping_type_tooltip(map) + relations = mapping_prefixed_relations(map) + process = map.process || {} + type = if map.source.to_s.include? 'SKOS' + 'SKOS' + else + map.source + end + types_description = { + 'CUI' => t('mappings.types_description.cui'), + 'LOOM' => t('mappings.types_description.loom'), + 'REST' => t('mappings.types_description.rest'), + 'SAME_URI' => t('mappings.types_description.same_uri'), + 'SKOS' => t('mappings.types_description.skos') + } + type_tooltip = content_tag(:div, "#{map.source} #{relations.join(', ')} : #{types_description[type]} #{process[:source_name]}".strip, style: 'width: 300px') + [type, type_tooltip] end # a little method that returns the uri with a prefix : http://purl.org/linguistics/gold/translation become gold:translation def get_prefixed_uri(uri) RELATIONSHIP_PREFIX.each { |k, v| uri.sub!(k, v) } - return uri + uri end # method to get (using http) prefLabel for interportal classes # Using bp_ajax_controller.ajax_process_interportal_cls will try to resolve class labels. def ajax_to_inter_portal_cls(cls) - inter_portal_acronym = get_inter_portal_acronym(cls.links["ui"]) + inter_portal_acronym = get_inter_portal_acronym(cls.links['ui']) href_cls = " href='#{cls.links["ui"]}' " if inter_portal_acronym data_cls = " data-cls='#{cls.links["self"]}?apikey=' " @@ -52,7 +94,7 @@ def ajax_to_inter_portal_cls(cls) def ajax_to_internal_cls(cls) link_to("#{cls.id}".html_safe, - ontology_path(cls.explore.ontology.acronym, p: 'classes', conceptid: cls.id), target: "_blank") + ontology_path(cls.explore.ontology.acronym, p: 'classes', conceptid: cls.id), target: '_blank') end # to get the apikey from the interportal instance of the interportal class. @@ -60,7 +102,7 @@ def ajax_to_internal_cls(cls) def get_inter_portal_acronym(class_ui_url) if !INTERPORTAL_HASH.nil? INTERPORTAL_HASH.each do |key, value| - if class_ui_url.start_with?(value["ui"]) + if class_ui_url.start_with?(value['ui']) return key else return nil @@ -71,11 +113,11 @@ def get_inter_portal_acronym(class_ui_url) # method to extract the prefLabel from the external class URI def get_label_for_external_cls(class_uri) - if class_uri.include? "#" - prefLabel = class_uri.split("#")[-1] - else - prefLabel = class_uri.split("/")[-1] - end + prefLabel = if class_uri.include? '#' + class_uri.split('#')[-1] + else + class_uri.split('/')[-1] + end return prefLabel end @@ -86,11 +128,11 @@ def ajax_to_external_cls(cls) # Replace the inter_portal mapping ontology URI (that link to the API) by the link to the ontology in the UI def get_inter_portal_ui_link(uri, process_name) process_name = '' if process_name.nil? - interportal_acronym = process_name.split(" ")[2] - if interportal_acronym.nil? || interportal_acronym.empty? + interportal_acronym = process_name.split(' ')[2] + if interportal_acronym.nil? || interportal_acronym.empty? || INTERPORTAL_HASH[interportal_acronym].nil? uri else - uri.sub!(INTERPORTAL_HASH[interportal_acronym]["api"], INTERPORTAL_HASH[interportal_acronym]["ui"]) + uri.sub!(INTERPORTAL_HASH[interportal_acronym]['api'], INTERPORTAL_HASH[interportal_acronym]['ui']) end end @@ -99,66 +141,14 @@ def internal_mapping?(cls) end def inter_portal_mapping?(cls) - !internal_mapping?(cls) && cls.links.has_key?("ui") - end - - def get_mappings_target_params - mapping_type = Array(params[:mapping_type]).first - external = true - case mapping_type - when 'interportal' - ontology_to = "#{params[:map_to_interportal]}/ontologies/#{params[:map_to_interportal_ontology]}" - concept_to_id = params[:map_to_interportal_class] - when 'external' - ontology_to = params[:map_to_external_ontology] - concept_to_id = params[:map_to_external_class] - else - ontology_to = params[:map_to_bioportal_ontology_id] - concept_to_id = params[:map_to_bioportal_full_id] - external = false - end - [ontology_to, concept_to_id, external] - end - - def set_mapping_target(concept_to_id:, ontology_to:, mapping_type: ) - case mapping_type - when 'interportal' - @map_to_interportal, @map_to_interportal_ontology = ontology_to.match(%r{(.*)/ontologies/(.*)}).to_a[1..] - @map_to_interportal_class = concept_to_id - when 'external' - @map_to_external_ontology = ontology_to - @map_to_external_class = concept_to_id - else - @map_to_bioportal_ontology_id = ontology_to - @map_to_bioportal_full_id = concept_to_id - end - end - - def get_mappings_target - ontology_to, concept_to_id, external_mapping = get_mappings_target_params - target = '' - if external_mapping - target_ontology = ontology_to - target = concept_to_id - else - if helpers.link?(ontology_to) - target_ontology = LinkedData::Client::Models::Ontology.find(ontology_to) - else - target_ontology = LinkedData::Client::Models::Ontology.find_by_acronym(ontology_to).first - end - if target_ontology - target = target_ontology.explore.single_class(concept_to_id).id - target_ontology = target_ontology.id - end - end - [target_ontology, target, external_mapping] + !internal_mapping?(cls) && cls.links.has_key?('ui') end def type?(type) @mapping_type.nil? && type.eql?('internal') || @mapping_type.eql?(type) end - def concept_mappings_loader(ontology_acronym: ,concept_id: ) + def concept_mappings_loader(ontology_acronym:, concept_id:) content_tag(:span, id: 'mapping_count') do concat(content_tag(:div, class: 'concepts-mapping-count ml-1 mr-1') do render(TurboFrameComponent.new( @@ -173,21 +163,23 @@ def concept_mappings_loader(ontology_acronym: ,concept_id: ) end def client_filled_modal - link_to_modal "", "" + link_to_modal '', '' end def mappings_bubble_view_legend content_tag(:div, class: 'mappings-bubble-view-legend') do - mappings_legend_section(t('mappings.bubble_view_legend.bubble_size'), t('mappings.bubble_view_legend.bubble_size_desc'), 'mappings-bubble-size-legend') + + mappings_legend_section(t('mappings.bubble_view_legend.bubble_size'), + t('mappings.bubble_view_legend.bubble_size_desc'), 'mappings-bubble-size-legend') + mappings_legend_section( - t('mappings.bubble_view_legend.color_degree'),t('mappings.bubble_view_legend.color_degree_desc'),'mappings-bubble-color-legend') + + t('mappings.bubble_view_legend.color_degree'), t('mappings.bubble_view_legend.color_degree_desc'), 'mappings-bubble-color-legend') + content_tag(:div, class: 'content-container') do content_tag(:div, class: 'bubble-view-legend-item') do content_tag(:div, class: 'title') do - content_tag(:div, t('mappings.bubble_view_legend.yellow_bubble'), class: 'd-inline') + content_tag(:span, t('mappings.bubble_view_legend.selected_bubble')) + content_tag(:div, t('mappings.bubble_view_legend.yellow_bubble'), + class: 'd-inline') + content_tag(:span, t('mappings.bubble_view_legend.selected_bubble')) end + - content_tag(:div, class: "mappings-bubble-size-legend d-flex justify-content-center") do - content_tag(:div, '', class: "bubble yellow") + content_tag(:div, class: 'mappings-bubble-size-legend d-flex justify-content-center') do + content_tag(:div, '', class: 'bubble yellow') end end end @@ -209,7 +201,7 @@ def mappings_legend_section(title_text, description_text, css_class) def mappings_legend(css_class) content_tag(:div, class: css_class) do content_tag(:div, t('mappings.bubble_view_legend.less_mappings'), class: 'mappings-legend-text') + - (1..6).map { |i| content_tag(:div, "", class: "bubble bubble#{i}") }.join.html_safe + + (1..6).map { |i| content_tag(:div, '', class: "bubble bubble#{i}") }.join.html_safe + content_tag(:div, t('mappings.bubble_view_legend.more_mappings'), class: 'mappings-legend-text') end end diff --git a/app/views/mappings/_concept_mappings.html.haml b/app/views/mappings/_concept_mappings.html.haml index c71fe9066b..2a6b56b29e 100644 --- a/app/views/mappings/_concept_mappings.html.haml +++ b/app/views/mappings/_concept_mappings.html.haml @@ -2,17 +2,16 @@ = "#{@mappings.size}" = turbo_frame_tag @type.eql?('modal') ? 'application_modal_content' : 'concept_mappings' do - %div{:style => "padding: 1%; width: 98%"} + %div.p-1 - if session[:user].nil? = link_to "Create New Mapping", "/login?redirect=/ontologies/#{@ontology.acronym}/?p=classes&t=mappings&conceptid=#{escape(@concept.id)}", :method => :get, :class => "btn btn-default mb-3" - else - = link_to_modal("Create New Mapping", - new_mapping_path(ontology_from: "#{@ontology.id}", conceptid_from: "#{@concept.id}"), - id: "new_mapping_btn", - role: "button", - class: "btn btn-default mb-3", - data: { show_modal_title_value: "Create a new mapping for #{@concept.prefLabel}" }, - ) + %div{style: 'width: 250px; margin: 0 0 10px 0;'} + = link_to_modal(nil, + new_mapping_path(ontology_from: "#{@ontology.id}", conceptid_from: "#{@concept.id}"), + data: { show_modal_title_value: "Create a new mapping for #{@concept.prefLabel}" }, + ) do + = render Buttons::RegularButtonComponent.new(id:'new_mapping_btn' , value:t('mappings.create_new_mapping'), variant: 'secondary', size: 'slim', state: 'no-anim') #mapping_details - = render :partial => '/mappings/mapping_table' \ No newline at end of file + = render :partial => '/mappings/mapping_table' diff --git a/app/views/mappings/_mapping_table.html.haml b/app/views/mappings/_mapping_table.html.haml index 1d91b3b5bb..20efcd92d5 100644 --- a/app/views/mappings/_mapping_table.html.haml +++ b/app/views/mappings/_mapping_table.html.haml @@ -1,16 +1,14 @@ = check_box_tag "delete_mappings_permission", @delete_mapping_permission, @delete_mapping_permission, style: "display: none;" %div#concept_mappings_tables_div = render_alerts_container(MappingsController) - %table#concept_mappings_table.table-content-stripped.table-content{width: "100%", style:'word-break: break-word'} + %table#concept_mappings_table.table-content-stripped.table-content.table-mini %thead %tr %th= t("mappings.mapping_table.mapping_to") - %th{width: "30%"}= t("mappings.count.ontology") - %th= t("mappings.mapping_table.relations") - %th= t("mappings.mapping_table.source") + %th= t("mappings.count.ontology") %th= t("mappings.mapping_table.type") - - if current_user_admin? - %th{:class => 'delete_mappings_column'}= t("mappings.mapping_table.actions") + - if current_user_admin? && !@mappings.all? { |obj| !obj.source.eql?('REST') } + %th{:class => 'delete_mappings_column'} %tbody#concept_mappings_table_content - @mappings.each do |map| = render partial: 'mappings/show_line' , locals: {map: map, concept: @concept} @@ -19,4 +17,4 @@ :javascript jQuery(document).ready(function(){ ajax_process_init(); - }) \ No newline at end of file + }) diff --git a/app/views/mappings/_show_line.html.haml b/app/views/mappings/_show_line.html.haml index bbfab2b78e..4c94555c4d 100644 --- a/app/views/mappings/_show_line.html.haml +++ b/app/views/mappings/_show_line.html.haml @@ -1,56 +1,23 @@ -- process = map.process || {} -- source = "#{map.source} #{process[:source_name]}" -- relations = process[:relation]&.each { |relation| get_prefixed_uri(relation)} +- type, type_tooltip = mapping_type_tooltip(map) +- cls_link, ont_link, source_tooltip = mapping_links(map, concept) - map_id = map.id.to_s.split("/").last -- target_concept = map.classes.select {|target_concept| target_concept.id != concept.id && target_concept.links['ontology'] != concept.links['ontology']}.first || map.classes.last -- if inter_portal_mapping?(target_concept) - - cls_link = ajax_to_inter_portal_cls(target_concept) - - ont_name = target_concept.links['ontology'] - - ont_acronym = ont_name - - ont_link = link_to ont_acronym , get_inter_portal_ui_link(target_concept.links['ontology'], process["name"]), target: '_blank' - - type = 'Inter-portal' -- elsif internal_mapping?(target_concept) - - begin - - ont = target_concept.explore.ontology - - ont_name = ont.acronym - - ont_link = link_to ont_name, ontology_path(ont_name), 'data-turbo-frame':'_top' - - rescue - - ont_name = target_concept.links['ontology'] || target_concept.id - - ont_link = ont_name - - cls_link = raw(get_link_for_cls_ajax(target_concept.id, ont_name, '_top')) - - type = 'Internal' -- else - - cls_label = get_label_for_external_cls(target_concept.links["self"]) - - cls_link = raw("#{cls_label}") - - ont_name = target_concept.links['ontology'] - - ont_link = link_to ont_name, target_concept.links['ontology'], target: "_blank" - - type = 'External' - %tr.human{:id => map_id} - %td + %td.mappings-table-mapping-to = cls_link - %td + %td.mappings-table-mapping-to = ont_link %td - - relations&.each do |r| - = r - %br/ - %td - #{source} - - if !process.nil? - - if translation?(process["relation"]) - %img{:src => asset_path('sifr/english_language_flag.png'), :style => "padding: 5px", :align => "right", :title => "Traduction"} - %td - = type + = render ChipButtonComponent.new(class: 'chip_button_small mr-1', text: type, tooltip:"#{source_tooltip} mapping of type #{type_tooltip}") + - if current_user_admin? %td{:class => 'delete_mappings_column'} - if map.id && !map.id.empty? && session[:user] && (session[:user].id.to_i == map.creator || session[:user].admin?) && map.source.eql?('REST') - %div.d-flex - = link_to_modal(t("mappings.show_line.edit_modal"), + %div.d-flex.mappings-table-actions + = link_to_modal(nil, mapping_path(map_id, {conceptid_from: @concept.id}), - role: "button", - class: "btn btn-link", + class: 'btn btn-link p-0 mr-1', data: { show_modal_title_value: t("mappings.show_line.edit_mapping", preflabel: @concept.prefLabel)}, - ) - = button_to t("mappings.show_line.delete_button"), CGI.unescape(mapping_path(map.id)), method: :delete, class:'btn btn-link', form: {data: { turbo: true, turbo_confirm: t("mappings.show_line.turbo_confirm"), turbo_frame: '_top'}} \ No newline at end of file + ) do + = inline_svg_tag "edit.svg", width: '15px', height: '15px' + = button_to inline_svg_tag('icons/delete.svg', width: '16px', heigth: '16px'), CGI.unescape(mapping_path(map.id)), class: 'btn btn-link p-0', method: :delete, form: {data: { turbo: true, turbo_confirm: t("mappings.show_line.turbo_confirm"), turbo_frame: '_top'}} diff --git a/config/locales/en.yml b/config/locales/en.yml index 4f9ee4c48b..a63e3500eb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -572,12 +572,19 @@ en: date: Date ontology_visits: Ontology visits mappings: + create_new_mapping: Create new mapping all: All description: Dive into an overview of the mappings in the bubble view, efficiently locate a specific ontology in the table view or upload your own mappings. tabs: bubble_view: Bubbles view table_view: Table view upload_mappings: Upload mappings + types_description: + cui: Created between 2 concepts that have the same CUI (Concept Unique Identifiers) + loom: Lexical mappings created between 2 concepts with very similar labels (preferred name) + rest: A mapping added by a user using the REST API (or the UI, which is calling the API to create it) + same_uri: Created between 2 concepts with the same URI. + skos: Mappings based on SKOS relationships, (e.g. skos:exactMatch or skos:closeMatch) filter_ontologies: Filter ontologies in the bubble view filter_bubbles: Filter bubbles external_mappings: "External Mappings (%{number_with_delimiter})" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0b319d391e..d0969e5795 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -580,12 +580,20 @@ fr: ontology_visits: Visites d'ontologies mappings: + create_new_mapping: Créer un nouveau mapping all: Tous description: Plongez dans une vue d'ensemble des mappings dans la vue en bulles, localisez efficacement une ontologie spécifique dans la vue en tableau ou téléchargez vos propres mappings. tabs: bubble_view: Vue en bulles table_view: Vue en tableau upload_mappings: Télécharger des mappings + types_description: + cui: Créé entre 2 concepts ayant le même CUI (Identifiants Uniques de Concepts) + loom: Mappings lexicaux créés entre 2 concepts avec des libellés très similaires (nom préféré) + rest: Un mapping ajouté par un utilisateur via l'API REST (ou l'interface utilisateur, qui appelle l'API pour le créer) + same_uri: Créé entre 2 concepts ayant la même URI. + skos: Mapping basés sur des relations SKOS (par exemple, skos:exactMatch ou skos:closeMatch) + filter_ontologies: Filtrer les ontologies dans la vue en bulles filter_bubbles: Filtrer les bulles external_mappings: "Mappings externes (%{number_with_delimiter})"