diff --git a/.gitignore b/.gitignore index e5eb8ea5bd..ab0ea98b5a 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ node_modules /config/credentials/test.key /config/credentials/staging.key /config/credentials/production.key +/config/credentials/appliance.key /app/assets/builds/* !/app/assets/builds/.keep diff --git a/Gemfile b/Gemfile index 215fecf35e..c423cef31f 100644 --- a/Gemfile +++ b/Gemfile @@ -68,9 +68,16 @@ gem 'recaptcha', '~> 5.9.0' gem 'rest-client' gem 'rexml', '~> 3' gem 'stackprof', require: false + +# pinning strscan to v 3.0.1 to deal with deployment issue. Remove line below when issue is fixed +gem 'strscan', '3.0.1' + gem 'terser' gem 'thin' gem 'will_paginate', '~> 3.0' +gem 'net-ftp' +gem 'flag-icons-rails', '~> 3.4' +gem 'inline_svg' group :staging, :production do # Application monitoring diff --git a/Gemfile.lock b/Gemfile.lock index bcd80c4489..a577448127 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,29 +82,33 @@ GEM i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) - airbrussh (1.5.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + airbrussh (1.5.3) 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.1.0) - bcrypt_pbkdf (1.1.0) - bootsnap (1.17.1) + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) + bcrypt_pbkdf (1.1.1-x86_64-darwin) + bigdecimal (3.1.8) + bootsnap (1.18.4) msgpack (~> 1.2) bootstrap (5.2.3) autoprefixer-rails (>= 9.1.0) popper_js (>= 2.11.6, < 3) sassc-rails (>= 2.0.0) - brakeman (6.0.1) - builder (3.2.4) - capistrano (3.18.0) + brakeman (6.2.1) + racc + builder (3.3.0) + 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) @@ -118,11 +122,11 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.39.2) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) @@ -130,41 +134,46 @@ GEM chart-js-rails (0.1.7) railties (> 3.1) coderay (1.1.3) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.4) crass (1.0.6) cube-ruby (0.0.3) daemons (1.4.1) - dalli (3.2.6) + dalli (3.2.8) date (3.3.4) - debug (1.9.1) + debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) - diff-lcs (1.5.0) - domain_name (0.6.20231109) + diff-lcs (1.5.1) + domain_name (0.6.20240107) ed25519 (1.3.0) - erubi (1.12.0) + erubi (1.13.0) erubis (2.7.0) eventmachine (1.2.7) - excon (0.108.0) + excon (0.111.0) execjs (2.9.1) - faraday (2.7.12) - base64 - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) + faraday (2.11.0) + faraday-net_http (>= 2.0, < 3.4) + logger faraday-excon (2.1.0) excon (>= 0.27.4) faraday (~> 2.0) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (3.0.2) - ffi (1.16.3) + faraday-net_http (3.3.0) + net-http + ffi (1.17.0) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86_64-darwin) + flag-icons-rails (3.4.6.1) + sass-rails flamegraph (0.9.5) globalid (1.2.1) activesupport (>= 6.1) - graphql (2.0.27) - graphql-client (0.18.0) + graphql (2.0.31) + base64 + graphql-client (0.23.0) activesupport (>= 3.0) - graphql + graphql (>= 1.13.0) haml (5.2.2) temple (>= 0.8.0) tilt @@ -174,28 +183,32 @@ GEM nokogiri (>= 1.6.0) ruby_parser (~> 3.5) http-accept (1.7.0) - http-cookie (1.0.5) + http-cookie (1.0.7) domain_name (~> 0.5) - i18n (1.14.1) + i18n (1.14.5) concurrent-ruby (~> 1.0) + inline_svg (1.10.0) + activesupport (>= 3.0) + nokogiri (>= 1.6) io-console (0.7.2) - irb (1.12.0) - rdoc + irb (1.14.0) + rdoc (>= 4.0.0) reline (>= 0.4.2) iso-639 (0.3.6) jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (6.0.1) + 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) language_server-protocol (3.17.0.3) - listen (3.8.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.1) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -205,19 +218,24 @@ GEM net-imap net-pop net-smtp - marcel (1.0.2) + marcel (1.0.4) matrix (0.4.2) - method_source (1.0.0) - mime-types (3.5.1) + method_source (1.1.0) + mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2023.1003) + mime-types-data (3.2024.0903) mini_mime (1.1.5) - minitest (5.21.2) + minitest (5.25.1) msgpack (1.7.2) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.1) mysql2 (0.5.5) - net-imap (0.4.7) + net-ftp (0.3.7) + net-protocol + time + net-http (0.4.1) + uri + net-imap (0.4.16) date net-protocol net-pop (0.1.2) @@ -226,23 +244,27 @@ GEM timeout net-scp (4.0.0) net-ssh (>= 2.6.5, < 8.0.0) - net-smtp (0.4.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.0) net-protocol - net-ssh (7.2.0) + net-ssh (7.2.3) netrc (0.11.0) - newrelic_rpm (9.6.0) - base64 - nio4r (2.6.1) - nokogiri (1.16.0-arm64-darwin) + newrelic_rpm (9.13.0) + nio4r (2.7.3) + nokogiri (1.16.7-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.0-x86_64-darwin) + nokogiri (1.16.7-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.0-x86_64-linux) + nokogiri (1.16.7-x86_64-linux) racc (~> 1.4) - oj (3.16.1) + oj (3.16.5) + bigdecimal (>= 3.0) + ostruct (>= 0.2) open_uri_redirections (0.2.1) - parallel (1.24.0) - parser (3.3.0.5) + ostruct (0.6.0) + parallel (1.26.3) + parser (3.3.5.0) ast (~> 2.4.1) racc popper_js (2.11.8) @@ -250,10 +272,10 @@ GEM coderay (~> 1.1) method_source (~> 1.0) psych (3.3.4) - public_suffix (5.0.4) - racc (1.7.3) - rack (2.2.8) - rack-mini-profiler (3.1.1) + public_suffix (6.0.1) + racc (1.8.1) + rack (2.2.9) + rack-mini-profiler (3.3.1) rack (>= 1.2.0) rack-test (2.1.0) rack (>= 1.3) @@ -292,55 +314,56 @@ GEM rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) rdoc (6.3.4.1) recaptcha (5.9.0) json redis (4.8.1) - regexp_parser (2.9.0) - reline (0.5.0) + regexp_parser (2.9.2) + reline (0.5.9) 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.2.6) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + rexml (3.3.7) + rspec-core (3.13.1) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-rails (6.1.0) - actionpack (>= 6.1) - activesupport (>= 6.1) - railties (>= 6.1) - rspec-core (~> 3.12) - rspec-expectations (~> 3.12) - rspec-mocks (~> 3.12) - rspec-support (~> 3.12) - rspec-support (3.12.1) - rubocop (1.63.3) + rspec-support (~> 3.13.0) + 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.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 (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.4, < 3.0) + rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.2) - parser (>= 3.3.0.4) + rubocop-ast (1.32.3) + parser (>= 3.3.1.0) ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) - ruby_parser (3.20.3) + ruby_parser (3.21.1) + racc (~> 1.5) sexp_processor (~> 4.16) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) ffi (~> 1.9) sassc-rails (2.1.2) @@ -354,40 +377,46 @@ GEM sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) sprockets (>= 3.0.0) - sshkit (1.21.6) + sshkit (1.23.0) + base64 net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.25) - stimulus-rails (1.3.3) + stackprof (0.2.26) + stimulus-rails (1.3.4) railties (>= 6.0.0) + strscan (3.0.1) temple (0.10.3) - terser (1.1.20) + terser (1.2.3) execjs (>= 0.3.0, < 3) thin (1.8.2) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) - thor (1.3.0) - tilt (2.3.0) + thor (1.3.2) + tilt (2.4.0) + time (0.3.0) + date timeout (0.4.1) - turbo-rails (1.5.0) + turbo-rails (2.0.6) 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) + uri (0.13.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) will_paginate (3.3.1) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.12) + zeitwerk (2.6.18) PLATFORMS arm64-darwin-22 @@ -414,12 +443,14 @@ DEPENDENCIES dalli debug ed25519 (>= 1.2, < 2.0) + flag-icons-rails (~> 3.4) flamegraph graphql (~> 2.0.27) graphql-client haml (~> 5.1) html2haml i18n + inline_svg iso-639 (~> 0.3.6) jquery-rails jquery-ui-rails @@ -427,6 +458,7 @@ DEPENDENCIES listen multi_json mysql2 (= 0.5.5) + net-ftp newrelic_rpm oj ontologies_api_client! @@ -448,6 +480,7 @@ DEPENDENCIES sprockets-rails stackprof stimulus-rails + strscan (= 3.0.1) terser thin turbo-rails @@ -455,4 +488,4 @@ DEPENDENCIES will_paginate (~> 3.0) BUNDLED WITH - 2.4.19 + 2.5.11 diff --git a/app/assets/javascripts/bp_class_tree.js.erb b/app/assets/javascripts/bp_class_tree.js.erb index e3cbbc8c95..74c7e9f622 100644 --- a/app/assets/javascripts/bp_class_tree.js.erb +++ b/app/assets/javascripts/bp_class_tree.js.erb @@ -86,7 +86,7 @@ function initClassTree() { }); }; -function nodeClicked(node_id) { +function nodeClicked(node_id, lang = "en") { // Get current html and store data in cache (to account for changes since the cache was retrieved) setCacheCurrent(); @@ -128,12 +128,12 @@ function nodeClicked(node_id) { // Insert notes table insertNotesTable(tabData["notes_table_data"]); - wrapupTabChange(selectedTab); } else { jQuery.blockUI({ message: '

' + '<%= image_tag src="jquery.simple.tree/spinner.gif" %>' + ' Loading Class...

', showOverlay: false }); - jQuery.get('/ajax_concepts/'+jQuery(document).data().bp.ont_viewer.ontology_id+'/?conceptid='+node_id+'&callback=load', - function(data){ + var ajaxUrl = '/ajax_concepts/'+jQuery(document).data().bp.ont_viewer.ontology_id+'/?conceptid='+node_id+'&lang=' + lang + '&callback=load' + jQuery.get(ajaxUrl, + function(data) { var tabData = data.split("|||"); // the tabs @@ -181,14 +181,16 @@ function placeTreeView(treeHTML) { } // Retrieve the tree view using ajax -function getTreeView() { +function getTreeView(lang = "en") { + var url = "/ajax/classes/treeview?ontology="+jQuery(document).data().bp.ont_viewer.ontology_id+"&lang=" + lang + "&conceptid="+encodeURIComponent(jQuery(document).data().bp.ont_viewer.concept_id); jQuery.ajax({ - url: "/ajax/classes/treeview?ontology="+jQuery(document).data().bp.ont_viewer.ontology_id+"&conceptid="+encodeURIComponent(jQuery(document).data().bp.ont_viewer.concept_id), + url: url, success: function(data) { placeTreeView(data); }, error: function(data) { - jQuery.get("/ajax/classes/treeview?ontology="+jQuery(document).data().bp.ont_viewer.ontology_id+"&conceptid=root", function(data){ + var url = "/ajax/classes/treeview?ontology="+jQuery(document).data().bp.ont_viewer.ontology_id+"&lang=" + lang + "&conceptid=root"; + jQuery.get(url, function(data) { var rootTree = "
Displaying the path to this class has taken too long. You can browse classes below.
" + data; placeTreeView(rootTree); }); @@ -201,6 +203,7 @@ function getTreeView() { // We do this right after writing #sd_content to the dom to make sure it loads before other async portions of the page jQuery(document).ready(function(){ if (pageUsesTreeView()) { - getTreeView(); + var lang = jQuery(document).data().bp.ont_viewer.lang; + getTreeView(lang); } }); diff --git a/app/assets/javascripts/bp_ontology_viewer.js.erb b/app/assets/javascripts/bp_ontology_viewer.js.erb index e595cf3c23..276cfa27d5 100644 --- a/app/assets/javascripts/bp_ontology_viewer.js.erb +++ b/app/assets/javascripts/bp_ontology_viewer.js.erb @@ -82,18 +82,16 @@ function displayTree(data) { var metadata_only = jQuery(document).data().bp.ont_viewer.metadata_only; var purl_prefix = jQuery(document).data().bp.ont_viewer.purl_prefix; var concept_name_title = jQuery(document).data().bp.ont_viewer.concept_name_title; + var lang = jQuery(document).data().bp.ont_viewer.lang; // Check to see if we're actually loading a new concept or just displaying the one we already loaded previously if (typeof new_concept_id === 'undefined' || new_concept_id == concept_id) { - if (concept_id !== "") { History.replaceState({p:"classes", conceptid:concept_id}, jQuery.bioportal.ont_pages["classes"].page_name + " | " + org_site, "?p=classes" + "&conceptid=" + concept_id); } jQuery.unblockUI(); return; - } else { - var new_concept_param = (typeof new_concept_id === 'undefined') ? "" : "&conceptid=" + new_concept_id; if (typeof new_concept_id !== 'undefined') { @@ -137,7 +135,7 @@ function displayTree(data) { simpleTreeCollection.get(0).setTreeNodes(list); // Simulate node click - nodeClicked(data.conceptid); + nodeClicked(data.conceptid, lang); // Make "clicked" node active jQuery("a.active").removeClass("active"); @@ -146,7 +144,7 @@ function displayTree(data) { // Clear the search box jQuery("#search_box").val(""); } else { - nodeClicked(data.conceptid); + nodeClicked(data.conceptid, lang); // Clear the search box jQuery("#search_box").val(""); @@ -161,7 +159,7 @@ function displayTree(data) { if (document.getElementById(new_concept_id) !== null) { // We have a visible node that's been clicked, get the details for that node jQuery.bioportal.ont_pages["classes"].published = true; - nodeClicked(new_concept_id); + nodeClicked(new_concept_id, lang); } else { // Get a new copy of the tree because our concept isn't visible // This could be due to using the forward/back button @@ -201,6 +199,12 @@ function showOntologyContent(content_section) { // Instead, fire some history events var nav_ont = function(link) { var page = jQuery(link).attr("data-bp-ont-page"); + + if (jQuery(document).data().bp.ont_viewer.lang_sections.includes(page)) { + jQuery("#navbar-ontology li.lang-dropdown").show(); + } else { + jQuery("#navbar-ontology li.lang-dropdown").hide(); + } History.pushState({p:page}, jQuery.bioportal.ont_pages[page].page_name + " | " + jQuery(document).data().bp.ont_viewer.org_site, "?p=" + page); } diff --git a/app/assets/javascripts/bp_visualize.js.erb b/app/assets/javascripts/bp_visualize.js.erb index 0e4b255426..4e5740ded7 100644 --- a/app/assets/javascripts/bp_visualize.js.erb +++ b/app/assets/javascripts/bp_visualize.js.erb @@ -98,7 +98,10 @@ jQuery(document).data().bp.classesTab.search_box_init = function(){ if (jQuery("#search_box").bioportal_autocomplete) { jQuery("#search_box").bioportal_autocomplete("/search/json_search/"+jQuery(document).data().bp.ontology.acronym, { - extraParams: { objecttypes: "class" }, + extraParams: { + objecttypes: "class", + lang: jQuery(document).data().bp.ont_viewer.lang + }, selectFirst: true, lineSeparator: "~!~", matchSubset: 0, diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 6686c3dace..3647b07fa4 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -19,6 +19,7 @@ *= require thickbox *= require select2 *= require trumbowyg + *= require flag-icon * */ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 488cbda5a1..06cd2e3024 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -243,7 +243,8 @@ def redirect_new_api(class_view = false) not_found unless acronym if class_view @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(acronym).first - concept = get_class(params).first.to_s + @submission = get_ontology_submission_ready(@ontology) + concept = get_class(params, @submission).first.to_s redirect_to "/ontologies/#{acronym}?p=classes#{params_string_for_redirect(params, prefix: "&")}", :status => :moved_permanently else redirect_to "/ontologies/#{acronym}#{params_string_for_redirect(params)}", :status => :moved_permanently @@ -349,10 +350,10 @@ def using_captcha? ENV['USE_RECAPTCHA'].present? && ENV['USE_RECAPTCHA'] == 'true' end - def get_class(params) + def get_class(params, submission) + lang = helpers.request_lang(submission) if @ontology.flat? - ignore_concept_param = params[:conceptid].nil? || params[:conceptid].empty? || params[:conceptid].eql?("root") || @@ -367,34 +368,37 @@ def get_class(params) @concept.children = [] else # Display only the requested class in the tree - @concept = @ontology.explore.single_class({full: true}, params[:conceptid]) + @concept = @ontology.explore.single_class({full: true, lang: lang}, params[:conceptid]) @concept.children = [] end @root = LinkedData::Client::Models::Class.new @root.children = [@concept] - else - # not ignoring 'bp_fake_root' here ignore_concept_param = params[:conceptid].nil? || params[:conceptid].empty? || params[:conceptid].eql?("root") + if ignore_concept_param # get the top level nodes for the root # TODO_REV: Support views? Replace old view call: @ontology.top_level_classes(view) - roots = @ontology.explore.roots + roots = @ontology.explore.roots(lang: lang) + if roots.nil? || roots.empty? LOG.add :debug, "Missing roots for #{@ontology.acronym}" not_found end @root = LinkedData::Client::Models::Class.new(read_only: true) - @root.children = roots.sort{|x,y| (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase} + @root.children = roots.sort{|x,y| + x.prefLabel = helpers.link_last_part(x.id) if x.prefLabel.to_s.empty? + y.prefLabel = helpers.link_last_part(y.id) if y.prefLabel.to_s.empty? + (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase} # get the initial concept to display root_child = @root.children.first + @concept = root_child.explore.self(full: true, lang: lang) - @concept = root_child.explore.self(full: true) # Some ontologies have "too many children" at their root. These will not process and are handled here. if @concept.nil? LOG.add :debug, "Missing class #{root_child.links.self}" @@ -402,20 +406,23 @@ def get_class(params) end else # if the id is coming from a param, use that to get concept - @concept = @ontology.explore.single_class({full: true}, params[:conceptid]) + @concept = @ontology.explore.single_class({full: true, lang: lang}, params[:conceptid]) if @concept.nil? || @concept.errors LOG.add :debug, "Missing class #{@ontology.acronym} / #{params[:conceptid]}" not_found end # Create the tree - rootNode = @concept.explore.tree(include: "prefLabel,hasChildren,obsolete") + rootNode = @concept.explore.tree(include: "prefLabel,hasChildren,obsolete", lang: lang) + if rootNode.nil? || rootNode.empty? - roots = @ontology.explore.roots + roots = @ontology.explore.roots(lang: lang) + if roots.nil? || roots.empty? LOG.add :debug, "Missing roots for #{@ontology.acronym}" not_found end + if roots.any? {|c| c.id == @concept.id} rootNode = roots else @@ -423,10 +430,12 @@ def get_class(params) end end @root = LinkedData::Client::Models::Class.new(read_only: true) - @root.children = rootNode.sort{|x,y| (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase} + @root.children = rootNode.sort{|x,y| + x.prefLabel = helpers.link_last_part(x.id) if x.prefLabel.to_s.empty? + y.prefLabel = helpers.link_last_part(y.id) if y.prefLabel.to_s.empty? + (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase} end end - @concept end diff --git a/app/controllers/concepts_controller.rb b/app/controllers/concepts_controller.rb index f518eed670..b945f04e60 100644 --- a/app/controllers/concepts_controller.rb +++ b/app/controllers/concepts_controller.rb @@ -16,13 +16,15 @@ def show # find_by_acronym includes views by default @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(params[:ontology]).first + @submission = get_ontology_submission_ready(@ontology) @ob_instructions = helpers.ontolobridge_instructions_template(@ontology) if request.xhr? display = params[:callback].eql?('load') ? { full: true } : { display: 'prefLabel' } + display[:language] = helpers.request_lang(@submission) @concept = @ontology.explore.single_class(display, params[:id]) not_found if @concept.nil? - show_ajax_request + show_ajax_request(@submission) else render plain: 'Non-AJAX requests are not accepted at this URL', status: :forbidden end @@ -53,7 +55,8 @@ def show_tree if @ontology.nil? not_found else - get_class(params) + @submission = get_ontology_submission_ready(@ontology) + get_class(params, @submission) render partial: 'ontologies/treeview' end end @@ -105,14 +108,18 @@ def redirect_new_api # Load data for a concept or retrieve a concept's children, depending on the value of the :callback parameter. # Children are retrieved for drawing ontology class trees. - def show_ajax_request + def show_ajax_request(submission) case params[:callback] when 'load' gather_details render partial: 'load' when 'children' - @children = @concept.explore.children(pagesize: 750).collection || [] - @children.sort! { |x, y| (x.prefLabel || '').downcase <=> (y.prefLabel || '').downcase } + @children = @concept.explore.children(pagesize: 750, language: helpers.request_lang(submission)).collection || [] + @children.sort! do |x, y| + x.prefLabel = helpers.link_last_part(x.id) if x.prefLabel.to_s.empty? + y.prefLabel = helpers.link_last_part(y.id) if y.prefLabel.to_s.empty? + (x.prefLabel || '').downcase <=> (y.prefLabel || '').downcase + end render partial: 'child_nodes' end end @@ -123,4 +130,5 @@ def gather_details @delete_mapping_permission = check_delete_mapping_permission(@mappings) update_tab(@ontology, @concept.id) end + end diff --git a/app/controllers/language_controller.rb b/app/controllers/language_controller.rb new file mode 100644 index 0000000000..0ccf6b59c5 --- /dev/null +++ b/app/controllers/language_controller.rb @@ -0,0 +1,21 @@ +class LanguageController < ApplicationController + + # set locale to the language selected by the user + def set_locale_language + language = params[:language].strip.downcase.to_sym + supported_languages = I18n.available_locales + + if language + if supported_languages.include?(language) + cookies.permanent[:locale] = language + else + # in case we want to show a message if the language is not available + flash.now[:notice] = t('language.translation_not_available', language: language) + logger.error flash.now[:notice] + end + end + + redirect_to request.referer || root_path + end + +end diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index 4c0847c5f1..aa8fadeed4 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -132,14 +132,14 @@ def index end def classes - get_class(params) + @submission = get_ontology_submission_ready(@ontology) + get_class(params, @submission) if ["application/ld+json", "application/json"].include?(request.accept) render plain: @concept.to_jsonld, content_type: request.accept and return end @current_purl = @concept.purl if Rails.configuration.settings.purl[:enabled] - @submission = get_ontology_submission_ready(@ontology) unless @concept.id == "bp_fake_root" @notes = @concept.explore.notes diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 95d08d8ff7..d8d9d028b1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -61,18 +61,19 @@ def remove_owl_notation(string) end end - def draw_tree(root, id = nil, type = "Menu") + def draw_tree(root, id = nil, type = "Menu", submission) if id.nil? id = root.children.first.id end # TODO: handle tree view for obsolete classes, e.g. 'http://purl.obolibrary.org/obo/GO_0030400' - raw build_tree(root, "", id) # returns a string, representing nested list items + raw build_tree(root, "", id, submission) # returns a string, representing nested list items end - def build_tree(node, string, id) + def build_tree(node, string, id, submission) if node.children.nil? || node.children.length < 1 return string # unchanged end + node.children.sort! {|a,b| (a.prefLabel || a.id).downcase <=> (b.prefLabel || b.id).downcase} for child in node.children if child.id.eql?(id) @@ -93,12 +94,13 @@ def build_tree(node, string, id) string << "
  • #{child.prefLabel}
  • " else icons = child.relation_icon(node) - string << "
  • #{child.prefLabel({use_html: true})} #{icons}" + string << "
  • #{child.prefLabel({use_html: true})} #{icons}" + if child.hasChildren && !child.expanded? - string << "" + string << "" elsif child.expanded? string << "" end string << "
  • " @@ -234,6 +236,16 @@ def at_slice? !@subdomain_filter.nil? && !@subdomain_filter[:active].nil? && @subdomain_filter[:active] == true end + def link_last_part(url) + return "" if url.nil? + + if url.include?('#') + url.split('#').last + else + url.split('/').last + end + end + def truncate_with_more(text, options = {}) length ||= options[:length] ||= 30 trailing_text ||= options[:trailing_text] ||= " ... " @@ -362,5 +374,4 @@ def get_link_for_ont_ajax(ont_acronym) end ###END ruby equivalent of JS code in bp_ajax_controller. - end diff --git a/app/helpers/internationalisation_helper.rb b/app/helpers/internationalisation_helper.rb new file mode 100644 index 0000000000..6bda49f76d --- /dev/null +++ b/app/helpers/internationalisation_helper.rb @@ -0,0 +1,43 @@ +module InternationalisationHelper + + # Implement logic to make the term 'ontology' configurable throughout the portal, + # allowing it to be replaced with the variable $RESOURCE_TERM + def self.t(*args, **kwargs) + return I18n.t(*args, **kwargs) unless $RESOURCE_TERM + + begin + original_translation = I18n.t(*args, **kwargs) + downcase_translation = original_translation.downcase + rescue StandardError => e + return e.message + end + + term = I18n.t("resource_term.ontology") + plural_term = I18n.t("resource_term.ontology_plural") + single_term = I18n.t("resource_term.ontology_single") + resource = I18n.t("resource_term.#{$RESOURCE_TERM}") + resources = I18n.t("resource_term.#{$RESOURCE_TERM}_plural") + a_resource = I18n.t("resource_term.#{$RESOURCE_TERM}_single") + + if downcase_translation.include?(term) && resource + replacement = resource.capitalize + replacement = resource if downcase_translation.include?(term) + if downcase_translation.include?(single_term) + term = single_term + replacement = a_resource + end + original_translation.gsub(term, replacement) + elsif downcase_translation.include?(plural_term) && resources + replacement = resources.capitalize + replacement = resources if downcase_translation.include?(plural_term) + original_translation.gsub(plural_term, replacement) + else + I18n.t(*args, **kwargs) + end + end + + # def t(*args, **kwargs) + # InternationalisationHelper.t(*args, **kwargs) + # end + +end diff --git a/app/helpers/multi_languages_helper.rb b/app/helpers/multi_languages_helper.rb new file mode 100644 index 0000000000..d12d5dd881 --- /dev/null +++ b/app/helpers/multi_languages_helper.rb @@ -0,0 +1,146 @@ +module MultiLanguagesHelper + + def portal_language_help_text + t('language.portal_language_help_text') + end + + def portal_languages + { + en: { badge: nil, disabled: false }, + fr: { badge: 'beta', disabled: false }, + it: { badge: 'coming', disabled: true }, + de: { badge: 'coming', disabled: true } + } + end + + def portal_language_selector + languages = portal_languages + selected_language = portal_lang + selected_language = content_tag(:span, selected_language.upcase, data: { controller: 'tooltip' }, title: portal_language_help_text) + render DropdownButtonComponent.new do |d| + d.header { selected_language } + d.section(divide: false, selected_index: languages.find_index(selected_language)) do |s| + languages.each do |lang, metadata| + s.item do + text = content_tag(:div, class: 'd-flex align-items-center') do + content_tag(:span, render(LanguageFieldComponent.new(value: lang, auto_label: true)), class: 'mr-1') + beta_badge(metadata[:badge]) + end + link_options = { data: { turbo: false } } + + if metadata[:disabled] + link_options[:class] = 'disabled-link' + link_options[:disabled] = 'disabled' + end + + link_to(text, "/locale/#{lang}", link_options) + end + end + + end + end + end + + def search_language_help_text + content_tag(:div, style: 'width: 300px; text-align: center') do + t('language.search_language_help_text') + end + end + + def search_languages + # top ten spoken languages + portal_languages.keys + %w[zh es hi ar bn pt ru ur id] + end + + def language_hash(concept_label, multiple: false) + if concept_label.is_a?(Array) + return concept_label.first unless multiple + return concept_label + end + + return concept_label.to_h.reject { |key, _| %i[links context].include?(key) } if concept_label.is_a?(OpenStruct) + + concept_label + end + + def sorted_labels(labels) + Array(labels).sort_by { |label| label['prefLabel'].is_a?(String) ? label['prefLabel'] : label['prefLabel'].last } + end + + def select_language_label(concept_label, platform_languages = %i[en fr]) + concept_value = nil + + concept = language_hash(concept_label) + + return ['@none', concept] if concept.is_a?(String) + + concept = concept.to_h + + platform_languages.each do |lang| + if concept[lang] + concept_value = [lang, concept[lang]] + break + end + end + + concept_value || concept.to_a.first + end + + def main_language_label(label) + select_language_label(label)&.last + end + + def selected_language_label(label) + language_hash(label).values.first + end + + def content_language_selector(id: 'content_language', name: 'content_language') + languages, selected = content_languages + select_tag(name, options_for_select(languages, selected || 'all'), class: "form-select", + data: { controller: "language-change", 'language-change-section-value': "classes", action: "change->language-change#dispatchLangChangeEvent" }) if languages&.length > 1 + end + + def content_languages(submission = @submission || @submission_latest) + current_lang = request_lang(submission).downcase + submission_lang = submission_languages(submission) + # 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.english_name, lang.alpha2] + end.compact + + [submission_lang, current_lang] + end + + def portal_lang + session[:locale] || 'en' + end + + def request_lang(submission = @submission || @submission_latest) + lang = params[:language] || params[:lang] + lang = submission_languages(submission)&.first unless lang + lang = portal_lang unless lang + lang + end + + def lang_code(code_in) + code_out = code_in + case code_in + when 'en' + code_out = 'us' + when 'ar' + code_out = 'sa' + when 'hi' + code_out = 'in' + when 'ur' + code_out = 'pk' + when 'zh' + code_out = 'cn' + when 'ja' + code_out = 'jp' + end + code_out + end + +end diff --git a/app/helpers/ontologies_helper.rb b/app/helpers/ontologies_helper.rb index 88ea877d94..e255fdedd4 100644 --- a/app/helpers/ontologies_helper.rb +++ b/app/helpers/ontologies_helper.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true +require 'iso-639' module OntologiesHelper + + LANGUAGE_FILTERABLE_SECTIONS = %w[classes properties].freeze + def additional_details return '' if $ADDITIONAL_ONTOLOGY_DETAILS.nil? || $ADDITIONAL_ONTOLOGY_DETAILS[@ontology.acronym].nil? @@ -158,4 +162,34 @@ def change_requests_enabled?(ontology_acronym) Rails.configuration.change_request[:ontologies].include? ontology_acronym.to_sym end + + def current_section + (params[:p]) ? params[:p] : 'summary' + end + + def ontology_data_sections + LANGUAGE_FILTERABLE_SECTIONS + end + + def ontology_data_section?(section_title = current_section) + ontology_data_sections.include?(section_title) + end + + def language_selector_tag(name) + content_language_selector(id: name, name: name) + end + + def submission_languages(submission = @submission) + Array(submission&.naturalLanguage).map { |natural_language| natural_language.split('/').last }.compact + end + + def abbreviations_to_languages(abbreviations) + # Use iso-639 gem to convert language codes to their English names + languages = abbreviations.map do |abbr| + language = ISO_639.find_by_code(abbr) || ISO_639.find_by_english_name(abbr) + language ? language.english_name : abbr + end + languages.sort + end + end diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index c4d0fd2b2e..273754277c 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -4,5 +4,9 @@ import { application } from "./application" +import LanguageChangeController from "./language_change_controller" +application.register("language-change", LanguageChangeController) + import ClipboardController from "./clipboard_controller" application.register("clipboard", ClipboardController) + diff --git a/app/javascript/controllers/language_change_controller.js b/app/javascript/controllers/language_change_controller.js new file mode 100644 index 0000000000..b1fc34aa5f --- /dev/null +++ b/app/javascript/controllers/language_change_controller.js @@ -0,0 +1,47 @@ +import { Controller } from "@hotwired/stimulus" + +// Connects to data-controller="language-change" +export default class extends Controller { + + + connect() { + // can be used for debugging + // console.log(this.element.value); + jQuery(document).data().bp.ont_viewer.lang = this.element.value; + } + + dispatchLangChangeEvent() { + jQuery(document).data().bp.ont_viewer.lang = this.element.value; + + var url = window.location.href; + url = this.removeURLParameter(url, 'lang'); + if (url.indexOf('?') > -1) { + url += '&lang=' + this.element.value; + } else { + url += '?lang=' + this.element.value; + } + jQuery.blockUI({ message: '

    Switching language...

    ', showOverlay: true }); + window.location.href = url; + } + + removeURLParameter(url, parameter) { + //prefer to use l.search if you have a location/link object + var urlparts = url.split('?'); + + if (urlparts.length >= 2) { + var prefix = encodeURIComponent(parameter) + '='; + var pars = urlparts[1].split(/[&;]/g); + + //reverse iteration as may be destructive + for (var i = pars.length; i-- > 0;) { + //idiom for string.startsWith + if (pars[i].lastIndexOf(prefix, 0) !== -1) { + pars.splice(i, 1); + } + } + return urlparts[0] + (pars.length > 0 ? '?' + pars.join('&') : ''); + } + return url; + } + +} diff --git a/app/views/concepts/_child_nodes.html.haml b/app/views/concepts/_child_nodes.html.haml index 79107a3cb3..b734cc5944 100644 --- a/app/views/concepts/_child_nodes.html.haml +++ b/app/views/concepts/_child_nodes.html.haml @@ -1,8 +1,9 @@ - output ="" - for child in @children - icons = child.relation_icon(@concept) - - output << "
  • #{child.prefLabel} #{icons}" + - output << "
  • #{child.prefLabel} #{icons}" + - if child.hasChildren - - output << "" + - output << "" - output << "
  • " = raw output diff --git a/app/views/concepts/_details.html.haml b/app/views/concepts/_details.html.haml index 4bc9657688..cb0ff44494 100644 --- a/app/views/concepts/_details.html.haml +++ b/app/views/concepts/_details.html.haml @@ -25,7 +25,11 @@ %tr %td Preferred Name %td - %p= @concept.prefLabel({:use_html => true}).html_safe + %p + - if @concept.prefLabel({:use_html => false}).to_s.empty? + %div.alert.alert-warning= 'No preferred name provided for selected language' + - else + = @concept.prefLabel({:use_html => true}).html_safe %td %tr %td Synonyms diff --git a/app/views/concepts/_request_term.html.haml b/app/views/concepts/_request_term.html.haml index 7185c38a35..7c3bed4a93 100644 --- a/app/views/concepts/_request_term.html.haml +++ b/app/views/concepts/_request_term.html.haml @@ -36,11 +36,7 @@ var g_prefLabel; jQuery(document).ready(function() { - g_prefLabel = '#{@concept.prefLabel({:use_html => false}).html_safe}'; - - if (!g_prefLabel) { - g_prefLabel = '#{@concept.id}'; - } + g_prefLabel = '#{@concept.prefLabel({:use_html => false}).to_s.empty? ? @concept.id : @concept.prefLabel({:use_html => false}).html_safe}'; var isPopulated = window.localStorage.getItem('request_term_form_populated'); if (isPopulated) { diff --git a/app/views/layouts/_ontology_viewer.html.haml b/app/views/layouts/_ontology_viewer.html.haml index 64f4899d0c..0370abc0a0 100644 --- a/app/views/layouts/_ontology_viewer.html.haml +++ b/app/views/layouts/_ontology_viewer.html.haml @@ -36,6 +36,8 @@ jQuery(document).data().bp.ont_viewer.purl_prefix = "#{(Rails.configuration.settings.purl[:enabled] ? Rails.configuration.settings.purl[:prefix]+"/"+@ontology.acronym : '')}"; jQuery(document).data().bp.ont_viewer.concept_name_title = (jQuery(document).data().bp.ont_viewer.concept_name == "") ? "" : " - " + jQuery(document).data().bp.ont_viewer.concept_name; + jQuery(document).data().bp.ont_viewer.lang = "#{request_lang(sub)}"; + jQuery(document).data().bp.ont_viewer.lang_sections = #{ontology_data_sections.to_json.html_safe}; -# Modal dialog for creating a new mapping (must reside in a top-level position in the document to display properly). %div#createNewMappingModal{class: "modal fade", tabindex: "-1", "aria-labelledby": "createNewMappingLabel"} @@ -139,6 +141,8 @@ class: "nav-link", type: 'button', role: "tab", data: {bp_ont_page: "widgets", "bs-toggle": "tab", "bs-target": "#ont_widgets_content"}, aria: {controls: "ont_widgets_content", selected: "false"}) + %li{style: "display: #{ontology_data_section? ? 'block' : 'none'}", class: 'nav-item lang-dropdown', role: 'presentation'} + = language_selector_tag(:content_language) %div.card-body -# Tab panes for displaying ontology content sections %div.tab-content{id: "ontology_content"} @@ -160,7 +164,6 @@ %div.tab-pane{id: "ont_widgets_content", role: "tabpanel", aria: {labelledby: "ont-widgets-tab"}} - if content_section.eql?("widgets") = yield - - if Rails.env.appliance? = render partial: "footer_appliance" diff --git a/app/views/ontologies/_metadata.html.haml b/app/views/ontologies/_metadata.html.haml index aa5c44a6dc..df5be9e159 100644 --- a/app/views/ontologies/_metadata.html.haml +++ b/app/views/ontologies/_metadata.html.haml @@ -49,6 +49,9 @@ %tr %td Groups %td= groups.map {|g| groups_hash[g].name}.sort.join(", ") + %tr + %td Language + %td= abbreviations_to_languages(@submission_latest.naturalLanguage).join(", ") = raw additional_details -# Submissions pane diff --git a/app/views/ontologies/_treeview.html.haml b/app/views/ontologies/_treeview.html.haml index 3640b54510..20db396a8b 100644 --- a/app/views/ontologies/_treeview.html.haml +++ b/app/views/ontologies/_treeview.html.haml @@ -2,4 +2,4 @@ %ul.simpleTree %li.root %ul - = draw_tree(@root, @concept.id) # application_helper::draw_tree + = draw_tree(@root, @concept.id, @submission) # application_helper::draw_tree diff --git a/config/locales/en.yml b/config/locales/en.yml index 8562532e75..e3187b171e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,4 @@ en: - activerecord: errors: models: @@ -48,7 +47,7 @@ en: find_ontology_placeholder: Start typing ontology name, then choose from list query_placeholder: Enter a class, e.g. Melanoma tagline: the world's most comprehensive repository of biomedical ontologies - title: Welcome to the %{organization} + title: Welcome to the %{organization} welcome: Welcome to %{site}, help: welcome: Welcome to the National Center for Biomedical Ontology’s %{site}. %{site} is a web-based application for accessing and sharing biomedical ontologies. diff --git a/db/schema.rb b/db/schema.rb index fbc53cdaba..5d5db5ddc7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -28,20 +28,6 @@ t.datetime "updated_at", precision: nil, null: false end - create_table "margin_notes", id: :integer, charset: "utf8", force: :cascade do |t| - t.integer "parent_id" - t.integer "mapping_id" - t.integer "note_type" - t.integer "user_id" - t.integer "ontology_id" - t.integer "ontology_version_id" - t.string "concept_id" - t.string "subject" - t.text "comment" - t.datetime "created_at", precision: nil - t.datetime "updated_at", precision: nil - end - create_table "ontologies", charset: "utf8", force: :cascade do |t| t.string "acronym", null: false t.text "new_term_instructions" diff --git a/yarn.lock b/yarn.lock index 6da874dbfd..7da15e91d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,120 +2,120 @@ # yarn lockfile v1 -"@esbuild/aix-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz#509621cca4e67caf0d18561a0c56f8b70237472f" - integrity sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw== - -"@esbuild/android-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz#109a6fdc4a2783fc26193d2687827045d8fef5ab" - integrity sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q== - -"@esbuild/android-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.0.tgz#1397a2c54c476c4799f9b9073550ede496c94ba5" - integrity sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g== - -"@esbuild/android-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.0.tgz#2b615abefb50dc0a70ac313971102f4ce2fdb3ca" - integrity sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ== - -"@esbuild/darwin-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz#5c122ed799eb0c35b9d571097f77254964c276a2" - integrity sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ== - -"@esbuild/darwin-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz#9561d277002ba8caf1524f209de2b22e93d170c1" - integrity sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw== - -"@esbuild/freebsd-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz#84178986a3138e8500d17cc380044868176dd821" - integrity sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ== - -"@esbuild/freebsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz#3f9ce53344af2f08d178551cd475629147324a83" - integrity sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ== - -"@esbuild/linux-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz#24efa685515689df4ecbc13031fa0a9dda910a11" - integrity sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw== - -"@esbuild/linux-arm@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz#6b586a488e02e9b073a75a957f2952b3b6e87b4c" - integrity sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg== - -"@esbuild/linux-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz#84ce7864f762708dcebc1b123898a397dea13624" - integrity sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w== - -"@esbuild/linux-loong64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz#1922f571f4cae1958e3ad29439c563f7d4fd9037" - integrity sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw== - -"@esbuild/linux-mips64el@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz#7ca1bd9df3f874d18dbf46af009aebdb881188fe" - integrity sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ== - -"@esbuild/linux-ppc64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz#8f95baf05f9486343bceeb683703875d698708a4" - integrity sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw== - -"@esbuild/linux-riscv64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz#ca63b921d5fe315e28610deb0c195e79b1a262ca" - integrity sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA== - -"@esbuild/linux-s390x@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz#cb3d069f47dc202f785c997175f2307531371ef8" - integrity sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ== - -"@esbuild/linux-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz#ac617e0dc14e9758d3d7efd70288c14122557dc7" - integrity sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg== - -"@esbuild/netbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz#6cc778567f1513da6e08060e0aeb41f82eb0f53c" - integrity sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ== - -"@esbuild/openbsd-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz#76848bcf76b4372574fb4d06cd0ed1fb29ec0fbe" - integrity sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA== - -"@esbuild/sunos-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz#ea4cd0639bf294ad51bc08ffbb2dac297e9b4706" - integrity sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g== - -"@esbuild/win32-arm64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz#a5c171e4a7f7e4e8be0e9947a65812c1535a7cf0" - integrity sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ== - -"@esbuild/win32-ia32@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz#f8ac5650c412d33ea62d7551e0caf82da52b7f85" - integrity sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg== - -"@esbuild/win32-x64@0.20.0": - version "0.20.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz#2efddf82828aac85e64cef62482af61c29561bee" - integrity sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg== +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== "@hotwired/stimulus@^3.2.2": version "3.2.2" @@ -123,17 +123,17 @@ integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A== "@hotwired/turbo-rails@^8.0.3": - version "8.0.3" - resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-8.0.3.tgz#e60375f4eea4b30ec0cd6d7e3fdb3d6349a2b57b" - integrity sha512-n5B9HdFsNiGJfXFAriCArmvFZyznIh/OriB5ZVAWz4Fsi4oLkpgmJNw5pibBAM7NMQQGN6cfKa/nhZT4LWcqbQ== + version "8.0.4" + resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-8.0.4.tgz#d224f524a9e33fe687cec5d706054eb6fe13fa5b" + integrity sha512-GHCv5+B2VzYZZvMFpg/g9JLx/8pl/8chcubSB7T+Xn1zYOMqAKB6cT80vvWUzxdwfm/2KfaRysfDz+BmvtjFaw== dependencies: - "@hotwired/turbo" "^8.0.3" + "@hotwired/turbo" "^8.0.4" "@rails/actioncable" "^7.0" -"@hotwired/turbo@^8.0.3": - version "8.0.3" - resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.3.tgz#338e07278f4b3c76921328d3c92dbc4831c209d0" - integrity sha512-qLgp7d6JaegKjMToTJahosrFxV3odfSbiekispQ3soOzE5jnU+iEMWlRvYRe/jvy5Q+JWoywtf9j3RD4ikVjIg== +"@hotwired/turbo@^8.0.4": + version "8.0.4" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-8.0.4.tgz#5c5361c06a37cdf10dcba4223f1afd0ca1c75091" + integrity sha512-mlZEFUZrJnpfj+g/XeCWWuokvQyN68WvM78JM+0jfSFc98wegm259vCbC1zSllcspRwbgXK31ibehCy5PA78/Q== "@rails/actioncable@^7.0": version "7.1.3" @@ -141,30 +141,30 @@ integrity sha512-ojNvnoZtPN0pYvVFtlO7dyEN9Oml1B6IDM+whGKVak69MMYW99lC2NOWXWeE3bmwEydbP/nn6ERcpfjHVjYQjA== esbuild@^0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.0.tgz#a7170b63447286cd2ff1f01579f09970e6965da4" - integrity sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA== + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== optionalDependencies: - "@esbuild/aix-ppc64" "0.20.0" - "@esbuild/android-arm" "0.20.0" - "@esbuild/android-arm64" "0.20.0" - "@esbuild/android-x64" "0.20.0" - "@esbuild/darwin-arm64" "0.20.0" - "@esbuild/darwin-x64" "0.20.0" - "@esbuild/freebsd-arm64" "0.20.0" - "@esbuild/freebsd-x64" "0.20.0" - "@esbuild/linux-arm" "0.20.0" - "@esbuild/linux-arm64" "0.20.0" - "@esbuild/linux-ia32" "0.20.0" - "@esbuild/linux-loong64" "0.20.0" - "@esbuild/linux-mips64el" "0.20.0" - "@esbuild/linux-ppc64" "0.20.0" - "@esbuild/linux-riscv64" "0.20.0" - "@esbuild/linux-s390x" "0.20.0" - "@esbuild/linux-x64" "0.20.0" - "@esbuild/netbsd-x64" "0.20.0" - "@esbuild/openbsd-x64" "0.20.0" - "@esbuild/sunos-x64" "0.20.0" - "@esbuild/win32-arm64" "0.20.0" - "@esbuild/win32-ia32" "0.20.0" - "@esbuild/win32-x64" "0.20.0" + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2"