From 719e45604eb6a303c5ca0fae9b34753a34f19ade Mon Sep 17 00:00:00 2001 From: Bilelkihal <61744974+Bilelkihal@users.noreply.github.com> Date: Mon, 27 Mar 2023 16:23:13 +0200 Subject: [PATCH] Add the filtering logic of the browse page --- Gemfile.lock | 9 +- app/controllers/ontologies_controller.rb | 375 ++++++++++++++---- .../controllers/browse_filters_controller.js | 65 +++ app/javascript/controllers/index.js | 6 + .../controllers/turbo_frame_controller.js | 11 +- app/javascript/mixins/useHistory.js | 43 +- package.json | 1 + yarn.lock | 5 + 8 files changed, 407 insertions(+), 108 deletions(-) create mode 100644 app/javascript/controllers/browse_filters_controller.js diff --git a/Gemfile.lock b/Gemfile.lock index 2c9f387e8..10925e47f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -210,8 +210,9 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2023.0218.1) mini_mime (1.1.2) - minitest (5.18.0) - msgpack (1.6.1) + mini_portile2 (2.8.1) + minitest (5.17.0) + msgpack (1.6.0) multi_json (1.15.0) multipart-post (2.3.0) mysql2 (0.5.3) @@ -235,6 +236,9 @@ GEM netrc (0.11.0) newrelic_rpm (9.0.0) nio4r (2.5.8) + nokogiri (1.14.2) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) nokogiri (1.14.2-x86_64-linux) racc (~> 1.4) oj (3.14.2) @@ -418,6 +422,7 @@ GEM zeitwerk (2.6.7) PLATFORMS + ruby x86_64-linux DEPENDENCIES diff --git a/app/controllers/ontologies_controller.rb b/app/controllers/ontologies_controller.rb index f37a0e2a3..e8b6d3bff 100644 --- a/app/controllers/ontologies_controller.rb +++ b/app/controllers/ontologies_controller.rb @@ -14,51 +14,44 @@ class OntologiesController < ApplicationController helper :concepts helper :fair_score - layout :determine_layout + layout 'ontology' - before_action :authorize_and_redirect, :only=>[:edit,:update,:create,:new] + before_action :authorize_and_redirect, :only => [:edit, :update, :create, :new] before_action :submission_metadata, only: [:show] - KNOWN_PAGES = Set.new(["terms", "classes", "mappings", "notes", "widgets", "summary", "properties" ,"instances", "schemes", "collections"]) + KNOWN_PAGES = Set.new(["terms", "classes", "mappings", "notes", "widgets", "summary", "properties", "instances", "schemes", "collections"]) EXTERNAL_MAPPINGS_GRAPH = "http://data.bioontology.org/metadata/ExternalMappings" INTERPORTAL_MAPPINGS_GRAPH = "http://data.bioontology.org/metadata/InterportalMappings" - # GET /ontologies def index - @app_name = 'FacetedBrowsing' - @app_dir = '/browse' - @base_path = @app_dir - ontologies = LinkedData::Client::Models::Ontology.all(include: LinkedData::Client::Models::Ontology.include_params + ',viewOf', include_views: true, display_context: false) - ontologies_hash = Hash[ontologies.map {|o| [o.id, o] }] + @categories = LinkedData::Client::Models::Category.all(display_links: false, display_context: false) + @groups = LinkedData::Client::Models::Group.all(display_links: false, display_context: false) + @filters = ontology_filters_init(@categories, @groups) + render 'browse' + end + + def ontologies_filter + ontologies = LinkedData::Client::Models::Ontology.all( + include: LinkedData::Client::Models::Ontology.include_params + ',viewOf', include_views: true, display_context: false) + ontologies_hash = Hash[ontologies.map { |o| [o.id, o] }] @admin = session[:user] ? session[:user].admin? : false - @development = Rails.env.development? - # We could get naturalLanguages, isOfType and formalityLevels from the API, but for performance we are storing it in config/bioportal_config_production.rb - #@metadata = submission_metadata - # The attributes used when retrieving the submission. We are not retrieving all attributes to be faster - browse_attributes = 'ontology,acronym,submissionStatus,description,pullLocation,creationDate,released,name,naturalLanguage,hasOntologyLanguage,hasFormalityLevel,isOfType,contact' - submissions = LinkedData::Client::Models::OntologySubmission.all(include_views: true, display_links: false,display_context: false, include: browse_attributes) - submissions_map = Hash[submissions.map {|sub| [sub.ontology.acronym, sub] }] + browse_attributes = 'ontology,acronym,submissionStatus,description,pullLocation,creationDate,released,name, + naturalLanguage,hasOntologyLanguage,hasFormalityLevel,isOfType,contact,deprecated,status' + submissions = LinkedData::Client::Models::OntologySubmission.all(include_views: true, display_links: false, + display_context: false, include: browse_attributes) + submissions_map = Hash[submissions.map { |sub| [sub.ontology.acronym, sub] }] @categories = LinkedData::Client::Models::Category.all(display_links: false, display_context: false) - @categories_hash = Hash[@categories.map {|c| [c.id, c] }] - @groups = LinkedData::Client::Models::Group.all(display_links: false, display_context: false) - @groups_hash = Hash[@groups.map {|g| [g.id, g] }] analytics = LinkedData::Client::Analytics.last_month - @analytics = Hash[analytics.onts.map {|o| [o[:ont].to_s, o[:views]]}] + @analytics = Hash[analytics.onts.map { |o| [o[:ont].to_s, o[:views]] }] - reviews = {} - LinkedData::Client::Models::Review.all(display_links: false, display_context: false).each do |r| - reviews[r.reviewedOntology] ||= [] - reviews[r.reviewedOntology] << r - end metrics_hash = get_metrics_hash - @formats = Set.new #get fairscores of all ontologies @fair_scores = fairness_service_enabled? ? get_fair_score('all') : nil; @@ -76,30 +69,29 @@ def index o[:class_count_formatted] = number_with_delimiter(o[:class_count], delimiter: ',') o[:individual_count_formatted] = number_with_delimiter(o[:individual_count], delimiter: ',') - o[:id] = ont.id - o[:type] = ont.viewOf.nil? ? 'ontology' : 'ontology_view' - o[:show] = ont.viewOf.nil? ? true : false # show ontologies only by default - o[:reviews] = reviews[ont.id] || [] - o[:groups] = ont.group || [] - o[:categories] = ont.hasDomain || [] - o[:note_count] = ont.notes.length - o[:review_count] = ont.reviews.length - o[:project_count] = ont.projects.length - o[:private] = ont.private? - o[:popularity] = @analytics[ont.acronym] || 0 + o[:id] = ont.id + o[:type] = ont.viewOf.nil? ? 'ontology' : 'ontology_view' + o[:show] = ont.viewOf.nil? ? true : false # show ontologies only by default + o[:groups] = ont.group || [] + o[:categories] = ont.hasDomain || [] + o[:note_count] = ont.notes.length + o[:review_count] = ont.reviews.length + o[:project_count] = ont.projects.length + o[:private] = ont.private? + o[:popularity] = @analytics[ont.acronym] || 0 o[:submissionStatus] = [] - o[:administeredBy] = ont.administeredBy - o[:name] = ont.name - o[:acronym] = ont.acronym - o[:projects] = ont.projects - o[:notes] = ont.notes + o[:administeredBy] = ont.administeredBy + o[:name] = ont.name + o[:acronym] = ont.acronym + o[:projects] = ont.projects + o[:notes] = ont.notes if !@fair_scores.nil? && !@fair_scores[ont.acronym].nil? - o[:fairScore] = @fair_scores[ont.acronym]['score'] - o[:normalizedFairScore] = @fair_scores[ont.acronym]['normalizedScore'] + o[:fairScore] = @fair_scores[ont.acronym]['score'] + o[:normalizedFairScore] = @fair_scores[ont.acronym]['normalizedScore'] else - o[:fairScore] = nil - o[:normalizedFairScore] = 0 + o[:fairScore] = nil + o[:normalizedFairScore] = 0 end if o[:type].eql?('ontology_view') @@ -119,19 +111,20 @@ def index sub = submissions_map[ont.acronym] if sub - o[:submissionStatus] = sub.submissionStatus - o[:submission] = true - o[:pullLocation] = sub.pullLocation - o[:description] = sub.description - o[:creationDate] = sub.creationDate - o[:released] = sub.released - o[:naturalLanguage] = sub.naturalLanguage - o[:hasFormalityLevel] = sub.hasFormalityLevel - o[:isOfType] = sub.isOfType + o[:submissionStatus] = sub.submissionStatus + o[:deprecated] = sub.deprecated + o[:status] = sub.status + o[:submission] = true + o[:pullLocation] = sub.pullLocation + o[:description] = sub.description + o[:creationDate] = sub.creationDate + o[:released] = sub.released + o[:naturalLanguage] = sub.naturalLanguage + o[:hasFormalityLevel] = sub.hasFormalityLevel + o[:isOfType] = sub.isOfType o[:submissionStatusFormatted] = submission_status2string(sub).gsub(/\(|\)/, '') o[:format] = sub.hasOntologyLanguage - @formats << sub.hasOntologyLanguage else # Used to sort ontologies without submissions to the end when sorting on upload date o[:creationDate] = DateTime.parse('19900601') @@ -139,19 +132,20 @@ def index @ontologies << o end + + @ontologies.sort! { |a, b| b[:popularity] <=> a[:popularity] } - @ontologies.sort! {|a,b| b[:popularity] <=> a[:popularity]} - - render 'browse' + @ontologies = apply_ontology_filters(@ontologies, @categories, @groups) + render partial: "ontologies" end - + def classes @submission = get_ontology_submission_ready(@ontology) get_class(params) if @submission.hasOntologyLanguage == 'SKOS' - @schemes = get_schemes(@ontology) + @schemes = get_schemes(@ontology) @collections = get_collections(@ontology, add_colors: true) else @instance_details, type = get_instance_and_type(params[:instanceid]) @@ -161,7 +155,6 @@ def classes @instances_concept_id = get_concept_id(params, @concept, @root) end - if ['application/ld+json', 'application/json'].include?(request.accept) render plain: @concept.to_jsonld, content_type: request.accept and return end @@ -218,8 +211,8 @@ def edit redirect_to_home unless session[:user] && @ontology.administeredBy.include?(session[:user].id) || session[:user].admin? @categories = LinkedData::Client::Models::Category.all @groups = LinkedData::Client::Models::Group.all - @user_select_list = LinkedData::Client::Models::User.all.map {|u| [u.username, u.id]} - @user_select_list.sort! {|a,b| a[1].downcase <=> b[1].downcase} + @user_select_list = LinkedData::Client::Models::User.all.map { |u| [u.username, u.id] } + @user_select_list.sort! { |a, b| a[1].downcase <=> b[1].downcase } end def mappings @@ -234,11 +227,12 @@ def mappings def new @ontology = LinkedData::Client::Models::Ontology.new - @ontologies = LinkedData::Client::Models::Ontology.all(include: 'acronym', include_views: true,display_links: false, display_context: false) + @ontologies = LinkedData::Client::Models::Ontology.all(include: 'acronym', include_views: true, + display_links: false, display_context: false) @categories = LinkedData::Client::Models::Category.all @groups = LinkedData::Client::Models::Group.all - @user_select_list = LinkedData::Client::Models::User.all.map {|u| [u.username, u.id]} - @user_select_list.sort! {|a,b| a[1].downcase <=> b[1].downcase} + @user_select_list = LinkedData::Client::Models::User.all.map { |u| [u.username, u.id] } + @user_select_list.sort! { |a, b| a[1].downcase <=> b[1].downcase } end def notes @@ -256,9 +250,9 @@ def notes def instances if request.xhr? - render partial: 'instances/instances', locals: { id: 'instances-data-table'}, layout: false + render partial: 'instances/instances', locals: { id: 'instances-data-table' }, layout: false else - render partial: 'instances/instances', locals: { id: 'instances-data-table'}, layout: 'ontology_viewer' + render partial: 'instances/instances', locals: { id: 'instances-data-table' }, layout: 'ontology_viewer' end end @@ -392,7 +386,7 @@ def summary @metrics = @ontology.explore.metrics rescue [] #@reviews = @ontology.explore.reviews.sort {|a,b| b.created <=> a.created} || [] - @projects = @ontology.explore.projects.sort {|a,b| a.name.downcase <=> b.name.downcase } || [] + @projects = @ontology.explore.projects.sort { |a, b| a.name.downcase <=> b.name.downcase } || [] @analytics = LinkedData::Client::HTTP.get(@ontology.links['analytics']) #Call to fairness assessment service @@ -400,7 +394,7 @@ def summary @fair_scores_data = create_fair_scores_data(tmp.values.first) unless tmp.nil? @views = get_views(@ontology) - @view_decorators = @views.map{ |view| ViewDecorator.new(view, view_context) } + @view_decorators = @views.map { |view| ViewDecorator.new(view, view_context) } if request.xhr? render partial: 'ontologies/sections/metadata', layout: false @@ -421,8 +415,8 @@ def update error_response = @ontology.update if response_error?(error_response) @categories = LinkedData::Client::Models::Category.all - @user_select_list = LinkedData::Client::Models::User.all.map {|u| [u.username, u.id]} - @user_select_list.sort! {|a,b| a[1].downcase <=> b[1].downcase} + @user_select_list = LinkedData::Client::Models::User.all.map { |u| [u.username, u.id] } + @user_select_list.sort! { |a, b| a[1].downcase <=> b[1].downcase } @errors = response_errors(error_response) @errors = { acronym: 'Acronym already exists, please use another' } if error_response.status == 409 flash[:error] = @errors @@ -452,16 +446,92 @@ def widgets end end - private + def views_filter(filtered_ontologies, check) + filtered_ontologies.select { |o| (o[:viewOfOnt] && check) || o[:viewOfOnt].nil? } + end + + def retired_filter(filtered_ontologies, check) + filtered_ontologies.select { |o| (o[:status].eql?('retired') && check) || !o[:status].eql?('retired') } + end + + def apply_ontology_filters(ontologies, categories, groups) + @filters = ontology_filters_init(categories, groups) + filtered_ontologies = ontologies + + filtered_ontologies = views_filter(filtered_ontologies, @show_views) + filtered_ontologies = retired_filter(filtered_ontologies, @show_retired) + filtered_ontologies = search(filtered_ontologies, params[:search]) unless (params[:search].nil? || params[:search].empty?) + filtered_ontologies = format_filter(filtered_ontologies, params[:format]) unless params[:format].nil? + filtered_ontologies = sort_by_filter(filtered_ontologies, params[:sort_by]) unless params[:sort_by].nil? + filtered_ontologies = filter_ontologies_by(filtered_ontologies, :categories) + filtered_ontologies = filter_ontologies_by(filtered_ontologies, :groups) + filtered_ontologies = filter_ontologies_by(filtered_ontologies, :naturalLanguage) + filtered_ontologies = filter_ontologies_by(filtered_ontologies, :hasFormalityLevel) + filtered_ontologies = filter_ontologies_by(filtered_ontologies, :isOfType) + filtered_ontologies = filter_ontologies_by(filtered_ontologies, :missingStatus) + + filtered_ontologies + end + + def ontology_filters_init(categories, groups) + @languages = submission_metadata.select { |x| x["@id"]["naturalLanguage"] }.first["enforcedValues"].map do |id, name| + { "id" => id, "name" => name, "acronym" => id.split('/').last } + end + + @formalityLevel = submission_metadata.select { |x| x["@id"]["hasFormalityLevel"] }.first["enforcedValues"].map do |id, name| + { "id" => id, "name" => name, "acronym" => name.camelize(:lower) } + end + + @isOfType = submission_metadata.select { |x| x["@id"]["isOfType"] }.first["enforcedValues"].map do |id, name| + { "id" => id, "name" => name, "acronym" => name.camelize(:lower) } + end + + @missingStatus = [ + {"id" => "RDF", "name" => "RDF", "acronym" => "RDF"}, + {"id" => "ABSOLETE", "name" => "ABSOLETE", "acronym" => "ABSOLETE"}, + {"id" => "METRICS", "name" => "METRICS", "acronym" => "METRICS"}, + {"id" => "RDF_LABELS", "name" => "RDF LABELS", "acronym" => "RDFLABELS"}, + {"id" => "UPLOADED", "name" => "UPLOADED", "acronym" => "UPLOADED"}, + {"id" => "INDEXED_PROPERTIES", "name" => "INDEXED PROPERTIES", "acronym" => "INDEXED_PROPERTIES"}, + {"id" => "ANNOTATOR", "name" => "ANNOTATOR", "acronym" => "ANNOTATOR"}, + {"id" => "DIFF", "name" => "DIFF", "acronym" => "DIFF"} + ] + + #@missingStatus = submission_metadata.select { |x| x["@id"]["submissionStatus"] }.first["enforcedValues"].map do |id, name| + # { "id" => id, "name" => name, "acronym" => name.camelize(:lower) } + #end + + @show_views = params[:show_views]&.eql?('true') + @show_retired = params[:show_retired]&.eql?('true') + @selected_format = params[:format] + @search = params[:search] + + { + categories: object_filter(categories, :categories), + groups: object_filter(groups, :groups), + naturalLanguage: object_filter(@languages, :naturalLanguage), + hasFormalityLevel: object_filter(@formalityLevel, :hasFormalityLevel), + isOfType: object_filter(@isOfType, :isOfType), + missingStatus: object_filter(@missingStatus, :missingStatus) + } + end + + def filter_ontologies_by(filtered_ontologies, object_name) + objects, checks, _ = @filters[object_name] + + return filtered_ontologies if checks.empty? + + filtered_ontologies(filtered_ontologies, checks, object_name) + end def ontology_params - p = params.require(:ontology).permit(:name, :acronym, { administeredBy:[] }, :viewingRestriction, { acl:[] }, - { hasDomain:[] }, :isView, :viewOf, :subscribe_notifications, {group:[]}) + p = params.require(:ontology).permit(:name, :acronym, { administeredBy: [] }, :viewingRestriction, { acl: [] }, + { hasDomain: [] }, :isView, :viewOf, :subscribe_notifications, { group: [] }) p[:administeredBy].reject!(&:blank?) p[:acl].reject!(&:blank?) @@ -470,19 +540,152 @@ def ontology_params p.to_h end - def determine_layout - case action_name - when 'index' - 'angular' - else - super - end - end def get_views(ontology) views = ontology.explore.views || [] - views.select!{ |view| view.access?(session[:user]) } - views.sort{ |a,b| a.acronym.downcase <=> b.acronym.downcase } + views.select! { |view| view.access?(session[:user]) } + views.sort { |a, b| a.acronym.downcase <=> b.acronym.downcase } end + def search(ontologies_table, search_input) + # I need to fix the problem of capitale&small letters + filtered_table = [] + ontologies_table.each do |ontology| + if ontology[:name].downcase.include? search_input.downcase + filtered_table << ontology unless filtered_table.include? ontology + end + end + filtered_table + end + + def format_filter(ontologies_table, format) + filtered_table = [] + ontologies_table.each do |ontology| + + if ontology[:format]&.downcase == format.downcase + filtered_table << ontology + end + end + + filtered_table + end + + def sort_by_filter(ontologies_table, sort_by) + + filtered_table = ontologies_table + case sort_by + when "Name" + for i in 0..ontologies_table.length-1 do + for j in i..ontologies_table.length-1 do + if ontologies_table[i][:name]>ontologies_table[j][:name] + tmp = ontologies_table[i] + ontologies_table[i] = ontologies_table[j] + ontologies_table[j] = tmp + end + end + end + when "class count" + for i in 0..ontologies_table.length-1 do + for j in i..ontologies_table.length-1 do + if ontologies_table[i][:class_count] x.name) + filter = this.element.id.split("_")[0] + } + + this.#dispatchEvent(filter, checks) + } + + + #dispatchEvent(filter, checks){ + let data = { + [filter]: checks, + } + const customEvent = new CustomEvent('changed', { + detail: { + data: data + }, bubbles: true + }); + + this.element.dispatchEvent(customEvent); + } + #getSelectedChecks() { + return Array.from(this.element.querySelectorAll('input:checked')) + } + +} diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index cd0bcb223..e1d8816ad 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -4,6 +4,9 @@ import { application } from "./application" +import BrowseFiltersController from "./browse_filters_controller" +application.register("browse-filters", BrowseFiltersController) + import ChosenController from "./chosen_controller" application.register("chosen", ChosenController) @@ -37,6 +40,9 @@ application.register("metadata-downloader", MetadataDownloaderController) import OntoportalAutocompleteController from "./ontoportal_autocomplete_controller" application.register("ontoportal-autocomplete", OntoportalAutocompleteController) +import ShowFilterCountController from "./show_filter_count_controller" +application.register("show-filter-count", ShowFilterCountController) + import ShowModalController from "./show_modal_controller" application.register("show-modal", ShowModalController) diff --git a/app/javascript/controllers/turbo_frame_controller.js b/app/javascript/controllers/turbo_frame_controller.js index 41c67dd5d..11c1f2246 100644 --- a/app/javascript/controllers/turbo_frame_controller.js +++ b/app/javascript/controllers/turbo_frame_controller.js @@ -14,13 +14,10 @@ export default class extends Controller { } updateFrame(event) { - const { data } = event.detail - const values = Object.values(data) - - // remove null and empty values - values.filter((value) => value !== "" || value !== undefined) - - if (values.length === 0) { + + const newData = event.detail.data + const values = Object.entries(newData)[0][1] + if (values.filter(x => x.length !== 0).length === 0 && this.placeHolderValue) { this.frame.innerHTML = this.placeHolderValue } else { this.frame.innerHTML = "" diff --git a/app/javascript/mixins/useHistory.js b/app/javascript/mixins/useHistory.js index f84ff4133..874f82310 100644 --- a/app/javascript/mixins/useHistory.js +++ b/app/javascript/mixins/useHistory.js @@ -22,16 +22,20 @@ export class HistoryService { } getUpdatedURL(currentUrl, newData) { - const base = document.location.origin - const url = new URL(currentUrl, base) - this.#updateURLFromState(url.searchParams, this.getState().data) - this.#addNewDataToUrl(url, newData) - return url.pathname + url.search - } + const url = new URL(currentUrl, document.location.origin) + const urlParams = url.searchParams + this.#updateURLFromState(urlParams, this.getState()) - #addNewDataToUrl(url, newData) { - const wantedData = this.#filterUnwantedData(newData, this.unWantedData); + + this.#filterUnwantedData(newData).forEach(([updatedParam, newValue]) => { + newValue = Array.isArray(newValue) ? newValue : [newValue] + if (newValue !== null && Array.from(newValue).length > 0) { + urlParams.set(updatedParam, newValue.join(',')) + }else{ + urlParams.delete(updatedParam) + } + }) wantedData.forEach(([updatedParam, newValue]) => { if (newValue === null) { @@ -43,10 +47,13 @@ export class HistoryService { }); } - #filterUnwantedData(data, unWantedData) { - return Object.entries(data).filter(([key]) => !unWantedData.some(uw => key.toLowerCase().includes(uw.toLowerCase()))) + #filterUnwantedData(newData) { + const unWantedData = ['turbo', 'controller', 'target', 'value'] + return Object.entries(newData).filter(([key]) => unWantedData.filter(x => key.toLowerCase().includes(x)).length === 0) } + #initStateFromUrl(currentUrl) { + #initStateFromUrl(currentUrl) { const url = new URL(currentUrl, document.location.origin) const urlParams = url.searchParams @@ -58,9 +65,19 @@ export class HistoryService { } #updateURLFromState(urlParams, state) { - Object.entries(state).forEach(([key, val]) => { - if (key !== 'p'){ - urlParams.set(key, val) + let oldValue = null + urlParams.forEach((newVal, key) => { + oldValue = state[key] + + if (oldValue !== undefined && oldValue !== newVal) { + if (newVal.length !== 0){ + urlParams.set(key, newVal) + }else{ + urlParams.remove(key) + } + + } else if (oldValue !== undefined) { + state[key] = newVal } }) } diff --git a/package.json b/package.json index d112843e7..067f33b97 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "dependencies": { "@hotwired/stimulus": "^3.0.1", "@hotwired/turbo-rails": "^7.1.1", + "debounce": "^1.2.1", "esbuild": "^0.14.41", "flatpickr": "^4.6.13", "split.js": "^1.6.5", diff --git a/yarn.lock b/yarn.lock index b3e75bb55..4034c999f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,6 +30,11 @@ resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.4.tgz#70a3ca56809f7aaabb80af2f9c01ae51e1a8ed41" integrity sha512-tz4oM+Zn9CYsvtyicsa/AwzKZKL+ITHWkhiu7x+xF77clh2b4Rm+s6xnOgY/sGDWoFWZmtKsE95hxBPkgQQNnQ== +debounce@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" + integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== + esbuild-android-64@0.14.54: version "0.14.54" resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"