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