diff --git a/app/assets/stylesheets/components/table.scss b/app/assets/stylesheets/components/table.scss index f3fb8711a9..6c03be874d 100644 --- a/app/assets/stylesheets/components/table.scss +++ b/app/assets/stylesheets/components/table.scss @@ -2,7 +2,7 @@ border-collapse: collapse; width: 100%; border-spacing: 0; - + box-sizing: border-box !important; } .table-auto-layout{ table-layout: auto !important; diff --git a/app/assets/stylesheets/mappings.scss b/app/assets/stylesheets/mappings.scss index 070fafea72..4e16155302 100644 --- a/app/assets/stylesheets/mappings.scss +++ b/app/assets/stylesheets/mappings.scss @@ -1,4 +1,265 @@ +.mappigs-page-container{ + display: flex; + justify-content: center; + .file_uploader{ + width: 700px; + } +} +.mappings-page-subcontainer{ + width: 1248px; + padding: 20px 50px; +} + +.mappings-page-title{ + font-size: 25px; + font-weight: 700; +} +.mappings-page-title .line{ + height: 2px; + width: 57px; + background-color: var(--primary-color); + border-radius: 10px; +} +.mappings-page-decription{ + color: #888888; + margin: 20px 0; +} + +.mappings-bubble{ + cursor: pointer; +} +.mappings-bubble-view-frame{ + width: 600px; + height: 600px; + overflow: auto; +} + +.mappings-zoom-buttons{ + display: flex; + margin-top: 20px; + justify-content: center; +} +.mappings-zoom-buttons svg{ + width: 40px; + cursor: pointer; + path{ + fill: var(--primary-color) + } + &:hover path{ + fill: var(--hover-color) + } +} +.mappings-zoom-buttons div + div{ + margin-left: 40px; +} +.mappings-bubble-view-container{ + display: flex; + flex-direction: column; + align-items: center; +} +.mappings-table-view-container{ + display: flex; + flex-direction: column; + align-items: center; + margin: 20px 0; +} +.mappings-ontologies-select{ + width: 700px; +} + +.mappings-table-container{ + width: 678px; + margin: 20px 0; +} +.mappings-table-ontology-name{ + font-weight: 600; +} + +.bubble-tooltip { + position: absolute; + text-align: center; + padding: 6px; + font-size: 12px; + background: rgba(0, 0, 0, 0.7); + color: white; + border-radius: 4px; + pointer-events: none; + z-index: 9999; +} + +.upload-mappings{ + display: flex; + flex-direction: column; + align-items: center; + margin-top: 20px; +} + +.upload-mappings-example{ + width: 700px; + margin-bottom: 20px; +} +.upload-mappings-example.ontologies{ + width: 600px; +} + +.mappings-page-ontologies-selector{ + width: 100%; + padding: 0 30px 30px 30px; +} + +.mappings-page-ontologies-selector .selector-button{ + margin-top: 20px; +} + +.upload-mappings-example .title-bar{ + display: flex; + justify-content: center; + width: 100%; + cursor: pointer; + padding: 15px; + color: var(--primary-color); + align-items: center; +} +.upload-mappings-example .title-bar svg{ + margin-right: 10px; +} + +.summary-mappigs-page-container{ + margin-left: 90px; +} +.mapping-bubbles-loader{ + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.summary-mappings-tab { + margin-top: 10px; +} + +#ontology_viewermappings_content { + .summary-mappings-tab { + display: flex; + } + + .summary-mappings-tab-table { + width: 400px; + } +} + +.mappings-table-pagination a, .mappings-table-pagination em{ + margin-left: 10px; +} + +.mappings-bubble-view-legend{ + width: 600px; + display: flex; + flex-direction: column; + align-items: center; + font-weight: 500; + padding: 20px; +} +.mappings-bubble-view-legend > .title { + display: flex; + flex-direction: column; + align-items: center; +} + +.mappings-bubble-view-legend > .title .text{ + font-size: 18px; + color: var(--primary-color); +} +.mappings-bubble-view-legend > .title .line{ + background-color: var(--primary-color); + border-radius: 100%; + width: 40px; + height: 2px; +} + +.content-container{ + width: 100%; + margin-top: -35px; +} +.bubble-view-legend-item{ + margin-top: 50px; +} +.bubble-view-legend-item .title{ + font-weight: 500; +} +.bubble-view-legend-item .title span{ + font-weight: 400; +} + +.mappings-bubble-size-legend, .mappings-bubble-color-legend{ + display: flex; + justify-content: space-between; + align-items: center; + margin: 20px 0; +} +.mappings-bubble-size-legend .bubble{ + border-radius: 100%; + border: 2px solid var(--primary-color); +} +.mappings-bubble-size-legend .bubble.bubble1{ + width: 10px; + height: 10px; +} +.mappings-bubble-size-legend .bubble.bubble2{ + width: 20px; + height: 20px; +} +.mappings-bubble-size-legend .bubble.bubble3{ + width: 30px; + height: 30px; +} +.mappings-bubble-size-legend .bubble.bubble4{ + width: 40px; + height: 40px; +} +.mappings-bubble-size-legend .bubble.bubble5{ + width: 50px; + height: 50px; +} +.mappings-bubble-size-legend .bubble.bubble6{ + width: 60px; + height: 60px; +} +.mappings-bubble-color-legend .bubble{ + border-radius: 100%; + background-color: var(--primary-color); + width: 35px; + height: 35px; +} +.mappings-bubble-color-legend .bubble.bubble1{ + opacity: 30%; +} +.mappings-bubble-color-legend .bubble.bubble2{ + opacity: 50%; +} +.mappings-bubble-color-legend .bubble.bubble3{ + opacity: 70%; +} +.mappings-bubble-color-legend .bubble.bubble4{ + opacity: 80%; +} +.mappings-bubble-color-legend .bubble.bubble5{ + opacity: 90%; +} +.mappings-bubble-color-legend .bubble.bubble6{ + opacity: 100%; +} +.mappings-bubble-size-legend .bubble.yellow{ + width: 35px; + height: 35px; + background-color: var(--secondary-color); +} +.mappings-legend-text{ + color: var(--primary-color); + font-size: 15px; + font-weight: 500; +} @media (min-width: 1000px) { #mappings_container{ @@ -76,3 +337,23 @@ div#map_from_concept_details_table, div#map_to_concept_details_table { width: 100%; } +.summary-mappings-tab-table { + .dataTables_wrapper .dataTables_filter { + float: none; + text-align: unset; + } +} + +.summary-mappings-tab-table label { + display: block; + font-size: 0; +} + +.summary-mappings-tab-table label input[type="search"] { + font-size: 16px; + width: 100%; + border-radius: 8px; + padding: 10px; + outline: none; + margin-left: 0 !important; +} \ No newline at end of file diff --git a/app/controllers/annotator_controller.rb b/app/controllers/annotator_controller.rb index 6f1ec810b8..d0cd478007 100644 --- a/app/controllers/annotator_controller.rb +++ b/app/controllers/annotator_controller.rb @@ -201,6 +201,7 @@ def empty_advanced_options params[:fast_context].nil? && params[:lemmatize].nil? end + def remove_special_chars(input) regex = /^[a-zA-Z0-9\s]*$/ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ebcbcc6b36..d650f26460 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -193,19 +193,9 @@ def bp_config_json def rest_url - # Split the URL into protocol and path parts - protocol, path = REST_URI.split("://", 2) - - # Remove duplicate "//" - cleaned_url = REST_URI.gsub(/\/\//, '/') - - # Remove the last '/' in the path part - cleaned_path = path.chomp('/') - # Reconstruct the cleaned URL - "#{protocol}://#{cleaned_path}" + helpers.rest_url end - - + def check_http_file(url) session = Net::HTTP.new(url.host, url.port) session.use_ssl = true if url.port == 443 diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index 2f262a6a38..e6c9fabd4b 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -15,32 +15,26 @@ class MappingsController < ApplicationController INTERPORTAL_HASH = $INTERPORTAL_HASH ||= {} def index + @ontologies_mapping_count = LinkedData::Client::HTTP.get("#{MAPPINGS_URL}/statistics/ontologies") ontology_list = LinkedData::Client::Models::Ontology.all.select { |o| !o.summaryOnly } - ontologies_mapping_count = LinkedData::Client::HTTP.get("#{MAPPINGS_URL}/statistics/ontologies") ontologies_hash = {} ontology_list.each do |ontology| ontologies_hash[ontology.acronym] = ontology end - # TODO_REV: Views support for mappings - # views_list.each do |view| - # ontologies_hash[view.ontologyId] = view - # end - @options = {} - ontologies_mapping_count&.members&.each do |ontology_acronym| - # Adding external and interportal mappings to the dropdown list + @ontologies_mapping_count&.members&.each do |ontology_acronym| if ontology_acronym.to_s == EXTERNAL_MAPPINGS_GRAPH - mapping_count = ontologies_mapping_count[ontology_acronym.to_s] || 0 + mapping_count = @ontologies_mapping_count[ontology_acronym.to_s] || 0 select_text = t('mappings.external_mappings', number_with_delimiter: number_with_delimiter(mapping_count, delimiter: ',')) if mapping_count >= 0 ontology_acronym = EXTERNAL_URL_PARAM_STR elsif ontology_acronym.to_s.start_with?(INTERPORTAL_MAPPINGS_GRAPH) - mapping_count = ontologies_mapping_count[ontology_acronym.to_s] || 0 + mapping_count = @ontologies_mapping_count[ontology_acronym.to_s] || 0 select_text = t('mappings.interportal_mappings', acronym: ontology_acronym.to_s.split("/")[-1].upcase, number_with_delimiter: number_with_delimiter(mapping_count, delimiter: ',')) if mapping_count >= 0 ontology_acronym = INTERPORTAL_URL_PARAM_STR + ontology_acronym.to_s.split("/")[-1] else ontology = ontologies_hash[ontology_acronym.to_s] - mapping_count = ontologies_mapping_count[ontology_acronym] || 0 + mapping_count = @ontologies_mapping_count[ontology_acronym] || 0 next unless ontology && mapping_count > 0 select_text = "#{ontology.name} - #{ontology.acronym} (#{number_with_delimiter(mapping_count, delimiter: ',')})" end @@ -48,16 +42,8 @@ def index end @options = @options.sort - end - - def count - @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:id]).first - @ontology_acronym = @ontology&.acronym || params[:id] - @mapping_counts = mapping_counts(@ontology_acronym) - render partial: 'count' - end + @options.unshift([]) - def loader @example_code = [{ "classes": ["http://bioontology.org/ontologies/BiomedicalResourceOntology.owl#Image_Algorithm", "http://purl.org/incf/ontology/Computational_Neurosciences/cno_alpha.owl#cno_0000202"], @@ -74,9 +60,20 @@ def loader "source_contact_info": 'orcid:1234,orcid:5678', "date": '2020-05-30' }] - render partial: 'mappings/bulk_loader/loader' end + + def count + @ontology_acronym = params[:ontology] || params[:id] + @mapping_counts = mapping_counts(@ontology_acronym) + + respond_to do |format| + format.html { render partial: 'mappings/count' } + format.json { render json: @mapping_counts } + end + end + + def loader_process response = LinkedData::Client::HTTP.post('/mappings/load', file: params[:file]) errors = response.errors @@ -85,6 +82,7 @@ def loader_process created = response.created respond_to do |format| format.turbo_stream do + # TO test render turbo_stream: turbo_stream.replace('file_loader_result', partial: 'mappings/bulk_loader/loaded_mappings', locals: { errors: errors, created: created }) @@ -105,7 +103,7 @@ def show def show_mappings page = params[:page] || 1 @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:id]).first - @target_ontology = LinkedData::Client::Models::Ontology.find(params[:target]) + @target_ontology = LinkedData::Client::Models::Ontology.find(params[:target].split('/').last) # Cases if ontology or target are interportal or external if @ontology.nil? @@ -277,7 +275,7 @@ def mapping_form(mapping: nil) else mapping = LinkedData::Client::Models::Mapping.new @ontology_from = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology_from].split('/').last).first - @ontology_to = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology_to]&.split('/')&.last).first + @ontology_to = params[:ontology_to].present? ? LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology_to].split('/').last).first : nil @concept_from = @ontology_from.explore.single_class({ full: true }, params[:conceptid_from]) if @ontology_from if @ontology_to @concept_to = @ontology_to.explore.single_class({ full: true }, params[:conceptid_to]) @@ -343,4 +341,4 @@ def valid_values?(values) end errors end -end +end \ No newline at end of file diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 69e6d04d37..981523bbfd 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -137,6 +137,7 @@ def edit def mappings @ontology_acronym = @ontology.acronym || params[:id] @mapping_counts = mapping_counts(@ontology_acronym) + @ontologies_mapping_count = LinkedData::Client::HTTP.get("#{MAPPINGS_URL}/statistics/ontologies") if request.xhr? render partial: 'ontologies/sections/mappings', layout: false else diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 669ce29f64..93a6742854 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -563,6 +563,16 @@ def prefix_properties(concept_properties) modified_properties end + def rest_url + # Split the URL into protocol and path parts + protocol, path = $REST_URL.split("://", 2) + + # Remove the last '/' in the path part + cleaned_path = path.chomp('/') + # Reconstruct the cleaned URL + "#{protocol}://#{cleaned_path}" + end + def prefix_property_url(key_string, key = nil) namespace_key, _ = RESOLVE_NAMESPACE.find { |_, value| key_string.include?(value) } @@ -641,5 +651,7 @@ def cancel_button_component(class_name: nil, id: , value:, data: nil) end end end - + + + end diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 9824327927..eae423d829 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -139,6 +139,10 @@ 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) + end + def info_tooltip(text) render Display::InfoTooltipComponent.new(text: text) end @@ -220,6 +224,14 @@ 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 diff --git a/app/helpers/mappings_helper.rb b/app/helpers/mappings_helper.rb index 7e0643e513..cee0168da4 100644 --- a/app/helpers/mappings_helper.rb +++ b/app/helpers/mappings_helper.rb @@ -1,12 +1,5 @@ module MappingsHelper - RELATIONSHIP_URIS = { - "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:" - } - # Used to replace the full URI by the prefixed URI RELATIONSHIP_PREFIX = { "http://www.w3.org/2004/02/skos/core#" => "skos:", @@ -19,11 +12,6 @@ module MappingsHelper INTERPORTAL_HASH = $INTERPORTAL_HASH - def get_short_id(uri) - split = uri.split("#") - name = split.length > 1 && RELATIONSHIP_URIS.keys.include?(split[0]) ? RELATIONSHIP_URIS[split[0]] + split[1] : uri - "#{name}" - end # a little method that returns true if the URIs array contain a gold:translation or gold:freeTranslation def translation?(relation_array) @@ -106,35 +94,6 @@ def get_inter_portal_ui_link(uri, process_name) end end - def onts_and_views_for_select - @onts_and_views_for_select = [] - ontologies = LinkedData::Client::Models::Ontology.all(include: "acronym,name", include_views: true) - ontologies.each do |ont| - next if (ont.acronym.nil? || ont.acronym.empty?) - ont_acronym = ont.acronym - ont_display_name = "#{ont.name.strip} (#{ont_acronym})" - @onts_and_views_for_select << [ont_display_name, ont_acronym] - end - @onts_and_views_for_select.sort! { |a, b| a[0].downcase <=> b[0].downcase } - return @onts_and_views_for_select - end - - def get_concept_mappings(concept) - mappings = concept.explore.mappings - # Remove mappings where the destination class exists in an ontology that the logged in user doesn't have permissions to view. - # Workaround for https://github.com/ncbo/ontologies_api/issues/52. - mappings.delete_if do |mapping| - #mapping.classes.reject! { |cls| (cls.id == concept.id) && (cls.links['ontology'] == concept.links['ontology']) } - begin - ont = mapping.classes[0].explore.ontology - ont.errors && ont.errors.grep(/Access denied/).any? - rescue => e - Rails.logger.warn t('mappings.mapping_issue', mapping: mapping.inspect, message: e.message) - false - end - end - end - def internal_mapping?(cls) cls.links['self'].to_s.start_with?(LinkedData::Client.settings.rest_url) || ($LOCAL_IP.present? && cls.links['self'].to_s.include?($LOCAL_IP)) end @@ -212,4 +171,35 @@ def concept_mappings_loader(ontology_acronym: ,concept_id: ) end) end end + + def client_filled_modal + link_to_modal "", "" + end + + def mappings_bubble_view_legend + content_tag(:div, class: 'mappings-bubble-view-legend') do + mappings_legend_section('Bubble size:', 'The global number of mappings with all other ontologies.', 'mappings-bubble-size-legend') + + mappings_legend_section('Color degree:', 'The number of mappings with the selected ontology.', 'mappings-bubble-color-legend') + end + end + + def mappings_legend_section(title_text, description_text, css_class) + 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, "#{title_text} ", class: 'd-inline') + + content_tag(:span, description_text) + end + + mappings_legend(css_class) + end + end + end + + def mappings_legend(css_class) + content_tag(:div, class: css_class) do + content_tag(:div, "Less mappings", class: 'mappings-legend-text') + + (1..6).map { |i| content_tag(:div, "", class: "bubble bubble#{i}") }.join + + content_tag(:div, "More mappings", class: 'mappings-legend-text') + end + end end diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index b93b16d5e4..e490eb3a17 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -99,3 +99,7 @@ application.register('form-url', FormUrlController) import OntologiesSelector from "./ontologies_selector_controller" application.register("ontologies-selector", OntologiesSelector) + + +import MappingsController from "./mappings_visualization_controller" +application.register('mappings', MappingsController) \ No newline at end of file diff --git a/app/javascript/controllers/mappings_visualization_controller.js b/app/javascript/controllers/mappings_visualization_controller.js new file mode 100644 index 0000000000..e75813d368 --- /dev/null +++ b/app/javascript/controllers/mappings_visualization_controller.js @@ -0,0 +1,259 @@ +import { Controller } from '@hotwired/stimulus' +import { useMappingsDrawBubbles } from '../mixins/useMappingsBubbles' + +export default class extends Controller { + + static values = { + mappingsList: Object, + zoomRatio: { type: Number, default: 1 }, + acronym: String, + containerId: { type: String, default: 'mappings-bubbles-view' } + } + + static targets = ['frame', 'bubbles', 'submit', 'modal', 'selector', 'ontologies', 'loader'] + + connect () { + + this.drawBubbles = (mappingsList) => { + const zoomRatio = this.zoomRatioValue + const width = 600 * zoomRatio + const height = 600 * zoomRatio + const margin = 1 + const logScaleFactor = 10 + const normalization_ratio = this.#normalizationRatio(mappingsList) + + const data = Object.entries(mappingsList).map(([key, value]) => ({ + ontology_name: key.split('/').pop(), + ontology_mappings: value, + })) + + useMappingsDrawBubbles(data, width, height, margin, this.bubblesTarget, normalization_ratio, logScaleFactor) + + this.#centerScroll(this.frameTarget) + } + + this.drawBubbles(this.mappingsListValue) + + if (this.#selectionDisabled()) { + this.#clickOnSelectedAcronymBubble() + } + + } + + filterOntologies () { + const selectOptions = Array.from(this.ontologiesTarget.querySelector('select').selectedOptions) + const acronyms = selectOptions.map(option => option.value) + + const filteredList = Object.fromEntries( + Object.entries(this.mappingsListValue).filter(([key]) => acronyms.includes(key)) + ) + + this.drawBubbles(filteredList) + } + + submit (event) { + const itemElement = event.currentTarget.querySelector('.item') + if (!itemElement) return + + this.submitTarget.click() + + const selectAcronym = event.currentTarget.querySelector('select').value + + const bubblesContainer = document.getElementById(this.containerIdValue) + const selectedBubble = bubblesContainer.querySelector('[data-selected="true"]') + const currentBubble = bubblesContainer.querySelector(`[data-acronym="${selectAcronym}"]`) + + if (selectedBubble && selectedBubble.dataset.acronym === selectAcronym) return + + const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }) + + if (currentBubble && (currentBubble.getAttribute('data-enabled') === 'false' || currentBubble.getAttribute('data-highlighted') === 'true')) { + selectedBubble.dispatchEvent(clickEvent) + } + + if (currentBubble) currentBubble.dispatchEvent(clickEvent) + } + + zoomIn () { + this.zoomRatioValue++ + this.drawBubbles(this.mappingsListValue) + } + + zoomOut () { + if (this.zoomRatioValue > 1) { + this.zoomRatioValue-- + this.drawBubbles(this.mappingsListValue) + } + } + + selectBubble (event) { + const selected_bubble = event.currentTarget + + if (selected_bubble.getAttribute('data-enabled') === 'false') { + // user clicks on a bubble that is disabled (has no mappings with the current bubble) do nothing + return + } + + this.#toggleAnimation() + + if (selected_bubble.getAttribute('data-highlighted') === 'true') { + // user clicks on a bubble that have mapping with the current highlighted bubble, should show a modal with the mappings + this.#showMappingsModal(selected_bubble) + this.#toggleAnimation() + } else if (selected_bubble.getAttribute('data-selected') === 'true') { + // user clicks on current bubble (should deselect it, but nothing happen if we're in ontology mappings section not the page) + this.#unSelectBubble(selected_bubble) + this.#toggleAnimation() + } else { + this.#selectBubble(selected_bubble) + } + } + + #selectBubble (selected_bubble) { + + const acronym = selected_bubble.getAttribute('data-acronym') + let url = '/mappings/count/' + acronym + selected_bubble.setAttribute('data-selected', 'true') + + if (this.#selectionEnabled()) { + const input = this.selectorTarget.querySelector('input') + input.value = acronym + input.dispatchEvent(new Event('input', { bubbles: true })) + + const selectValue = Array.from(this.selectorTarget.querySelectorAll('.option')) + .find(option => option.getAttribute('data-value') === acronym) + + if (selectValue) selectValue.click() + } + + this.#fetchMappingsDataAndSetBubblesColor(url) + } + + #unSelectBubble (selected_bubble) { + + if (this.#selectionDisabled()) return + + selected_bubble.setAttribute('data-selected', 'false') + + const selected_circle = selected_bubble.querySelector('circle') + selected_circle.style.fill = 'var(--primary-color)' + + const leafs = this.bubblesTarget.querySelectorAll('.leaf') + leafs.forEach(leaf => { + const circle = leaf.querySelector('circle') + circle.style.fill = 'var(--primary-color)' + circle.style.opacity = '1' + leaf.setAttribute('data-enabled', 'true') + leaf.setAttribute('data-highlighted', 'false') + }) + } + + #showMappingsModal (selected_bubble) { + const selected_leaf = this.bubblesTarget.querySelector('[data-selected="true"]') + const acronym = selected_leaf.getAttribute('data-acronym') + const target_acronym = selected_bubble.getAttribute('data-acronym') + this.modalTarget.querySelector('a').href = `/mappings/show_mappings?id=${acronym}&target=${target_acronym}` + this.modalTarget.querySelector('a').click() + } + + #fetchMappingsDataAndSetBubblesColor (url) { + fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json' + }, + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok') + } + return response.json() + }) + .then(data => { + const mappings_list = data.map(item => ({ + acronym: item.target_ontology.acronym, + count: item.count + })) + + this.#setBubblesColors(mappings_list) + + this.#toggleAnimation() + }) + .catch(error => { + console.error('Error fetching or processing data:', error) + // Handle errors here + }) + } + + #setBubblesColors (mappings_list) { + const bubblesContainer = this.bubblesTarget + const leafs = bubblesContainer.querySelectorAll('.leaf') + const max_mappings_count = mappings_list.reduce((max, item) => Math.max(max, item.count), -Infinity) + + leafs.forEach(leaf => { + const circle = leaf.querySelector('circle') + const acronym = leaf.getAttribute('data-acronym') + + const matchingMapping = mappings_list.find(item => item.acronym === acronym) + + if (matchingMapping) { + leaf.setAttribute('data-highlighted', 'true') + circle.style.fill = 'var(--primary-color)' + + const opacity = (matchingMapping.count / max_mappings_count + Math.log(matchingMapping.count + 1)) / 10 + 0.3 + circle.style.opacity = `${opacity}` + } else { + leaf.setAttribute('data-enabled', 'false') + circle.style.fill = 'var(--light-color)' + } + }) + + const selected_leaf = bubblesContainer.querySelector('[data-selected="true"]') + selected_leaf.setAttribute('data-enabled', 'true') + + const selected_circle = selected_leaf.querySelector('circle') + selected_circle.style.fill = 'var(--secondary-color)' + } + + + + #centerScroll (frame) { + frame.scrollTop = frame.scrollHeight / 2 - frame.clientHeight / 2 + frame.scrollLeft = frame.scrollWidth / 2 - frame.clientWidth / 2 + } + + #normalizationRatio (ontologies_hash) { // try to find the biggest multiple of 10 inferior to the max mappings value + const maxValue = Math.max(...Object.values(ontologies_hash)) + let normalization_ratio = 1 + while (maxValue / normalization_ratio > 10) { + normalization_ratio *= 10 + } + return normalization_ratio + } + + #toggleAnimation () { + this.loaderTarget.classList.toggle('d-none') + this.bubblesTarget.classList.toggle('d-none') + } + + #selectionEnabled () { + return !this.hasAcronymValue + } + + #selectionDisabled () { + return !this.#selectionEnabled() + } + + #clickOnSelectedAcronymBubble () { + setTimeout(() => { + const currentBubble = this.bubblesTarget.querySelector(`[data-acronym="${this.acronymValue}"]`) + let clickEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }) + currentBubble.dispatchEvent(clickEvent) + }, 100) + + } +} \ No newline at end of file diff --git a/app/javascript/mixins/useMappingsBubbles.js b/app/javascript/mixins/useMappingsBubbles.js new file mode 100644 index 0000000000..60c5a36830 --- /dev/null +++ b/app/javascript/mixins/useMappingsBubbles.js @@ -0,0 +1,110 @@ +import * as d3 from 'd3' + +class BubbleData { + constructor(ontology_name, ontology_mappings) { + this.ontology_name = ontology_name; + this.ontology_mappings = ontology_mappings; + } +} + +/** + * Draws bubbles using D3.js based on the provided data. + * @param {Array} data - The array of BubbleData objects containing ontology names and mappings. + * @param {number} width - The width of the SVG container. + * @param {number} height - The height of the SVG container. + * @param {number} margin - The margin for the SVG container. + * @param {HTMLElement} bubblesTarget - The target HTML element to append the SVG container to. + * @param {number} normalization_ratio - The normalization ratio for bubble size calculation. + * @param {number} logScaleFactor - The logarithmic scale factor for bubble size calculation. + */ + +export function useMappingsDrawBubbles(data, width, height, margin, bubblesTarget, normalization_ratio, logScaleFactor) { + // Define pack layout + const pack = d3.pack() + .size([width - margin, height - margin]) + .padding(3); + + // Create hierarchy and sum for bubble sizes + const root = d3.hierarchy({ children: data }) + .sum(d => calculateBubbleSize(d)); + + // Create SVG container + const svg = d3.select(`#${bubblesTarget.id}`) + .append('svg') + .attr('width', width) + .attr('height', height) + .append('g') + .attr('transform', `translate(${margin}, ${margin})`) + + // Create nodes and bind data + const node = svg.selectAll('.node') + .data(pack(root).descendants().slice(1)) // Exclude the root node + .enter().append('g') + .attr('class', d => d.children ? 'node mappings-bubble' : 'leaf mappings-bubble') + .attr('transform', d => `translate(${d.x},${d.y})`) + .attr('data-action', 'click->mappings#selectBubble') + .attr('data-acronym', d => d.data.ontology_name) + .attr('data-enabled', d => 'true'); + + // Create circles + const circle = node.append('circle') + .attr('r', d => d.r) + .style('fill', 'var(--primary-color)'); + + // Display ontology names and mappings + const textOntology = node.append('text') + .attr('dy', '.35em') + .style('text-anchor', 'middle') + .style('font-size', '16px') + .style('fill', 'white') + .style('font-weight', '600') + .text(d => displayOntologyName(d)); + + const textMappings = node.append('text') + .attr('dy', '1.5em') + .style('text-anchor', 'middle') + .style('font-size', '12px') + .style('fill', 'white') + .text(d => displayMappings(d)); + + // Show tooltips on hover + circle.on('mouseover', (event, d) => showTooltip(event, d)) + .on('mouseout', () => hideTooltip()); + + // Function to calculate bubble size + function calculateBubbleSize(d) { + return d.ontology_mappings / normalization_ratio + Math.log(d.ontology_mappings + 1) / logScaleFactor; + } + + // Function to display ontology name + function displayOntologyName(d) { + return (d.r > d.data.ontology_name.length * 5 && d.r > 20) ? d.data.ontology_name : ''; + } + + // Function to display mappings count + function displayMappings(d) { + return (d.r > d.data.ontology_name.length * 5 && d.r > 20) ? d.data.ontology_mappings : ''; + } + + // Function to show tooltip + function showTooltip(event, d) { + if (!(d.r > d.data.ontology_name.length * 5 && d.r > 20)) { + // Remove existing tooltip + d3.selectAll('.bubble-tooltip').remove(); + + // Calculate tooltip position based on mouse coordinates + const tooltip = d3.select('body') + .append('div') + .attr('class', 'bubble-tooltip') + .style('left', `${event.pageX + 10}px`) // Adjust position relative to mouse pointer + .style('top', `${event.pageY + 10}px`) // Adjust position relative to mouse pointer + .html(`${d.data.ontology_name}
${d.data.ontology_mappings}`); + } + } + + // Function to hide tooltip + function hideTooltip() { + // Remove tooltip on mouseout + d3.selectAll('.bubble-tooltip').remove(); + } +} diff --git a/app/views/mappings/_concept_mappings_selector.html.haml b/app/views/mappings/_concept_mappings_selector.html.haml deleted file mode 100644 index c00a40fb44..0000000000 --- a/app/views/mappings/_concept_mappings_selector.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -.card - #headingTwo.card-header - %h2.mb-0 - %button.btn.btn-link.btn-block.text-left.collapsed{"data-target" => "#collapseTwo", "data-toggle" => "collapse", :type => "button"} - = t('mappings.find_mappings') - = link_to(Rails.configuration.settings.links[:mappings], id: "mappings-help") do - %i.fas.fa-question-circle.fa-lg{"aria-hidden": "true"} - #collapseTwo.collapse{"data-parent" => "#accordionExample"} - .card-body - %div - = render partial: 'shared/concept_picker', locals: {name: :concept_mapping_selector, concept_label: '', ontology_acronym: t('mappings.all'), include_definition: true } - %div.mt-1 - = render TurboFrameComponent.new(id:'concept_mappings') -:javascript - const picker_name = 'concept_mapping_selector' - const frame = document.getElementById('concept_mappings') - $('input[name="concept_mapping_selector"]').on('selected', () => { - const ontology_id = $(`input[name="${picker_name}_bioportal_ontology_id"]`).val() - const concept_id = $(`input[name="${picker_name}_bioportal_concept_id"]`).val() - frame.src = `/ajax/mappings/get_concept_table?ontologyid=${ontology_id}&conceptid=${encodeURIComponent(concept_id)}` - }) diff --git a/app/views/mappings/_count.html.haml b/app/views/mappings/_count.html.haml index 66b6d96e59..e3cd5f9336 100644 --- a/app/views/mappings/_count.html.haml +++ b/app/views/mappings/_count.html.haml @@ -1,18 +1,35 @@ -= render TableComponent.new(id: 'mapping_count_table') do |t| - - t.header do |h| - - h.th {t("mappings.count.ontology")} - - h.th {t("mappings.count.mappings")} += turbo_frame_tag "mappings_table" do + .summary-mappings-tab + .summary-mappings-tab-table + = render TableComponent.new(id: 'summary-mappings-table', borderless: true, outline: true, searching: true, sort_column: '0', no_init_sort: true) do |t| + - t.header do |h| + - h.th {t("mappings.count.ontology")} + - h.th {t("mappings.count.mappings")} - - if @mapping_counts.blank? - - t.row do |r| - - r.td {t("mappings.count.no_mappings")} - - r.td {' '} - - else - - @mapping_counts.each do |mapping_count| - - t.row do |r| - - r.td do - - title = mapping_count[:target_ontology].name - = link_to_modal title, mappings_show_mappings_path(id: @ontology_acronym ,target: mapping_count[:target_ontology].id), data: { show_modal_title_value: title, show_modal_size_value: 'modal-xl'} - - r.td do - = number_with_delimiter(mapping_count[:count], delimiter: ',') + - if @mapping_counts.blank? + - t.row do |r| + - r.td {t("mappings.count.no_mappings")} + - r.td {' '} + - else + - @mapping_counts.each do |mapping_count| + - t.row do |r| + - r.td do + - title = mapping_count[:target_ontology].name + = link_to_modal title, mappings_show_mappings_path(id: @ontology_acronym ,target: mapping_count[:target_ontology].id), data: { show_modal_title_value: title, show_modal_size_value: 'modal-xl'} + - r.td do + = number_with_delimiter(mapping_count[:count], delimiter: ',') + - if @ontologies_mapping_count + .summary-mappigs-page-container{'data-controller': 'mappings', + 'data-mappings-mappings-list-value': "#{@ontologies_mapping_count.to_h.to_json}", + 'data-mappings-acronym-value': @ontology_acronym, + 'data-mappings-api-url-value': rest_url + } + .mappings-bubble-view-frame{'data-mappings-target': 'frame'} + #mappings-bubbles-view{'data-mappings-target': 'bubbles'} + .mapping-bubbles-loader.d-none{'data-mappings-target': 'loader'} + = render LoaderComponent.new(type: 'pulsing') + .d-none{'data-mappings-target': 'modal'} + = client_filled_modal + .d-flex.justify-content-center + = render Display::InfoTooltipComponent.new(text: mappings_bubble_view_legend) \ No newline at end of file diff --git a/app/views/mappings/_form.html.haml b/app/views/mappings/_form.html.haml index 8bd7d159ea..264b680e70 100644 --- a/app/views/mappings/_form.html.haml +++ b/app/views/mappings/_form.html.haml @@ -38,5 +38,5 @@ = select("mapping", "relation", options_for_select(@mapping_relation_options, @selected_relation), {}, class: "form-control") %div.form-group - = submit_tag t("mappings.form.save") , class:'btn btn-success btn-block' + = submit_tag t("mappings.form.save") , class:'regular-button primary-button slim' diff --git a/app/views/mappings/_mapping_table.html.haml b/app/views/mappings/_mapping_table.html.haml index ad31dcd850..1d91b3b5bb 100644 --- a/app/views/mappings/_mapping_table.html.haml +++ b/app/views/mappings/_mapping_table.html.haml @@ -1,14 +1,3 @@ --# called from mappings_controller in several ways: --# 1. mappings_controller::get_concept_table via /app/views/mappings/_concept_mappings.html.haml --# 2. directly from mappings_controller::get_concept_table --#NOTES on control over mapping deletion: --#deleteMappings() is a callback that is called by "#delete_mappings_button" created below. --#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() --# --# The delete mappings button display is controlled by JS on page ready (see bp_mappings.js) --# check_box_tag(name, value = "1", checked = false, options = {}) = check_box_tag "delete_mappings_permission", @delete_mapping_permission, @delete_mapping_permission, style: "display: none;" %div#concept_mappings_tables_div = render_alerts_container(MappingsController) diff --git a/app/views/mappings/_ontology_mappings.html.haml b/app/views/mappings/_ontology_mappings.html.haml deleted file mode 100644 index 9890a668cb..0000000000 --- a/app/views/mappings/_ontology_mappings.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.card - #headingOne.card-header - %h2.mb-0 - %button.btn.btn-link.btn-block.text-left{"data-target" => "#collapseOne", "data-toggle" => "collapse", :type => "button"} - = t('mappings.intro').html_safe - = link_to(Rails.configuration.settings.links[:mappings], id: "mappings-help", "aria-label": "View mappings help") do - %i.fas.fa-question-circle.fa-lg{"aria-hidden": "true"} - #collapseOne.collapse{"data-parent" => "#accordionExample"} - .card-body - %div#mappings_select - - if @options.empty? - = t('mappings.no_mappings_available') - - else - - @options.unshift(['','']) - %div{onchange: "loadMappings(event.target.value)"} - = select_input(name: 'search[ontologies]', id: 'search_ontologies', label: '', values: @options, placeholder: t('mappings.select_ontologies_list')) - #mapping_load - %img{src: asset_path("jquery.simple.tree/spinner.gif")}/ - = t('mappings.loading_mappings') - #mappingCount{style:'min-height: 300px;'} diff --git a/app/views/mappings/_show.html.haml b/app/views/mappings/_show.html.haml index 62dbcd5018..6ca1cffe1f 100644 --- a/app/views/mappings/_show.html.haml +++ b/app/views/mappings/_show.html.haml @@ -1,11 +1,12 @@ = render_in_modal do #mappings.paginate_ajax{:style => "overflow: auto; max-height: 600px;"} #mapping_results - = will_paginate @page_results, :update => 'mappings', :params => { :target => params[:target] } + .mappings-table-pagination + = will_paginate @page_results, :update => 'mappings', :params => { :target => params[:target] } - if @mappings.nil? or @mappings.empty? = t("mappings.show.no_mappings_found") - - else - %table.zebra.w-100 + - else + %table.table-content.table-content-stripped %thead %th #{@ontology_name} %th #{@target_ontology_name} @@ -22,7 +23,8 @@ = ajax_to_external_cls(cls) %td #{map.source} #{(map.process || {})[:source_name]} - = will_paginate @page_results, :update => 'mappings', :params => { :target => params[:target] } + .mappings-table-pagination + = will_paginate @page_results, :update => 'mappings', :params => { :target => params[:target] } :javascript jQuery(document).ready(function(){ diff --git a/app/views/mappings/bulk_loader/_loader.html.haml b/app/views/mappings/bulk_loader/_loader.html.haml deleted file mode 100644 index fde3140323..0000000000 --- a/app/views/mappings/bulk_loader/_loader.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -= render_in_modal do - %div.d-flex.flex-column{:style => "overflow: auto; max-height: 600px;"} - = render TurboFrameComponent.new(id: 'file_loader_result') do - %div.my-2 - .card.mb-2 - %div - %h2 - %button.btn.btn-link{"data-target" => "#collapseOne", "data-toggle" => "collapse", :type => "button"} - = t("mappings.bulk_loader.loader.example_of_valid_file") - #collapseOne.collapse - .card-body - %pre - %code - = JSON.pretty_generate @example_code - = form_with url: '/mappings/loader', method: :post, multipart: true, data: { turbo: true} do - %div - = render Input::FileInputComponent.new(name: :file) - %button.btn.btn-secondary.btn-block.mt-2{type:'submit'} - = t("mappings.bulk_loader.loader.save") diff --git a/app/views/mappings/index.html.haml b/app/views/mappings/index.html.haml index 24a5d1dc9a..44b509894b 100644 --- a/app/views/mappings/index.html.haml +++ b/app/views/mappings/index.html.haml @@ -1,14 +1,23 @@ - @title= t('mappings.title') -%div.container - %div#mappings_container.container-fluid.py-4.flex-grow-1 - %h1.my-1= t('mappings.title') - - %div#mappings_uploader.my-2 - = link_to_modal t('mappings.upload_mappings') , "/mappings/loader", class: "btn btn-primary btn-block", - data: { show_modal_title_value: t('mappings.mappings_bulk_load'), show_modal_size_value: 'modal-xl'} - %hr.my-3.w-100 - #accordionExample.accordion - = render partial: 'ontology_mappings' - = render partial: 'concept_mappings_selector' - +.mappigs-page-container{'data-controller': 'mappings', + 'data-mappings-mappings-list-value': "#{@ontologies_mapping_count.to_h.to_json}", + 'data-mappings-api-url-value': rest_url + } + .mappings-page-subcontainer + .mappings-page-title + .text + = @title + .line + .mappings-page-decription + = t('mappings.description') + = render TabsContainerComponent.new do |c| + - c.item(title: t('mappings.tabs.bubble_view'), selected: true) + - c.item_content do + = render partial: '/mappings/tab_sections/bubble_view' + - c.item(title: t('mappings.tabs.table_view')) + - c.item_content do + = render partial: '/mappings/tab_sections/table_view' + - c.item(title: t('mappings.tabs.upload_mappings')) + - c.item_content do + = render partial: '/mappings/tab_sections/upload_mappings' \ No newline at end of file diff --git a/app/views/mappings/tab_sections/_bubble_view.html.haml b/app/views/mappings/tab_sections/_bubble_view.html.haml new file mode 100644 index 0000000000..31a335fa1e --- /dev/null +++ b/app/views/mappings/tab_sections/_bubble_view.html.haml @@ -0,0 +1,26 @@ +.mappings-bubble-view-container + .upload-mappings + .card.upload-mappings-example.ontologies + = render(Layout::RevealComponent.new(toggle: true, selected: false)) do |c| + - c.button do + .title-bar + = inline_svg_tag 'icons/settings.svg' + = t('mappings.filter_ontologies') + - c.container do + .mappings-page-ontologies-selector{'data-mappings-target': 'ontologies'} + = ontologies_selector(id:'mappings_page_ontologies' ,name: 'ontologies[]') + .selector-button{'data-action': 'click->mappings#filterOntologies'} + = regular_button('filter-bubbles', t('mappings.filter_bubbles')) + + .mappings-bubble-view-frame{'data-mappings-target': 'frame'} + #mappings-bubbles-view{'data-mappings-target': 'bubbles'} + .mapping-bubbles-loader.d-none{'data-mappings-target': 'loader'} + = loader_component + = info_tooltip(mappings_bubble_view_legend) + .mappings-zoom-buttons + .in{'data-action': 'click->mappings#zoomIn'} + = inline_svg_tag 'icons/zoom-in.svg' + .out{'data-action': 'click->mappings#zoomOut'} + = inline_svg_tag 'icons/zoom-out.svg' + .d-none{'data-mappings-target': 'modal'} + = client_filled_modal \ No newline at end of file diff --git a/app/views/mappings/tab_sections/_table_view.html.haml b/app/views/mappings/tab_sections/_table_view.html.haml new file mode 100644 index 0000000000..5ad3ac571e --- /dev/null +++ b/app/views/mappings/tab_sections/_table_view.html.haml @@ -0,0 +1,8 @@ +.mappings-table-view-container + .mappings-ontologies-select + = form_tag('/mappings/count/fake_id', method: :get, novalidate: true, data: { turbo: true, turbo_frame: 'mappings_table' }) do + .mappings-selector{data: {action: 'change->mappings#submit', 'mappings-target': 'selector'}} + = select_input(name: "ontology", values: @options, placeholder: t('mappings.intro')) + %input.d-none{ type: 'submit', 'data-mappings-target': 'submit'} + %div + = render TurboFrameComponent.new(id:"mappings_table") \ No newline at end of file diff --git a/app/views/mappings/tab_sections/_upload_mappings.html.haml b/app/views/mappings/tab_sections/_upload_mappings.html.haml new file mode 100644 index 0000000000..e10cb80645 --- /dev/null +++ b/app/views/mappings/tab_sections/_upload_mappings.html.haml @@ -0,0 +1,16 @@ +.upload-mappings + .card.upload-mappings-example + .title-bar{"data-target" => "#collapseOne", "data-toggle" => "collapse"} + = t("mappings.bulk_loader.loader.example_of_valid_file") + #collapseOne.collapse + .card-body + %pre + %code + = JSON.pretty_generate @example_code + + = form_with url: '/mappings/loader', method: :post, multipart: true, data: { turbo: true, turbo_frame: 'file_loader_result'} do + %div.mb-3 + = render Input::FileInputComponent.new(name: :file) + = render Buttons::RegularButtonComponent.new(id:'upload-mappings-button', value: "Save", variant: "primary", size: 'slim', type:'submit', state: "regular") + .mt-3 + = render TurboFrameComponent.new(id: 'file_loader_result') \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 4130b985ee..cba920dd17 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -558,6 +558,13 @@ en: ontology_visits: Ontology visits mappings: 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 + filter_ontologies: Filter ontologies in the bubble view + filter_bubbles: Filter bubbles external_mappings: "External Mappings (%{number_with_delimiter})" interportal_mappings: "Interportal Mappings - %{acronym} (%{number_with_delimiter})" test_bulk_load: This is the mappings produced to test the bulk load @@ -570,7 +577,6 @@ en: find_mappings: Find mappings of a class/concept intro: Find all the mappings of an ontology loading_mappings: Loading mappings... - mappings_bulk_load: Upload mappings in bulk from a source file no_mappings_available: No mappings available title: Mappings upload_mappings: Upload mappings diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 3dd03f4047..ba8dbc88f0 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -566,69 +566,75 @@ fr: ontology_visits: Visites d'ontologies mappings: - select_ontologies_list: "Sélectionner des ontologies" - external_mappings: "Alignements Externes (%{number_with_delimiter})" - interportal_mappings: "Alignements Interportail - %{acronym} (%{number_with_delimiter})" - test_bulk_load: Ceci est les alignements produits pour tester le chargement en masse - mapping_created: Alignement créé - mapping_updated: Alignement mis à jour + 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 + filter_ontologies: Filtrer les ontologies dans la vue en bulles + filter_bubbles: Filtrer les bulles + external_mappings: "Mappings externes (%{number_with_delimiter})" + interportal_mappings: "Mappings interportails - %{acronym} (%{number_with_delimiter})" + test_bulk_load: Ceci sont les mappings créés pour tester le chargement en masse + mapping_created: Mapping créé + mapping_updated: Mapping mis à jour mapping_deleted: "%{map_id} supprimé avec succès" - mapping_not_found: "Alignement %{id} non trouvé" + mapping_not_found: "Mapping %{id} non trouvé" error_of_source_and_target: Les concepts source et cible doivent être spécifiés - mapping_issue: "Problème d'alignement avec '%{mapping}' : %{message}" - find_mappings: Trouver les alignements d'une classe/concept - intro: Trouver tous les alignements d'une ontologie - loading_mappings: Chargement des alignements... - mappings_bulk_load: Télécharger des alignements en masse à partir d'un fichier source - no_mappings_available: Aucun alignement disponible - title: Alignements - upload_mappings: Télécharger des alignements - all: Tout + mapping_issue: "Problème de mapping avec '%{mapping}' : %{message}" + find_mappings: Trouver tous les mappings d'une classe/concept + intro: Trouver tous les mappings d'une ontologie + loading_mappings: Chargement des mappings... + no_mappings_available: Aucun mapping disponible + title: Mappings + upload_mappings: Télécharger des mappings + select_ontologies_list: Sélectionner les ontologies count: ontology: Ontologie - mappings: Alignements - no_mappings: Il n'y a aucun alignement vers ou depuis cette ontologie + mappings: Mappings + no_mappings: Il n'y a aucun mapping vers ou depuis cette ontologie form: source_class: Classe source - mapping_name: Description du alignement (nom) + mapping_name: Description du mapping (nom) contact_info: Informations de contact - mapping_source_name: Nom de la source (ID de l'ensemble de alignement) + mapping_source_name: Nom source (ID de l'ensemble de mappings) mapping_comment: Commentaire - mapping_relation: Type de relation de alignement - save: Sauvegarder + mapping_relation: Type de relation de mapping + save: Enregistrer mapping_table: - mapping_to: Alignement vers + mapping_to: Mapping vers relations: Relations source: Source type: Type actions: Actions - no_mappings: Il n'y a actuellement aucun alignement pour cette classe. + no_mappings: Il n'y a actuellement aucun mapping pour cette classe. mapping_type_selector: - mapping_type: Type de alignement + mapping_type: Type de mapping internal: Interne - interportal: InterPortail + interportal: Interportail external: Externe target_class: Classe cible details: Détails ontology_acronym: Ontologie (acronyme) class: Classe - ontology_acronym_placeholder: Entrez l'acronyme de l'ontologie + ontology_acronym_placeholder: Entrez l'ACRONYM de l'ontologie class_uri_placeholder: Entrez l'URI de la classe ontology_uri_placeholder: Entrez l'URI de l'ontologie show_line: - edit_modal: Éditer + edit_modal: Modifier delete_button: Supprimer - turbo_confirm: Êtes-vous sûr ? - edit_mapping: Éditer l'alignement pour %{preflabel} + turbo_confirm: Êtes-vous sûr(e) ? + edit_mapping: Modifier le mapping pour %{preflabel} show: - no_mappings_found: Aucun alignement trouvé + no_mappings_found: Aucun mapping trouvé bulk_loader: loader: example_of_valid_file: Voir un exemple de fichier valide - save: Sauvegarder + save: Enregistrer loaded_mappings: - mappings_created: "%{size} alignements créés avec succès" - id: Id + mappings_created: "%{size} mappings créés avec succès" + id: ID source: Source target: Cible relation: Relation @@ -636,6 +642,7 @@ fr: actions: Actions see_other_properties: Voir d'autres propriétés + agents: not_found_agent: Agent avec id %{id} add_agent: Nouvel agent ajouté avec succès diff --git a/config/routes.rb b/config/routes.rb index 2759ffd627..b556a90685 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,6 +18,7 @@ post 'agents/:id/usages', to: 'agents#update_agent_usages', constraints: { id: /.+/ } resources :agents, constraints: { id: /.+/ } post 'agents/:id', to: 'agents#update', constraints: { id: /.+/ } + resources :ontolobridge do post :save_new_term_instructions, on: :collection end @@ -29,7 +30,6 @@ get '/users/subscribe/:username', to: 'users#subscribe' get '/users/un-subscribe/:email', to: 'users#un_subscribe' - get '/mappings/loader', to: 'mappings#loader' post '/mappings/loader', to: 'mappings#loader_process' get 'mappings/count/:id', to: 'mappings#count', constraints: { id: /.+/ } get 'mappings/show_mappings', to: 'mappings#show_mappings' diff --git a/package.json b/package.json index 3e4673d338..392e650aaa 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@hotwired/turbo-rails": "^7.1.1", "@triply/yasgui": "^4.2.28", "chart.js": "^4.4.1", + "d3": "^7.8.5", "datatables.net-dt": "^1.13.8", "debounce": "^1.2.1", "esbuild": "^0.14.41", diff --git a/yarn.lock b/yarn.lock index 5cb3f6d1cc..131990647f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -265,6 +265,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@7: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + commander@^6.1.0: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" @@ -280,6 +285,250 @@ cookiejar@^2.1.2: resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== +"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +d3-axis@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" + integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== + +d3-brush@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" + integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "3" + d3-transition "3" + +d3-chord@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" + integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== + dependencies: + d3-path "1 - 3" + +"d3-color@1 - 3", d3-color@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-contour@4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc" + integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA== + dependencies: + d3-array "^3.2.0" + +d3-delaunay@6: + version "6.0.4" + resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" + integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== + dependencies: + delaunator "5" + +"d3-dispatch@1 - 3", d3-dispatch@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3", d3-drag@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-dsv@1 - 3", d3-dsv@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" + integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== + dependencies: + commander "7" + iconv-lite "0.6" + rw "1" + +"d3-ease@1 - 3", d3-ease@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +d3-fetch@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" + integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== + dependencies: + d3-dsv "1 - 3" + +d3-force@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" + integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== + dependencies: + d3-dispatch "1 - 3" + d3-quadtree "1 - 3" + d3-timer "1 - 3" + +"d3-format@1 - 3", d3-format@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +d3-geo@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e" + integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA== + dependencies: + d3-array "2.5.0 - 3" + +d3-hierarchy@3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" + integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== + +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-polygon@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" + integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== + +"d3-quadtree@1 - 3", d3-quadtree@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" + integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== + +d3-random@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" + integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== + +d3-scale-chromatic@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a" + integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g== + dependencies: + d3-color "1 - 3" + d3-interpolate "1 - 3" + +d3-scale@4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +"d3-selection@2 - 3", d3-selection@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + +d3-shape@3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4", d3-time-format@4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +"d3-timer@1 - 3", d3-timer@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +"d3-transition@2 - 3", d3-transition@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + +d3@^7.8.5: + version "7.8.5" + resolved "https://registry.yarnpkg.com/d3/-/d3-7.8.5.tgz#fde4b760d4486cdb6f0cc8e2cbff318af844635c" + integrity sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA== + dependencies: + d3-array "3" + d3-axis "3" + d3-brush "3" + d3-chord "3" + d3-color "3" + d3-contour "4" + d3-delaunay "6" + d3-dispatch "3" + d3-drag "3" + d3-dsv "3" + d3-ease "3" + d3-fetch "3" + d3-force "3" + d3-format "3" + d3-geo "3" + d3-hierarchy "3" + d3-interpolate "3" + d3-path "3" + d3-polygon "3" + d3-quadtree "3" + d3-random "3" + d3-scale "4" + d3-scale-chromatic "3" + d3-selection "3" + d3-shape "3" + d3-time "3" + d3-time-format "4" + d3-timer "3" + d3-transition "3" + d3-zoom "3" + data-uri-to-buffer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" @@ -338,6 +587,13 @@ define-data-property@^1.1.1: gopd "^1.0.1" has-property-descriptors "^1.0.0" +delaunator@5: + version "5.0.1" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" + integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== + dependencies: + robust-predicates "^3.0.2" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -590,6 +846,13 @@ highlight.js@^11.9.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0" integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw== +iconv-lite@0.6: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -600,6 +863,11 @@ inherits@^2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + jquery@>=1.7, jquery@^3.5.0: version "3.7.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" @@ -794,11 +1062,26 @@ remove-accents@^0.4.2: resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.4.tgz#73704abf7dae3764295d475d2b6afac4ea23e4d9" integrity sha512-EpFcOa/ISetVHEXqu+VwI96KZBmq+a8LJnGkaeFw45epGlxIZz5dhEEnNZMsQXgORu3qaMoLX4qJCzOik6ytAg== +robust-predicates@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" + integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== + +rw@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== + safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + semver@^7.3.2: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"