diff --git a/.github/workflows/cucumber_tests.yml b/.github/workflows/cucumber_tests.yml index 03840d7e63..32c34d65d7 100644 --- a/.github/workflows/cucumber_tests.yml +++ b/.github/workflows/cucumber_tests.yml @@ -17,6 +17,9 @@ env: on: push: + branches: + - develop + - master pull_request: types: # defaults @@ -72,7 +75,7 @@ jobs: env: DBPORT: ${{ job.services.mysql.ports[3306] }} run: | - bin/setup + bin/setup --skip-server bundle exec rake assets:precompile # Actually run our tests diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3c07524d38..e3bec5df12 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,6 +2,9 @@ name: Lint on: push: + branches: + - develop + - master pull_request: types: # defaults diff --git a/.github/workflows/rake_tests.yml b/.github/workflows/rake_tests.yml index 8858329514..9dabef0b20 100644 --- a/.github/workflows/rake_tests.yml +++ b/.github/workflows/rake_tests.yml @@ -14,6 +14,9 @@ env: on: push: + branches: + - develop + - master pull_request: types: # defaults diff --git a/.github/workflows/rspec_feature_tests.yml b/.github/workflows/rspec_feature_tests.yml index 148010ef76..184fd8563d 100644 --- a/.github/workflows/rspec_feature_tests.yml +++ b/.github/workflows/rspec_feature_tests.yml @@ -17,6 +17,9 @@ env: on: push: + branches: + - develop + - master pull_request: types: # defaults diff --git a/.github/workflows/rspec_unit_tests.yml b/.github/workflows/rspec_unit_tests.yml index fa8be7b232..58de58a87a 100644 --- a/.github/workflows/rspec_unit_tests.yml +++ b/.github/workflows/rspec_unit_tests.yml @@ -17,6 +17,9 @@ env: on: push: + branches: + - develop + - master pull_request: types: # defaults diff --git a/.gitignore b/.gitignore index d42ad8f7d2..0a27df8188 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,9 @@ app/sample_manifest_excel/doc/ docs/accessioning/**/*.png docs/accessioning/**/*.svg +# Generated files from notification templates +config/accession/*.j2 + # Test files capybara*.html spec/examples.txt diff --git a/.nvmrc b/.nvmrc index d845d9d88d..8e35034890 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.14.0 +24.14.1 diff --git a/.prettierignore b/.prettierignore index 430d204f7f..f1908d47d1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,6 +13,7 @@ *.log *.log.[0-9] *.min.js +*.mjml *.orig *.sublime-workspace *.swp diff --git a/.release-version b/.release-version index 11566873c1..a081b7573c 100644 --- a/.release-version +++ b/.release-version @@ -1 +1 @@ -14.90.0 +14.90.1 diff --git a/.rubocop.yml b/.rubocop.yml index 0b54779d0b..5709cbcfd1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -26,11 +26,12 @@ plugins: AllCops: NewCops: enable Exclude: + - bin/* + - config/initializers/content_security_policy.rb - db/schema.rb - db/views_schema.rb - features/support/env.rb - lib/tasks/cucumber.rake - - bin/* - node_modules/**/* - tmp/**/* @@ -255,6 +256,20 @@ Style/EmptyClassDefinition: Style/EmptyMethod: EnforcedStyle: expanded +# This is now default in Ruby 3.4 +# Excluding generated files +Style/FrozenStringLiteralComment: + Exclude: + - bin/* + - config/application.rb + - config/boot.rb + - config/environment.rb + - config/environments/*.rb + - config/initializers/content_security_policy.rb + - config/initializers/filter_parameter_logging.rb + - config/initializers/inflections.rb + - config/puma.rb + # Only use shorthand hash syntax when all keys match the variables for better readability Style/HashSyntax: EnforcedShorthandSyntax: consistent @@ -269,7 +284,17 @@ Style/QuotedSymbols: Exclude: - bin/* -# these are generally generated files and don't need excessive linting +# Files generated by Rails Style/StringLiterals: Exclude: - bin/* + - config/application.rb + - config/boot.rb + - config/environment.rb + - config/environments/*.rb + - config/puma.rb + +# Files generated by Rails and frequently updated +Style/SymbolArray: + Exclude: + - config/initializers/filter_parameter_logging.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6a3f7a19c3..e715df25f4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit` -# on 2026-03-13 13:25:22 UTC using RuboCop version 1.85.1. +# on 2026-04-20 08:03:49 UTC using RuboCop version 1.86.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -133,7 +133,7 @@ Lint/DuplicateMethods: - 'app/models/stock_stamper.rb' - 'lib/accession/tag.rb' -# Offense count: 62 +# Offense count: 61 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: Exclude: @@ -186,7 +186,6 @@ Lint/EmptyBlock: - 'test/factories/location_report_factories.rb' - 'test/unit/data_release_test.rb' - 'test/unit/fragment_test.rb' - - 'test/unit/tasks/plate_transfer_task_test.rb' # Offense count: 1 # Configuration parameters: AllowComments. @@ -499,7 +498,7 @@ Naming/PredicatePrefix: - 'lib/has_behaviour.rb' - 'lib/manifest_util.rb' -# Offense count: 246 +# Offense count: 245 # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer # AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 @@ -507,7 +506,6 @@ Naming/VariableNumber: Exclude: - 'app/controllers/quad_stamp_controller.rb' - 'app/models/transfer_request.rb' - - 'config/application.rb' - 'db/seeds/0015_transfer_templates.rb' - 'spec/api/asset_audit_spec.rb' - 'spec/controllers/api/v2/heron/tube_racks_controller_spec.rb' @@ -972,7 +970,7 @@ RSpec/MultipleDescribes: - 'spec/lib/label_printer/asset_labels_spec.rb' - 'spec/models/qc_result/qc_result_spec.rb' -# Offense count: 925 +# Offense count: 926 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: @@ -1563,14 +1561,6 @@ Rails/InverseOf: - 'app/models/well.rb' - 'app/models/work_order.rb' -# Offense count: 4 -Rails/LexicallyScopedActionFilter: - Exclude: - - 'app/controllers/admin/programs_controller.rb' - - 'app/controllers/pipelines_controller.rb' - - 'app/controllers/projects_controller.rb' - - 'app/controllers/sdb/sample_manifests_controller.rb' - # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/NegateInclude: @@ -1602,6 +1592,30 @@ Rails/RootPathnameMethods: Exclude: - 'spec/rails_helper.rb' +# Offense count: 19 +# This cop supports unsafe autocorrection (--autocorrect-all). +Rails/StrongParametersExpect: + Exclude: + - 'app/controllers/admin/bait_libraries/bait_library_suppliers_controller.rb' + - 'app/controllers/admin/primer_panels_controller.rb' + - 'app/controllers/admin/programs_controller.rb' + - 'app/controllers/batches_controller.rb' + - 'app/controllers/bulk_submission_excel/downloads_controller.rb' + - 'app/controllers/labware_controller.rb' + - 'app/controllers/location_reports_controller.rb' + - 'app/controllers/npg_actions/assets_controller.rb' + - 'app/controllers/phi_x/spiked_buffers_controller.rb' + - 'app/controllers/phi_x/stocks_controller.rb' + - 'app/controllers/poolings_controller.rb' + - 'app/controllers/qc_files_controller.rb' + - 'app/controllers/receptacles_controller.rb' + - 'app/controllers/report_fails_controller.rb' + - 'app/controllers/stock_stampers_controller.rb' + - 'app/controllers/tag_groups_controller.rb' + - 'app/controllers/tag_layout_templates_controller.rb' + - 'app/controllers/tag_sets_controller.rb' + - 'app/controllers/tag_substitutions_controller.rb' + # Offense count: 2 Rails/ThreeStateBooleanColumn: Exclude: @@ -2025,7 +2039,7 @@ Style/NumericPredicate: - 'lib/deployed.rb' - 'lib/submission_serializer.rb' -# Offense count: 32 +# Offense count: 20 # Configuration parameters: AllowedClasses. Style/OneClassPerFile: Exclude: @@ -2038,14 +2052,6 @@ Style/OneClassPerFile: - 'lib/api_tools.rb' - 'lib/authenticated_test_helper.rb' - 'lib/informatics/test/helpers/authentication_helper.rb' - - 'spec/models/broadcast_event/broadcast_event_spec.rb' - - 'spec/support/api_v2_resource_matchers.rb' - - 'spec/uat_actions/uat_actions_spec.rb' - - 'test/controllers/authentication_controller_test.rb' - - 'test/lib/label_printer/labels_multiplication_test.rb' - - 'test/test_helper.rb' - - 'test/unit/eventful_entry_test.rb' - - 'test/unit/tasks/plate_transfer_task_test.rb' # Offense count: 18 # Configuration parameters: AllowedMethods. diff --git a/Gemfile b/Gemfile index 8cf19d6709..e8fd7fba0e 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ group :default do gem 'concurrent-ruby', '1.3.5' gem 'configatron' gem 'formtastic' - gem 'rails', '~> 7.2.0' + gem 'rails', '~> 8.0.0' # Previously part of ruby or rails, now separate gems gem 'drb' diff --git a/Gemfile.lock b/Gemfile.lock index e2536e3eb8..eb7d386a62 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,33 +43,31 @@ GEM specs: aasm (5.5.2) concurrent-ruby (~> 1.0) - actioncable (7.2.3.1) - actionpack (= 7.2.3.1) - activesupport (= 7.2.3.1) + actioncable (8.0.5) + actionpack (= 8.0.5) + activesupport (= 8.0.5) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.3.1) - actionpack (= 7.2.3.1) - activejob (= 7.2.3.1) - activerecord (= 7.2.3.1) - activestorage (= 7.2.3.1) - activesupport (= 7.2.3.1) + actionmailbox (8.0.5) + actionpack (= 8.0.5) + activejob (= 8.0.5) + activerecord (= 8.0.5) + activestorage (= 8.0.5) + activesupport (= 8.0.5) mail (>= 2.8.0) - actionmailer (7.2.3.1) - actionpack (= 7.2.3.1) - actionview (= 7.2.3.1) - activejob (= 7.2.3.1) - activesupport (= 7.2.3.1) + actionmailer (8.0.5) + actionpack (= 8.0.5) + actionview (= 8.0.5) + activejob (= 8.0.5) + activesupport (= 8.0.5) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.3.1) - actionview (= 7.2.3.1) - activesupport (= 7.2.3.1) - cgi + actionpack (8.0.5) + actionview (= 8.0.5) + activesupport (= 8.0.5) nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4, < 3.3) + rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) @@ -78,38 +76,37 @@ GEM actionpack-xml_parser (2.0.1) actionpack (>= 5.0) railties (>= 5.0) - actiontext (7.2.3.1) - actionpack (= 7.2.3.1) - activerecord (= 7.2.3.1) - activestorage (= 7.2.3.1) - activesupport (= 7.2.3.1) + actiontext (8.0.5) + actionpack (= 8.0.5) + activerecord (= 8.0.5) + activestorage (= 8.0.5) + activesupport (= 8.0.5) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.3.1) - activesupport (= 7.2.3.1) + actionview (8.0.5) + activesupport (= 8.0.5) builder (~> 3.1) - cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.2.3.1) - activesupport (= 7.2.3.1) + activejob (8.0.5) + activesupport (= 8.0.5) globalid (>= 0.3.6) - activemodel (7.2.3.1) - activesupport (= 7.2.3.1) - activerecord (7.2.3.1) - activemodel (= 7.2.3.1) - activesupport (= 7.2.3.1) + activemodel (8.0.5) + activesupport (= 8.0.5) + activerecord (8.0.5) + activemodel (= 8.0.5) + activesupport (= 8.0.5) timeout (>= 0.4.0) activerecord-import (2.2.0) activerecord (>= 4.2) - activestorage (7.2.3.1) - actionpack (= 7.2.3.1) - activejob (= 7.2.3.1) - activerecord (= 7.2.3.1) - activesupport (= 7.2.3.1) + activestorage (8.0.5) + actionpack (= 8.0.5) + activejob (= 8.0.5) + activerecord (= 8.0.5) + activesupport (= 8.0.5) marcel (~> 1.0) - activesupport (7.2.3.1) + activesupport (8.0.5) base64 benchmark (>= 0.3) bigdecimal @@ -118,10 +115,11 @@ GEM drb i18n (>= 1.6, < 2) logger (>= 1.4.2) - minitest (>= 5.1, < 6) + minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - addressable (2.8.9) + uri (>= 0.13.1) + addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) after_commit_everywhere (1.6.0) activerecord (>= 4.2) @@ -133,7 +131,7 @@ GEM backports (3.25.2) base64 (0.3.0) benchmark (0.5.0) - bigdecimal (4.1.0) + bigdecimal (4.1.1) bootsnap (1.23.0) msgpack (~> 1.2) builder (3.3.0) @@ -167,7 +165,6 @@ GEM marcel (~> 1.0) nokogiri (~> 1.10, >= 1.10.4) rubyzip (>= 2.4, < 4) - cgi (0.5.1) childprocess (5.1.0) logger (~> 1.5) choice (0.2.0) @@ -247,14 +244,14 @@ GEM ffi (1.17.3-arm64-darwin) ffi (1.17.3-x86_64-darwin) ffi (1.17.3-x86_64-linux-gnu) - flipper (1.4.0) + flipper (1.4.1) concurrent-ruby (< 2) - flipper-active_record (1.4.0) + flipper-active_record (1.4.1) activerecord (>= 4.2, < 9) - flipper (~> 1.4.0) - flipper-ui (1.4.0) + flipper (~> 1.4.1) + flipper-ui (1.4.1) erubi (>= 1.0.0, < 2.0.0) - flipper (~> 1.4.0) + flipper (~> 1.4.1) rack (>= 1.4, < 4) rack-protection (>= 1.5.3, < 5.0.0) rack-session (>= 1.0.2, < 3.0.0) @@ -283,10 +280,7 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.19.2) - json-schema (6.2.0) - addressable (~> 2.8) - bigdecimal (>= 3.1, < 5) + json (2.19.3) jsonapi-resources (0.9.0) activerecord (>= 4.1) concurrent-ruby @@ -318,8 +312,6 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.3) - mcp (0.9.2) - json-schema (>= 4.1) memoist3 (1.0.0) method_source (1.1.0) mime-types (3.7.0) @@ -368,8 +360,8 @@ GEM nokogiri (1.19.2-x86_64-linux-gnu) racc (~> 1.4) ostruct (0.6.3) - parallel (1.27.0) - parser (3.3.10.2) + parallel (2.0.1) + parser (3.3.11.1) ast (~> 2.4.1) racc pp (0.6.3) @@ -391,7 +383,7 @@ GEM puma (7.2.0) nio4r (~> 2.0) racc (1.8.1) - rack (2.2.22) + rack (2.2.23) rack-acceptable (0.1.0) rack (>= 1.1.0) rack-cors (2.0.2) @@ -410,20 +402,20 @@ GEM rackup (1.0.1) rack (< 3) webrick - rails (7.2.3.1) - actioncable (= 7.2.3.1) - actionmailbox (= 7.2.3.1) - actionmailer (= 7.2.3.1) - actionpack (= 7.2.3.1) - actiontext (= 7.2.3.1) - actionview (= 7.2.3.1) - activejob (= 7.2.3.1) - activemodel (= 7.2.3.1) - activerecord (= 7.2.3.1) - activestorage (= 7.2.3.1) - activesupport (= 7.2.3.1) + rails (8.0.5) + actioncable (= 8.0.5) + actionmailbox (= 8.0.5) + actionmailer (= 8.0.5) + actionpack (= 8.0.5) + actiontext (= 8.0.5) + actionview (= 8.0.5) + activejob (= 8.0.5) + activemodel (= 8.0.5) + activerecord (= 8.0.5) + activestorage (= 8.0.5) + activesupport (= 8.0.5) bundler (>= 1.15.0) - railties (= 7.2.3.1) + railties (= 8.0.5) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -441,10 +433,9 @@ GEM loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-perftest (0.0.7) - railties (7.2.3.1) - actionpack (= 7.2.3.1) - activesupport (= 7.2.3.1) - cgi + railties (8.0.5) + actionpack (= 8.0.5) + activesupport (= 8.0.5) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -460,7 +451,7 @@ GEM logger rbtree (0.4.6) rdoc (6.3.4.1) - regexp_parser (2.11.3) + regexp_parser (2.12.0) reline (0.6.3) io-console (~> 0.5) rest-client (2.1.0) @@ -507,19 +498,18 @@ GEM rspec-support (>= 3.13.0, < 5.0.0) rspec-support (3.13.7) ruboclean (0.7.1) - rubocop (1.85.1) + rubocop (1.86.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) - mcp (~> 0.6) - parallel (~> 1.10) + parallel (>= 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.0) + rubocop-ast (1.49.1) parser (>= 3.3.7.2) prism (~> 1.7) rubocop-capybara (2.22.1) @@ -528,7 +518,7 @@ GEM rubocop-factory_bot (2.28.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) - rubocop-minitest (0.38.2) + rubocop-minitest (0.39.1) lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) @@ -570,7 +560,7 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.16.8) securerandom (0.4.1) - selenium-webdriver (4.41.0) + selenium-webdriver (4.43.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -613,10 +603,10 @@ GEM syslog (0.4.0) logger temple (0.10.4) - test-prof (1.6.0) + test-prof (1.6.1) thor (1.5.0) tilt (2.6.1) - timecop (0.9.10) + timecop (0.9.11) timeout (0.6.1) traceroute (0.8.1) rails (>= 3.0.0) @@ -656,7 +646,7 @@ GEM will_paginate (>= 3.0.3) xpath (3.2.0) nokogiri (~> 1.8) - yard (0.9.38) + yard (0.9.42) yard-activerecord (0.0.17) activesupport yard (>= 0.8.3) @@ -729,7 +719,7 @@ DEPENDENCIES rack-acceptable rack-cors rack-mini-profiler - rails (~> 7.2.0) + rails (~> 8.0.0) rails-controller-testing rails-erd rails-perftest diff --git a/README.md b/README.md index 767b7fbb12..d9b814bd3e 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ It will pick up the version from the .ruby-version file automatically To automatically install the required gems, set-up default configuration files, and set up your database run: ```shell -bin/setup +bin/setup --skip-server ``` ### Manual Sequencescape setup diff --git a/app/controllers/admin/programs_controller.rb b/app/controllers/admin/programs_controller.rb index 99389d4e10..0075c588dc 100644 --- a/app/controllers/admin/programs_controller.rb +++ b/app/controllers/admin/programs_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Admin::ProgramsController < ApplicationController authorize_resource - before_action :discover_program, only: %i[show edit update destroy] + before_action :discover_program, only: %i[show edit update] def index @programs = Program.all diff --git a/app/controllers/api/v2/orders_controller.rb b/app/controllers/api/v2/orders_controller.rb index aa08419060..db8e337d04 100644 --- a/app/controllers/api/v2/orders_controller.rb +++ b/app/controllers/api/v2/orders_controller.rb @@ -17,6 +17,17 @@ class OrderProcessor < JSONAPI::Processor def prepare_context context[:template] = find_template context[:template_attributes] = template_attributes unless context[:template].nil? + context[:project] = find_project + end + + def find_project + project_uuid = params[:data][:attributes][:project_uuid] + return nil if project_uuid.nil? + + project = Project.with_uuid(project_uuid).first + raise JSONAPI::Exceptions::InvalidFieldValue.new(:project_uuid, project_uuid) if project.nil? + + project end def find_template diff --git a/app/controllers/batches_controller.rb b/app/controllers/batches_controller.rb index 717c23fe38..4549d403f5 100644 --- a/app/controllers/batches_controller.rb +++ b/app/controllers/batches_controller.rb @@ -387,24 +387,34 @@ def find_batch_by_barcode # the batch. Ultima sample sheets are allowed to be downloaded without # authentication. For all other pipelines, the user must be logged in. # + # @note When checking the pipeline class, always check the most specific + # subclass first in the inheritance hierarchy, before considering parent + # classes. + # # @return [Boolean] true if download is allowed, false otherwise def allow_sample_sheet_download? - @batch.pipeline.is_a?(UltimaSequencingPipeline) || logged_in? + return true if @batch.pipeline.is_a?(UltimaUG200SequencingPipeline) + return true if @batch.pipeline.is_a?(UltimaSequencingPipeline) + + logged_in? end # Generates and sends the appropriate sample sheet(s) for the batch. + # + # @note When checking the pipeline class, always check the most specific + # subclass first in the inheritance hierarchy, before considering parent + # classes. + # # @return [void] def generate_sample_sheet return redirect_to(login_path) unless allow_sample_sheet_download? - if @batch.pipeline.is_a?(ElementAvitiSequencingPipeline) - generate_element_aviti_sample_sheet - elsif @batch.pipeline.is_a?(UltimaSequencingPipeline) - generate_ultima_sample_sheet - else - flash[:error] = 'Sample sheet generation is not supported for this pipeline.' - redirect_to controller: 'batches', action: 'show', id: @batch.id - end + return generate_element_aviti_sample_sheet if @batch.pipeline.is_a?(ElementAvitiSequencingPipeline) + return generate_ultima_ug200_sample_sheet if @batch.pipeline.is_a?(UltimaUG200SequencingPipeline) + return generate_ultima_sample_sheet if @batch.pipeline.is_a?(UltimaSequencingPipeline) + + flash[:error] = 'Sample sheet generation is not supported for this pipeline.' + redirect_to controller: 'batches', action: 'show', id: @batch.id end private @@ -422,7 +432,21 @@ def generate_element_aviti_sample_sheet # Generates and sends the Ultima sample sheet ZIP archive for the batch. # @return [void] def generate_ultima_sample_sheet - zip_string = UltimaSampleSheet::SampleSheetGenerator.generate(@batch) + send_run_manifest_zip(UltimaSampleSheet::SampleSheetGenerator) + end + + # Generates and sends the Ultima UG200 sample sheet ZIP archive for the batch. + # @return [void] + def generate_ultima_ug200_sample_sheet + send_run_manifest_zip(UltimaSampleSheet::UG200SampleSheetGenerator) + end + + # Helper method to generate and send the Ultima sample sheet ZIP archive for + # the batch using the provided generator class. + # @param generator [Class] the sample sheet generator class to use + # @return [void] + def send_run_manifest_zip(generator) + zip_string = generator.generate(@batch) send_data zip_string, type: 'application/zip', filename: "batch_#{@batch.id}_run_manifest.zip", diff --git a/app/controllers/pipelines_controller.rb b/app/controllers/pipelines_controller.rb index 4401972e23..7d7cf36be1 100644 --- a/app/controllers/pipelines_controller.rb +++ b/app/controllers/pipelines_controller.rb @@ -21,7 +21,7 @@ # - Release a batch (release) # - Show a batch summary (summary) class PipelinesController < ApplicationController - before_action :find_pipeline_by_id, only: %i[show activate deactivate destroy batches] + before_action :find_pipeline_by_id, only: %i[show activate deactivate batches] before_action :prepare_batch_and_pipeline, only: %i[summary finish] after_action :set_cache_disabled!, only: [:show] diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 98f865dcea..71a75677ca 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -5,7 +5,7 @@ class ProjectsController < ApplicationController # rubocop:todo Metrics/ClassLen # It should be removed wherever possible and the correct Strong Parameter options applied in its place. before_action :evil_parameter_hack! before_action :login_required - before_action :set_variables_for_project, only: %i[show edit update destroy studies] + before_action :set_variables_for_project, only: %i[show edit update destroy] # TODO: before_action :redirect_if_not_owner_or_admin, :only => [:create, :update, :destroy, :edit, :new] diff --git a/app/controllers/sdb/sample_manifests_controller.rb b/app/controllers/sdb/sample_manifests_controller.rb index 9943f2a5b5..0dc12cd373 100644 --- a/app/controllers/sdb/sample_manifests_controller.rb +++ b/app/controllers/sdb/sample_manifests_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class Sdb::SampleManifestsController < Sdb::BaseController # rubocop:todo Metrics/ClassLength - before_action :set_sample_manifest_id, only: %i[show generated print_labels] + before_action :set_sample_manifest_id, only: %i[show print_labels] before_action :validate_type, only: %i[new create] LIMIT_ERROR_LENGTH = 10_000 diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 212b3738c4..86f54aa5e0 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -15,33 +15,48 @@ def self.generate(batch) # for each request in the given Ultima sequencing batch. class Generator # rubocop:disable Metrics/ClassLength PLATE_LENGTH = 8 # Assumes 96-well tag plates with 8 rows (A-H). - HEADER_TITLE = ['[Header]'].freeze - GLOBAL_TITLE = ['[Global]'].freeze - GLOBAL_HEADERS = %w[ - Application - sequencing_recipe - analysis_recipe - ].freeze - SAMPLES_TITLE = ['[Samples]'].freeze - SAMPLES_HEADERS = %w[ - Sample_ID - Library_name - Index_Barcode_Num - Index_Barcode_Sequence - Barcode_Plate_Num - Barcode_Plate_Well - application_type - study_id - ].freeze - NUM_COLUMS = SAMPLES_HEADERS.size - # The names of the Ultima tag groups are mapped to the index numbers for - # the Barcode_Plate_Num column, i.e. 1 or 2. The number is also used for - # determining the consistent starting index number for the - # Index_Barcode_Num column, i.e. Z0001 or Z097. - ULTIMA_TAG_GROUPS = { - 'Ultima P1' => 1, - 'Ultima P2' => 2 - }.freeze + + # Using config methods instead of constants to allow easier overriding. + def header_title_config + ['[Header]'].freeze + end + + def global_title_config + ['[Global]'].freeze + end + + def global_headers_config + %w[Application sequencing_recipe analysis_recipe].freeze + end + + def samples_title_config + ['[Samples]'].freeze + end + + def samples_headers_config + %w[ + Sample_ID Library_name Index_Barcode_Num Index_Barcode_Sequence + Barcode_Plate_Num Barcode_Plate_Well application_type study_id + ].freeze + end + + def num_columns_config + samples_headers_config.size + end + + # Tag group config used to derive CSV plate numbers and Z-index starts. + # Values are explicit to avoid arithmetic assumptions in index mapping. + def ultima_tag_groups_config + { + 'Ultima P1' => { plate_num: 1, z_start: 1 }, + 'Ultima P2' => { plate_num: 2, z_start: 97 }, + 'Ultima P3' => { plate_num: 3, z_start: 193 }, + 'UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4)' => { + plate_num: 4, + z_start: 289 + } + }.freeze + end # Initializes the generator with the given batch. # @param batch [UltimaSequencingBatch] the batch to generate sample sheets for @@ -96,7 +111,7 @@ def folder_name # @param csv [CSV] the CSV object to append rows to # @param request [UltimaSequencingRequest] the request whose header data is to be added def add_header_section(csv, request) - csv << pad(HEADER_TITLE) + csv << pad(header_title_config) free_form_text = "Batch #{@batch.id} #{request.asset.human_barcode}" csv << pad([free_form_text]) end @@ -106,8 +121,8 @@ def add_header_section(csv, request) # @param csv [CSV] the CSV object to append rows to # @param _request [UltimaSequencingRequest] the request whose global data is to be added def add_global_section(csv, _request) - csv << pad(GLOBAL_TITLE) - csv << pad(GLOBAL_HEADERS) + csv << pad(global_title_config) + csv << pad(global_headers_config) # Currently there is only one UltimaGlobal record; get the last one. # Future enhancements may allow selecting different records based on # sequencing request or batch properties. @@ -121,22 +136,29 @@ def add_global_section(csv, _request) # @param csv [CSV] the CSV object to append rows to # @param request [UltimaSequencingRequest] the request whose samples are to be added def add_samples_section(csv, request) - csv << pad(SAMPLES_TITLE) - csv << pad(SAMPLES_HEADERS) + csv << pad(samples_title_config) + csv << pad(samples_headers_config) request.asset.aliquots.sort_by(&:id).each do |aliquot| - csv << [ - sample_id_for(aliquot), - library_name_for(aliquot), - index_barcode_num_for(aliquot), - index_barcode_sequence_for(aliquot), - barcode_plate_num_for(aliquot), - barcode_plate_well_for(aliquot), - 'native', # application_type - study_id_for(aliquot) - ] + csv << sample_row_for(aliquot) end end + # Builds a sample section row for a single aliquot. + # @param aliquot [Aliquot] the aliquot whose sample row is needed + # @return [Array] the sample row values + def sample_row_for(aliquot) + [ + sample_id_for(aliquot), + library_name_for(aliquot), + index_barcode_num_for(aliquot), + index_barcode_sequence_for(aliquot), + barcode_plate_num_for(aliquot), + barcode_plate_well_for(aliquot), + 'native', # application_type + study_id_for(aliquot) + ] + end + # Returns a unique sample_ID for the given aliquot. This prefixes numbers # with 's' (lowercase s character) for Sample_ID column, as in s1 … sN, # where N is num of samples on wafer. It starts them with s1 for each wafer, @@ -196,29 +218,30 @@ def study_id_for(aliquot) end # Returns a mapping of all Ultima tags to their respective 1-based index - # numbers. This sorts the tags by their tag group ID and map ID to ensure - # consistent ordering. The index numbers run across all Ultima tag groups, - # i.e. the index is 1 for the first tag in the first tag group and 97 for - # the first tag in the second tag group. + # numbers. Each group's starting index is taken directly from + # ultima_tag_groups_config[:z_start], so numbering is independent of which + # other tag groups exist in the database. # @return [Hash{Tag => Integer}] mapping of tags to index numbers def tag_index_map - @tag_index_map ||= begin - tags = ultima_tag_groups.flat_map { |tg| tg.tags.sort_by(&:map_id) } - tags.each_with_index.to_h { |tag, i| [tag, i + 1] } + @tag_index_map ||= ultima_tag_groups.each_with_object({}) do |tag_group, map| + start_index = ultima_tag_groups_config[tag_group.name][:z_start] + tag_group.tags.sort_by(&:map_id).each_with_index { |tag, i| map[tag] = start_index + i } end end # Returns a mapping of all Ultima tag groups to 1-based index numbers. - # This indexes the tag groups as given in the ULTIMA_TAG_GROUPS hash. + # This indexes the tag groups as given in the ultima_tag_groups_config hash. # @return [Hash{TagGroup => Integer}] mapping of tag groups to index numbers def tag_group_index_map - @tag_group_index_map ||= ultima_tag_groups.index_with { |tg| ULTIMA_TAG_GROUPS[tg.name] } + @tag_group_index_map ||= ultima_tag_groups.index_with { |tg| ultima_tag_groups_config[tg.name][:plate_num] } end - # Returns all unique tag groups used for Ultima sequencing from database. + # Returns all unique tag groups used for Ultima sequencing from database, + # sorted by their index number as defined in ultima_tag_groups_config. # @return [Array] the tag groups used for Ultima sequencing def ultima_tag_groups - @ultima_tag_groups ||= TagGroup.where(name: ULTIMA_TAG_GROUPS.keys) + @ultima_tag_groups ||= TagGroup.where(name: ultima_tag_groups_config.keys) + .sort_by { |tg| ultima_tag_groups_config[tg.name][:plate_num] } end # Returns the requests associated with the batch. @@ -250,7 +273,7 @@ def sample_id_index_map # @param row [Array] the row to pad (defaults to an empty array) # @return [Array] the padded row def pad(row = []) - row + Array.new(NUM_COLUMS - row.size, '') + row + Array.new(num_columns_config - row.size, '') end end end diff --git a/app/controllers/ultima_sample_sheet/ug200_sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/ug200_sample_sheet_generator.rb new file mode 100644 index 0000000000..2f3425103b --- /dev/null +++ b/app/controllers/ultima_sample_sheet/ug200_sample_sheet_generator.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module UltimaSampleSheet::UG200SampleSheetGenerator + # Initiates the sample sheet generation for the given batch. + # @param batch [Batch] the Ultima UG200 sequencing batch to generate sample sheets for + # @return [String] the ZIP archive as a binary string + def self.generate(batch) + Generator.new(batch).generate + end + + # Ultima UG200 sample sheet generator class. + # Uses the shared Ultima base implementation with UG200-specific globals and tags. + class Generator < UltimaSampleSheet::SampleSheetGenerator::Generator + def global_headers_config + ['Application'].freeze + end + + # Added ultima_tag_groups_config items to the parent class to make them + # available to all Ultima sample sheet generators. + + private + + # Adds the global section to the CSV for UG200. + # The request parameter is currently unused. + # @param csv [CSV] the CSV object to append rows to + # @param _request [UltimaSequencingRequest] the request whose global data is to be added + def add_global_section(csv, _request) + csv << pad(global_title_config) + csv << pad(global_headers_config) + data = ['WGS Native'] # Application + csv << pad(data) + end + end +end diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index 8495e54ae5..ff3b9a98d4 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -41,6 +41,11 @@ def stage # rubocop:todo Metrics/CyclomaticComplexity @stage = params[:id].to_i @task = @workflow.tasks[@stage] + # Track whether the current task execution succeeded; defaults to true when + # just rendering so we only redirect on "Update" after a successful do_task + # call. + task_success = true + # If params[:next_stage] is nil then just render the current task # else actually execute the task. unless params[:next_stage].nil? @@ -67,7 +72,7 @@ def stage # rubocop:todo Metrics/CyclomaticComplexity end end - if params[:commit] == 'Update' + if params[:commit] == 'Update' && task_success redirect_to batch_path(@batch) elsif @stage >= @workflow.tasks.size # All requests have finished all tasks: finish workflow diff --git a/app/helpers/submissions_helper.rb b/app/helpers/submissions_helper.rb index 7e12703c8e..49bcc6e0b8 100644 --- a/app/helpers/submissions_helper.rb +++ b/app/helpers/submissions_helper.rb @@ -139,7 +139,8 @@ def submission_link(submission, options) def field_selection_tag(request_options, field_info, name_format, enforce_required) select_tag( name_format % field_info.key, - options_for_select(field_info.selection.map(&:to_s), request_options[field_info.key]), + options_for_select(field_info.selection.map(&:to_s), + request_options[field_info.key].presence || field_info.default_value), class: 'custom-select', required: enforce_required && field_info.required, read_only: field_info.selection.size == 1 diff --git a/app/models/bulk_submission.rb b/app/models/bulk_submission.rb index 170ca24bc5..0f29773072 100644 --- a/app/models/bulk_submission.rb +++ b/app/models/bulk_submission.rb @@ -253,7 +253,8 @@ def process # rubocop:todo Metrics/CyclomaticComplexity 'scrna core cells per chip well', '% phix requested', 'low diversity', - 'ot recipe' + 'ot recipe', + 'wafer size' ].freeze ALIAS_FIELDS = { 'plate barcode' => 'barcode', 'tube barcode' => 'barcode' }.freeze @@ -360,7 +361,8 @@ def extract_request_options(details) ['scrna core cells per chip well', 'cells_per_chip_well'], ['% phix requested', 'percent_phix_requested'], ['low diversity', 'low_diversity'], - ['ot recipe', 'ot_recipe'] + ['ot recipe', 'ot_recipe'], + ['wafer size', 'wafer_size'] ].each do |source_key, target_key| assign_value_if_source_present(details, source_key, request_options, target_key) end diff --git a/app/models/descriptor.rb b/app/models/descriptor.rb index ed37873ea1..d1c37f07f9 100644 --- a/app/models/descriptor.rb +++ b/app/models/descriptor.rb @@ -3,6 +3,9 @@ class Descriptor < ApplicationRecord belongs_to :task serialize :selection, coder: YAML + DATE_YEAR_MIN = 1990 + DATE_YEAR_MAX = 2100 + def is_required? required end @@ -11,4 +14,42 @@ def matches?(search) search.descriptors.each { |descriptor| return true if descriptor.name == name && descriptor.value == value } false end + + # Returns an array of validation errors for the submitted descriptor value. + # The value comes from the Task Details form for a workflow task on a batch. + # @return [Array] An array of error messages, empty if the value is valid + def validate_value(submitted_value) + if is_required? && submitted_value.blank? && Flipper.enabled?(:y25_105_validate_descriptor_required_field) + return ["#{name} is required"] + end + return [] if submitted_value.blank? + return validate_date_value(submitted_value) if kind == 'Date' + + [] + end + + private + + # Validates that the submitted value is a valid date string in the format + # YYYY-MM-DD, and that the year is within a reasonable range. + # @return [Array] An array of error messages, empty if the value is valid + def validate_date_value(submitted_value) + unless submitted_value.match?(/\A\d{4}-\d{2}-\d{2}\z/) + return ["'#{submitted_value}' is not a valid date for #{name} (expected YYYY-MM-DD)"] + end + + parsed_date = Date.iso8601(submitted_value) + validate_date_year(parsed_date) + rescue ArgumentError + ["'#{submitted_value}' is not a valid date for #{name} (expected YYYY-MM-DD)"] + end + + # Validates that the year of the submitted date is within a reasonable range + # to catch common data entry errors (e.g. 62026 instead of 2026). + # @return [Array] An array of error messages, empty if the year is valid + def validate_date_year(parsed_date) + return [] if parsed_date.year.between?(DATE_YEAR_MIN, DATE_YEAR_MAX) + + ["Date year for #{name} must be between #{DATE_YEAR_MIN} and #{DATE_YEAR_MAX} (got #{parsed_date.year})"] + end end diff --git a/app/models/order.rb b/app/models/order.rb index b59a349d9d..6ef2f20b29 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -61,7 +61,12 @@ class Order < ApplicationRecord # rubocop:todo Metrics/ClassLength serialize :item_options, coder: YAML before_validation :set_study_from_aliquots, unless: :cross_study_allowed, if: :autodetect_studies - before_validation :set_project_from_aliquots, unless: :cross_project_allowed, if: :autodetect_projects + before_validation :set_project_from_aliquots, unless: :cross_project_allowed, + if: %i[autodetect_projects project_not_set] + + def project_not_set + project.blank? + end validates :study, presence: true, unless: :cross_study_allowed validates :project, presence: true, unless: :cross_project_allowed diff --git a/app/models/presenters/batch_submenu_presenter.rb b/app/models/presenters/batch_submenu_presenter.rb index ab61700e1b..ba60e58133 100644 --- a/app/models/presenters/batch_submenu_presenter.rb +++ b/app/models/presenters/batch_submenu_presenter.rb @@ -71,13 +71,35 @@ def build_submenu only_path: true } end add_submenu_option 'NPG run data', "#{configatron.run_data_by_batch_id_url}#{@batch.id}" - return unless aviti_run_manifest? || ultima_run_manifest? + return unless run_manifest? add_submenu_option 'Download Sample Sheet', id: @batch.id, controller: :batches, action: :generate_sample_sheet end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # Determines whether to display the submenu option for downloading the sample sheet. + # + # @return [Boolean] true if the link for the download should be displayed + def run_manifest? + aviti_run_manifest? || ultima_run_manifest? || ultima_ug200_run_manifest? + end + + # Determines whether the batch is released and contains any Ultima UG200 + # sequencing requests. + # + # @return [Boolean] true if the batch is released and has Ultima UG200 requests + def ultima_ug200_run_manifest? + @batch.released? && ultima_ug200? + end + + # Determines whether the batch is an Ultima UG200 batch. + # + # @return [Boolean] true if the batch's pipeline is Ultima UG200 + def ultima_ug200? + @pipeline.instance_of?(UltimaUG200SequencingPipeline) + end + # This is used to determine if we need to display the Aviti run manifest option # in the batch submenu. # @return [Boolean] true if the batch is released and has Element Aviti requests @@ -120,8 +142,11 @@ def plate_labels? cherrypicking? end + # Determines whether the batch is an Ultima sequencing batch + # @return [Boolean] true if the batch's pipeline is Ultima def ultima? - @pipeline.is_a?(UltimaSequencingPipeline) + # Use instance_of? instead of is_a? to avoid picking up subclasses. + @pipeline.instance_of?(UltimaSequencingPipeline) end end end diff --git a/app/models/submission_template.rb b/app/models/submission_template.rb index de6bc53fc5..45749a6845 100644 --- a/app/models/submission_template.rb +++ b/app/models/submission_template.rb @@ -27,7 +27,7 @@ class SubmissionTemplate < ApplicationRecord # rubocop:todo Metrics/ClassLength SUPERCEDED_BY_UNKNOWN_TEMPLATE = -2 scope :hidden, -> { order(product_line_id: :asc).where.not(superceded_by_id: LATEST_VERSION) } - scope :visible, -> { order(product_line_id: :asc).where(superceded_by_id: LATEST_VERSION) } + scope :visible, -> { order(product_line_id: :asc).where(superceded_by_id: LATEST_VERSION, automated: false) } scope :include_product_line, -> { includes(:product_line) } def self.grouped_by_product_lines @@ -35,7 +35,7 @@ def self.grouped_by_product_lines end def visible - superceded_by_id == LATEST_VERSION + superceded_by_id == LATEST_VERSION && !automated end def superceded_by_unknown! @@ -54,8 +54,9 @@ def supercede end end - def create_order!(attributes) + def create_order!(attributes, project = nil) new_order(attributes).tap do |order| + order.project = project if project yield(order) if block_given? order.save! end @@ -89,7 +90,7 @@ def input_field_infos end def sequencing? - request_types.any?(&:sequencing) + request_types.any?(&:sequencing?) end def input_asset_type diff --git a/app/models/tasks/set_descriptors_handler.rb b/app/models/tasks/set_descriptors_handler.rb index 62a501e45d..7ae7e56005 100644 --- a/app/models/tasks/set_descriptors_handler.rb +++ b/app/models/tasks/set_descriptors_handler.rb @@ -8,6 +8,9 @@ def render end def perform + errors = validate_descriptor_inputs + return [false, errors.join(', ')] if errors.any? + # Process each request that has been selected in the front end # by default all requests are selected, but in rare circumstances the user # can uncheck a request to exclude it from the step @@ -28,10 +31,39 @@ def perform private + # Iterates every descriptor on the task and asks each one to validate the + # submitted value. + # @return [Array] An array of error messages, empty if all values are valid + def validate_descriptor_inputs + return [] if task.descriptors.empty? + + selected_requests_for_validation.each_with_object([]) do |request, errors| + errors.concat(descriptor_errors_for(request)) + end.uniq + end + def params @params.respond_to?(:permit!) ? @params.permit!.to_h : @params end + # Returns an array of the requests that have been selected in the UI for + # the Task Details form. + # @return [Array] An array of selected requests + def selected_requests_for_validation + requests.select { |request| selected_requests.include?(request.id) } + end + + # For a given request, returns an array of error messages for any descriptors + # that fail validation. + # @return [Array] An array of error messages, empty if all values are valid + def descriptor_errors_for(request) + submitted = descriptors(request) + + task.descriptors.each_with_object([]) do |descriptor, errors| + errors.concat(descriptor.validate_value(submitted[descriptor.name])) + end + end + def process_request(request) LabEvent.create!( batch: batch, diff --git a/app/models/ultima_sequencing_pipeline.rb b/app/models/ultima_sequencing_pipeline.rb index 3a28998d3b..639c7ee349 100644 --- a/app/models/ultima_sequencing_pipeline.rb +++ b/app/models/ultima_sequencing_pipeline.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Specialized sequencing pipeline for Ultima +# Specialized sequencing pipeline for Ultima UG100 class UltimaSequencingPipeline < SequencingPipeline def ot_recipe_consistent_for_batch?(batch) ot_recipe_list = batch.requests.filter_map { |request| request.request_metadata.ot_recipe } diff --git a/app/models/ultima_ug200_sequencing_pipeline.rb b/app/models/ultima_ug200_sequencing_pipeline.rb new file mode 100644 index 0000000000..896f4e7696 --- /dev/null +++ b/app/models/ultima_ug200_sequencing_pipeline.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Specialized sequencing pipeline for Ultima UG200 +class UltimaUG200SequencingPipeline < UltimaSequencingPipeline + def wafer_size_consistent_for_batch?(batch) + wafer_size_list = batch.requests.filter_map { |request| request.request_metadata.wafer_size } + + # There are some requests that don't have the wafer_size attribute + return false if wafer_size_list.size != batch.requests.size + + (wafer_size_list.uniq.size == 1) + end +end diff --git a/app/models/ultima_ug200_sequencing_request.rb b/app/models/ultima_ug200_sequencing_request.rb new file mode 100644 index 0000000000..0bed36c602 --- /dev/null +++ b/app/models/ultima_ug200_sequencing_request.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Request class specific to the Ultima UG200 sequencing platform. +# Includes wafer size and OT recipe. +class UltimaUG200SequencingRequest < SequencingRequest + include Api::Messages::UseqWaferIo::LaneExtensions + + FREE = 'Free' + FLEX = 'Flex' + OT_RECIPE_OPTIONS = [FREE, FLEX].freeze + + has_metadata as: Request do + # Defining the sequencing request metadata here again, as 'has_metadata' + # does not automatically append these custom attributes to the request. + # + # The has_metadata call dynamically defines an inner Metadata class and + # takes the attributes from the block and adds them to the Metadata class. + # There is an assumption that the inner Metadata class is available in a + # sequencing request class defintion. Calling has_metadata again does not + # inherit the attributes given in the block supplied in the superclass. + # They need to be supplied again for this class for a proper inner Metadata + # class definition. In a future refactoring these attributes can be moved a + # class attribute and subclasses can merge its own attibutes to that. A + # common method can set up the inner Metadata class in the subclasses. + custom_attribute(:fragment_size_required_from, integer: true, minimum: 1) + custom_attribute(:fragment_size_required_to, integer: true, minimum: 1) + + custom_attribute(:ot_recipe, default: FREE, in: OT_RECIPE_OPTIONS, required: true) + enum :ot_recipe, { Free: 0, Flex: 1 } + custom_attribute(:wafer_size, default: '10TB', validator: true, required: true, selection: true) + end + + # Delegate to request_metadata so the attributes are visible to the validator in the RSpec tests. + # This delegation has no real effect outside of the tests. + delegate :wafer_size, :ot_recipe, to: :request_metadata + + # Generates unique wafer ID, concatenation of batch_for_opentrons, + # id_pool_lims, and request_order. + # @return [String] unique wafer ID for LIMS + def id_wafer_lims + "#{batch.id}_#{source_labware.human_barcode}_#{position}" + end +end diff --git a/app/resources/api/v2/order_resource.rb b/app/resources/api/v2/order_resource.rb index aa1a935920..49cb774279 100644 --- a/app/resources/api/v2/order_resource.rb +++ b/app/resources/api/v2/order_resource.rb @@ -75,6 +75,11 @@ class OrderResource < BaseResource # @return [String] The UUID of this {Order}. attribute :uuid, readonly: true + # @!attribute [w] project_uuid + # @return [String] The UUID of the project to associate with this Order on creation. + attribute :project_uuid, writeonly: true + attr_writer :project_uuid # Not stored, consumed by OrderProcessor. + ### # Relationships ### @@ -131,7 +136,7 @@ class OrderResource < BaseResource def self.create(context) return super if context[:template].nil? - order = context[:template].create_order!(context[:template_attributes]) + order = context[:template].create_order!(context[:template_attributes], context[:project]) new(order, context) end end diff --git a/app/uat_actions/uat_actions.rb b/app/uat_actions/uat_actions.rb index 1bffc6d0a0..f0e0fd66c2 100644 --- a/app/uat_actions/uat_actions.rb +++ b/app/uat_actions/uat_actions.rb @@ -15,6 +15,7 @@ class UatActions CATEGORY_LIST = %i[setup_and_test generating_samples auxiliary_data quality_control uncategorised].freeze class_attribute :title, :description, :category, :message + self.category = CATEGORY_LIST.last # default category, intended to be overridden by subclasses self.message = 'Completed successfully' class << self @@ -43,11 +44,6 @@ def uat_actions @uat_actions ||= {} end - # Default category should one not be provided - def category - UatActions::CATEGORY_LIST.last - end - # Returns a hash of all registered uat_actions grouped by category and sorted def grouped_and_sorted_uat_actions # raise error if any categories are not in the list diff --git a/app/validators/ultima_ug200_validator.rb b/app/validators/ultima_ug200_validator.rb new file mode 100644 index 0000000000..ae66f898ad --- /dev/null +++ b/app/validators/ultima_ug200_validator.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +class UltimaUG200Validator < UltimaValidator + WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.' + + # Used in _pipeline_limit.html to display custom validation warnings + def self.validation_info + 'Wafer Size and OT Recipe must be the same for both requests.' + end + + # Validates that a batch contains the two requests. + def validate(record) + validate_exactly_two_requests(record) + requests_have_same_ot_recipe(record) + requests_have_same_wafer_size(record) + end + + private + + def requests_have_same_wafer_size(record) + return if record.pipeline.wafer_size_consistent_for_batch?(record) + + record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG) + end +end diff --git a/app/views/bulk_submissions/_guidance.html.erb b/app/views/bulk_submissions/_guidance.html.erb index 6532b2d383..4706dfc832 100644 --- a/app/views/bulk_submissions/_guidance.html.erb +++ b/app/views/bulk_submissions/_guidance.html.erb @@ -47,4 +47,5 @@ used to split a submission up into orders, and as a result must have the same re
% PhiX requested
Required percentage of PhiX needed.
Low Diversity
Low Diversity value being "Yes" or "No"
OT Recipe
OT (OpenTron liquid handler) Recipe value being "Free" or "Flex"
+
Wafer Size
Wafer size value being "5TB", "10TB" or "20TB"
diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000000..5f91c20545 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/setup b/bin/setup index 26115bb2ef..f56c791e20 100755 --- a/bin/setup +++ b/bin/setup @@ -48,10 +48,9 @@ FileUtils.chdir APP_ROOT do puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" - puts "\n== Restarting application server ==" - system! "bin/rails restart" - - # puts "\n== Configuring puma-dev ==" - # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" - # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end end diff --git a/codecov.yml b/codecov.yml index f0cf57aeb3..d2662e06e2 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,12 +1,8 @@ codecov: notify: # do not notify until at least n builds have been uploaded from the CI pipeline - # rake_tests (push) # rake_tests (pull_request) - # rspec_unit_tests x4 (push) # rspec_unit_tests x4 (pull_request) - # rspec_feature_tests x2 (push) # rspec_feature_tests x2 (pull_request) - # cucumber_tests x2 (push) # cucumber_tests x2 (pull_request) - after_n_builds: 18 + after_n_builds: 9 diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml new file mode 100644 index 0000000000..35793042de --- /dev/null +++ b/config/accession/notification-template.mjml @@ -0,0 +1,171 @@ + + + + + + + + + Problems with Accessioning + There are validation errors in the samples provided + + .shadow { + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); + } + .bg-sequencescape-gradient { + background: linear-gradient(45deg, rgba(3, 155, 229, 1) 0%, rgba(139, 195, 74, 1) 100%); + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sequencescape + + + + + Accessioning Failure Notification + + A batch of + + {{ fields | length }} samples have failed to accession for study + + {{ fields[0]["study_name"] }} and manifest + + {{ fields[0]["manifest_id"] }}. +
+ Please review and address the warnings below. +
+
+
+ + + + + + + Summary: + + + + + + + + + + + + + + + +
    + + + {% set ns = namespace(reasons=[]) %} + {% for item in fields %} + {% for reason in item["failure_groups"] %} + {% if reason not in ns.reasons %} + {% set ns.reasons = ns.reasons + [reason] %} + {% endif %} + {% endfor %} + {% endfor %} + + {% for reason in ns.reasons %} + {% set ns2 = namespace(count=0) %} + {% for item in fields %} + {% if reason in item["failure_groups"] %} + {% set ns2.count = ns2.count + 1 %} + {% endif %} + {% endfor %} + +
  • {{ reason }}: {{ ns2.count }} samples
  • + {% endfor %} +
+
+
+
+ + + + + + First + {{ [fields | length, 5] | min }} + failures: + + + + {% for item in fields[:5] %} + + + + + {{ item["sample_name"] }} ({{ item["supplier_sample_name"] }}) + + + + + {{ item["accessioning_status_message"] }} + + + + {% endfor %} + + + + + + Open a support ticket + + + Sent automatically by Sequencescape, on behalf of the + + Production Software Development + + team. +
+ Please do not reply to this email - open a support ticket if you would like further assistance. +
+
+
+
+ +
+
diff --git a/config/application.rb b/config/application.rb index 595437b4af..2e88dd6805 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,8 +1,6 @@ -# frozen_string_literal: true +require_relative "boot" -require_relative 'boot' - -require 'rails/all' +require "rails/all" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -11,47 +9,42 @@ module Sequencescape class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.2 + config.load_defaults 8.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers # -- all .rb files in that directory are automatically loaded after loading # the framework and any gems in your application. - # Configure the default encoding used in templates for Ruby 1.9. - config.encoding = 'utf-8' - # Default options which predate the Rails 5 switch # Due to loading order, set these here and not in an initializer, see https://github.com/rails/rails/issues/23589 config.active_record.belongs_to_required_by_default = false config.action_controller.forgery_protection_origin_check = false config.action_controller.per_form_csrf_tokens = false - # Enable YJIT by default if running Ruby 3.3+ - # YJIT is Ruby's JIT compiler that is available in CRuby since Ruby 3.1. - # It can provide significant performance improvements for Rails applications, offering 15-25% latency improvements. - # In Rails 7.2, YJIT is enabled by default if running Ruby 3.3 or newer. - # You can disable YJIT by setting: - # Rails.application.config.yjit = false - # Sets the exceptions application invoked by the ShowException middleware when an exception happens. config.exceptions_app = routes - # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters += [:password] config.logger = Logger.new(Rails.root.join('log', "#{Rails.env}.log"), 5, 10 * 1024 * 1024) config.logger.formatter = ::Logger::Formatter.new # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true - config.filter_parameters += %i[password credential_1 uploaded_data] + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks generators informatics]) # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # + # Make Time.zone default to the specified zone, and make Active Record store time values + # in the database in UTC, and return them converted to the specified local zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Uncomment to use default local time. + config.time_zone = 'London' # Add additional load paths for your own custom dirs # config.load_paths += %W( #{Rails.root}/extras ) @@ -61,9 +54,6 @@ class Application < Rails::Application config.eager_load_paths += %W[#{Rails.root}/app] config.eager_load_paths += %W[#{Rails.root}/lib] - # Some lib files we don't want to autoload as they are not required in the rails app - %w[generators informatics].each { |file| Rails.autoloaders.main.ignore(Rails.root.join("lib/#{file}")) } - # Eager load when running rake tasks. This ensures our STI classes are loaded, required for record loader # To correctly access all purpose types config.rake_eager_load = true @@ -71,25 +61,10 @@ class Application < Rails::Application # Load the custom inflections to help with the AASM module Rails.autoloaders.main.inflector.inflect('aasm' => 'AASM') - # Make Time.zone default to the specified zone, and make Active Record store time values - # in the database in UTC, and return them converted to the specified local zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Uncomment to use default local time. - config.time_zone = 'London' - # Enable localisations to be split over multiple paths. config.i18n.load_path = Dir[File.join(Rails.root, %w[config locales metadata *.{rb,yml}])] # rubocop:disable Rails/RootPathnameMethods I18n.enforce_available_locales = false - ### - # Adds image/webp to the list of content types Active Storage considers as an image - # Prevents automatic conversion to a fallback PNG, and assumes clients support WebP, - # as they support gif, jpeg, and png. - # This is possible due to broad browser support for WebP, but older browsers and - # email clients may still not support WebP. - # Requires imagemagick/libvips built with WebP support. - #++ - Rails.application.config.active_storage.web_image_content_types = %w[image/png image/jpeg image/gif] - config.generators do |g| g.test_framework :rspec, fixtures: true, diff --git a/config/boot.rb b/config/boot.rb index b81cf42113..3b48b7a901 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,7 +1,6 @@ -# frozen_string_literal: true -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) -require 'bundler/setup' # Set up gems listed in the Gemfile. +require "bundler/setup" # Set up gems listed in the Gemfile. # Bootsnap does not purge its cache, which can cause boot-times to increase # over time. This change will purge the cache every 30 days. In development @@ -35,4 +34,4 @@ end # rubocop:enable Rails/Output, Rails/TimeZone -require 'bootsnap/setup' # Speed up boot time by caching expensive operations. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/bulk_submission_excel/columns.yml b/config/bulk_submission_excel/columns.yml index 02de62cb55..8e265391cb 100644 --- a/config/bulk_submission_excel/columns.yml +++ b/config/bulk_submission_excel/columns.yml @@ -367,3 +367,22 @@ ot_recipe: range_name: :ot_recipe conditional_formattings: empty_cell: +wafer_size: + heading: "Wafer Size" + unlocked: true + attribute: :wafer_size + validation: + options: + type: :list + formula1: "$A$1:$A$2" + allowBlank: false + showInputMessage: true + showErrorMessage: true + errorStyle: :stop + errorTitle: "Wafer Size" + error: "You must enter a value from the list provided." + promptTitle: "Wafer Size" + prompt: "Select a value type from the approved list" + range_name: :wafer_size + conditional_formattings: + empty_cell: diff --git a/config/bulk_submission_excel/ranges.yml b/config/bulk_submission_excel/ranges.yml index a1c091015a..b211a116cb 100644 --- a/config/bulk_submission_excel/ranges.yml +++ b/config/bulk_submission_excel/ranges.yml @@ -27,3 +27,8 @@ ot_recipe: options: - "Free" - "Flex" +wafer_size: + options: + - "5TB" + - "10TB" + - "20TB" diff --git a/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml b/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml new file mode 100644 index 0000000000..df4f31fa60 --- /dev/null +++ b/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml @@ -0,0 +1,86 @@ +--- +# Section names must be unique inside the record loader folder. +"OTR carrier Lot # UG200": + name: "OTR carrier Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 0 +OTR carrier expiry UG200: + name: OTR carrier expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 1 +"Reaction Mix 7 Lot # UG200": + name: "Reaction Mix 7 Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 2 +Reaction Mix 7 expiry UG200: + name: Reaction Mix 7 expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 3 +"NFW Lot # UG200": + name: "NFW Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 4 +NFW expiry UG200: + name: NFW expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 5 +"Oil Lot # UG200": + name: "Oil Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 6 +Oil expiry UG200: + name: Oil expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 7 +Pipette carousel UG200: + name: Pipette carousel + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 8 +Opentrons Inst. Name UG200: + name: Opentrons Inst. Name + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: true + sorter: 9 +Assign Control Bead Tube UG200: + name: Assign Control Bead Tube + task: Amp + workflow: Ultima UG200 + kind: Text + required: false + sorter: 0 +UG AMP Inst. Name UG200: + name: UG AMP Inst. Name + task: Amp + workflow: Ultima UG200 + kind: Text + required: true + sorter: 1 diff --git a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.wip.yml b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.wip.yml new file mode 100644 index 0000000000..76a81c2576 --- /dev/null +++ b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.wip.yml @@ -0,0 +1,16 @@ +--- +FragmentSizeRequiredFromInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: fragment_size_required_from + +FragmentSizeRequiredToInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: fragment_size_required_to + +WaferSizeInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: wafer_size + +OTRecipeInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: ot_recipe diff --git a/config/default_records/pipelines/004_ultima_ug200_pipelines.wip.yml b/config/default_records/pipelines/004_ultima_ug200_pipelines.wip.yml new file mode 100644 index 0000000000..21b128df05 --- /dev/null +++ b/config/default_records/pipelines/004_ultima_ug200_pipelines.wip.yml @@ -0,0 +1,17 @@ +--- +Ultima UG200: + name: Ultima UG200 Sequencing + sti_type: UltimaUG200SequencingPipeline + validator_class_name: UltimaUG200Validator + sorter: 10 + max_size: 2 + summary: 1 + externally_managed: 0 + group_name: Sequencing + control_request_type_id: 0 + min_size: 1 + request_type_keys: + - ultima_ug200_sequencing + workflow: + name: Ultima UG200 + item_limit: 2 diff --git a/config/default_records/plate_purposes/017_ultima_ug200_purposes.wip.yml b/config/default_records/plate_purposes/017_ultima_ug200_purposes.wip.yml new file mode 100644 index 0000000000..682ce88e23 --- /dev/null +++ b/config/default_records/plate_purposes/017_ultima_ug200_purposes.wip.yml @@ -0,0 +1,9 @@ +--- +# 96 well input plates +UPF2 Cherrypicked: + type: PlatePurpose::Input + stock_plate: true + default_state: passed + cherrypickable_target: true + target_type: Plate + size: 96 diff --git a/config/default_records/request_information_types/004_ultima_ug200_request_information_types.wip.yml b/config/default_records/request_information_types/004_ultima_ug200_request_information_types.wip.yml new file mode 100644 index 0000000000..bde23fdb31 --- /dev/null +++ b/config/default_records/request_information_types/004_ultima_ug200_request_information_types.wip.yml @@ -0,0 +1,7 @@ +--- +wafer_size: + name: Wafer size + key: wafer_size + label: Wafer size + width: 5 + hide_in_inbox: 0 diff --git a/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.wip.yml b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.wip.yml new file mode 100644 index 0000000000..028e593253 --- /dev/null +++ b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.wip.yml @@ -0,0 +1,8 @@ +--- +WaferSizeRequestedUltimaUG200Sequencing: + request_type_key: ultima_ug200_sequencing + request_option: wafer_size + valid_options: + - 5TB + - 10TB + - 20TB diff --git a/config/default_records/request_types/005_limber_bespoke.yml b/config/default_records/request_types/005_limber_bespoke.yml index 36969088f0..09ad8b31cc 100644 --- a/config/default_records/request_types/005_limber_bespoke.yml +++ b/config/default_records/request_types/005_limber_bespoke.yml @@ -45,6 +45,7 @@ limber_pcr_bespoke: - TraDIS - TruSeq mRNA (RNA Seq) - SGE Library v0.2 + - Custom 2-step amplicon PCR limber_chromium_bespoke: <<: *limber_bespoke_library name: Limber Chromium Bespoke diff --git a/config/default_records/request_types/025_limber_lcm_triomics_new_added_request_types.wip.yml b/config/default_records/request_types/025_limber_lcm_triomics_new_added_request_types.yml similarity index 100% rename from config/default_records/request_types/025_limber_lcm_triomics_new_added_request_types.wip.yml rename to config/default_records/request_types/025_limber_lcm_triomics_new_added_request_types.yml diff --git a/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml new file mode 100644 index 0000000000..bc5977f439 --- /dev/null +++ b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml @@ -0,0 +1,31 @@ +# Request types for Ultima UG200 sequencing platform. +--- +limber_ultima_ug200_htp_pcr_free: + name: Limber Ultima UG200 HTP PCR Free + order: 1 + request_class_name: IlluminaHtp::Requests::StdLibraryRequest + asset_type: Well + for_multiplexing: false + billable: true + product_line_name: Ultima # same as UG100 + acceptable_purposes: + - UPF2 Cherrypicked + library_types: + - Ultima High Throughput PCR Free 96 # same as UG100 +limber_multiplexing_ultima_ug200: + name: Limber Multiplexing Ultima UG200 + asset_type: Well + order: 2 + request_class_name: Request::Multiplexing + for_multiplexing: true + product_line_name: Ultima # same as UG100 + target_purpose_name: UPF2 EqVol Norm +ultima_ug200_sequencing: + name: Ultima UG200 sequencing + asset_type: LibraryTube + order: 2 + initial_state: pending + billable: true + product_line_name: Ultima # same as UG100 + request_class_name: UltimaUG200SequencingRequest + request_purpose: standard diff --git a/config/default_records/submission_templates/005_bioscan_submission_templates.yml b/config/default_records/submission_templates/005_bioscan_submission_templates.yml index 08234485f9..5c010597d4 100644 --- a/config/default_records/submission_templates/005_bioscan_submission_templates.yml +++ b/config/default_records/submission_templates/005_bioscan_submission_templates.yml @@ -12,6 +12,7 @@ Limber-Htp - Bioscan Lysate Prep: Limber-Htp - Bioscan Library Prep - Automated: name: "Limber-Htp - Bioscan Library Prep - Automated" submission_class_name: "AutomatedOrder" + automated: true related_records: request_type_keys: ["limber_bioscan_library_prep"] product_line_name: Bioscan diff --git a/config/default_records/submission_templates/016_bge.yml b/config/default_records/submission_templates/016_bge.yml index a417f7d879..e2bd8176df 100644 --- a/config/default_records/submission_templates/016_bge.yml +++ b/config/default_records/submission_templates/016_bge.yml @@ -16,6 +16,7 @@ Limber-Htp - BGE ISC: Limber-Htp - BGE Transition - Automated: name: "Limber-Htp - BGE Transition - Automated" submission_class_name: "AutomatedOrder" + automated: true related_records: product_line_name: Illumina-HTP product_catalogue_name: BGE diff --git a/config/default_records/submission_templates/019_ultima_submission_templates.yml b/config/default_records/submission_templates/019_ultima_submission_templates.yml index c6356c027f..53c6a698b4 100644 --- a/config/default_records/submission_templates/019_ultima_submission_templates.yml +++ b/config/default_records/submission_templates/019_ultima_submission_templates.yml @@ -9,6 +9,7 @@ Limber-Htp - Ultima PCR Free - Ultima sequencing: product_catalogue_name: GenericNoPCR Limber-Htp - Ultima PCR Free - Ultima sequencing Automated: submission_class_name: "LinearSubmission" + automated: true related_records: request_type_keys: ["ultima_sequencing"] order_role: PCR Free diff --git a/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml b/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.yml similarity index 90% rename from config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml rename to config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.yml index 824e90cde6..d1c4a6e313 100644 --- a/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml +++ b/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.yml @@ -6,7 +6,6 @@ Limber-Htp - LCM Triomics EMSeq: request_type_keys: ["limber_lcm_triomics_emseq"] product_line_name: LCM Triomics product_catalogue_name: LCM Triomics - project_name: "UAT Project" # LCM Triomics RNASeq submission template Limber-Htp - LCM Triomics RNASeq: @@ -15,4 +14,3 @@ Limber-Htp - LCM Triomics RNASeq: request_type_keys: ["limber_lcm_triomics_rnaseq"] product_line_name: LCM Triomics product_catalogue_name: LCM Triomics - project_name: "UAT Project" diff --git a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml new file mode 100644 index 0000000000..c1758e5c98 --- /dev/null +++ b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml @@ -0,0 +1,19 @@ +# This submission template is associated with the Ultima PCR Free Library preparation pipeline +# and the Ultima UG200 sequencing platform. +--- +Limber-Htp - Ultima UG200 PCR Free - Ultima UG200 sequencing: + submission_class_name: "LinearSubmission" + related_records: + request_type_keys: + ["limber_ultima_ug200_htp_pcr_free", "limber_multiplexing_ultima_ug200", "ultima_ug200_sequencing"] + order_role: PCR Free + product_line_name: Ultima # same as UG100 + product_catalogue_name: GenericNoPCR +Limber-Htp - Ultima UG200 PCR Free - Ultima UG200 sequencing Automated: + submission_class_name: "LinearSubmission" + automated: true + related_records: + request_type_keys: ["ultima_ug200_sequencing"] + order_role: PCR Free + product_line_name: Ultima # same as UG100 + product_catalogue_name: GenericNoPCR diff --git a/config/default_records/tag_groups/006_ultima_ug200.wip.yml b/config/default_records/tag_groups/006_ultima_ug200.wip.yml new file mode 100644 index 0000000000..e76b585f3b --- /dev/null +++ b/config/default_records/tag_groups/006_ultima_ug200.wip.yml @@ -0,0 +1,204 @@ +--- +# Ultima UG200 tag groups + +Ultima P3: + visible: true + adapter_type_name: Ultima + tags: + 1: CTGCACATTGTAGAT + 2: CTCATGGTATATGAT + 3: CTGCATGCTGGCGTGAT + 4: CGGCACGATGCTGAT + 5: CTCTTCATGAGCATGAT + 6: CTCATGGAGCATATGAT + 7: CATACTGCCTATGAT + 8: CAATGCATCTATATGAT + 9: CGAGCACAATGCATGAT + 10: CGGCAGCAGACTGAT + 11: CAACATGCACATCTGAT + 12: CGTGCATGGCATCTGAT + 13: CTACAGCAATGTGAT + 14: CATCAGTCCTGCGAT + 15: CAAGCGACATGCGAT + 16: CATGCTCAATATCTGAT + 17: CGACTCATGCCTGAT + 18: CATGGCATACTGCTGAT + 19: CAATCGCATCGTGAT + 20: CGGCACTGCAGTGAT + 21: CGGATCGCATGCGAT + 22: CTATTGCTCTGCATGAT + 23: CATCTGCGGCACATGAT + 24: CATCGATGCAGAATGAT + 25: CACATGACCAGCGAT + 26: CAATGCTGTAGTGAT + 27: CGACATGCACCTGAT + 28: CGCAAGCATGTCGAT + 29: CATGCTCGGCTGCTGAT + 30: CAAGTCTGCTCAGAT + 31: CATCATTGATCATCGAT + 32: CACTCACAATGCATGAT + 33: CATGTGGCTATCATGAT + 34: CATGTATGCGCAATGAT + 35: CTTGCGCTCTCAGAT + 36: CGCGCACAATATGAT + 37: CAGCGCACCAGCATGAT + 38: CTGCTATCTGGTGAT + 39: CGATATCATGGTGAT + 40: CATGCATGTGGATAGAT + 41: CTCTGCTCCTGCGAT + 42: CATCGCACCAGAGAT + 43: CAATCTGATATGATGAT + 44: CATACTGCAGGAGAT + 45: CTTCATGACAGAGAT + 46: CTACATCATGGCATGAT + 47: CGCTGTTGCATGATGAT + 48: CTTCACTCATGAGAT + 49: CATGCGCTTACAGAT + 50: CAGCATCTCTGCCTGAT + 51: CACTGCCATATCATGAT + 52: CTCAGCTGCGGCGAT + 53: CATGCAGAGCGCCTGAT + 54: CAATGCTATCGAGAT + 55: CAGATAAGAGCAGAT + 56: CAGAGCCACTCAGAT + 57: CATGTAATATCAGAT + 58: CAGCGCATGATCCTGAT + 59: CATAGATAGCCAGAT + 60: CACTAGCATGGCGAT + 61: CAATAGCATCGAGAT + 62: CTTCTGTGAGATGAT + 63: CTCTGCTCATTCATGAT + 64: CACAATAGCGATGAT + 65: CTGTAGGCATCTGAT + 66: CAACAGCTCTCTGAT + 67: CAGACACAATATGAT + 68: CTAGCAAGATCAGAT + 69: CTTGAGACATGAGAT + 70: CTCAGATGTCCAGAT + 71: CAGCCGCATGCACAGAT + 72: CTCGCATGTGGAGAT + 73: CACATGCATGTCCTGAT + 74: CGCGGCTGATATGAT + 75: CTGTGACAAGCTGAT + 76: CATGCGAGCATGGTGAT + 77: CAATACATGATCGAT + 78: CATGGACATATGCAGAT + 79: CAGCGCGCTGGCATGAT + 80: CATGCGTCACCTGAT + 81: CTGCATGTTGCTGAGAT + 82: CATCCTGCGATAGAT + 83: CAATATCTGAGTGAT + 84: CTGTGCAGGAGTGAT + 85: CGATGTGCCATAGAT + 86: CTCACATGGAGAGAT + 87: CAGAACATGTCAGAT + 88: CGCACAATGCGAGAT + 89: CATGCACAGAATGTGAT + 90: CGTATGGCAGCTGAT + 91: CTGATGGTGCTCATGAT + 92: CTCAGCCATGCGATGAT + 93: CATGCACAAGATCAGAT + 94: CTCTGTTGCTCAGAT + 95: CACAGCGCTCCAGAT + 96: CACAGAAGATGCGAT + +UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4): + visible: true + adapter_type_name: Ultima + tags: + 1: CATCATGCTCCGCTGAT + 2: CTTGCTATGAGCGAT + 3: CATGTATCAGGTGAT + 4: CATGCGCAATGTATGAT + 5: CATGTGAGCGGTGAT + 6: CGCACAAGTCATGAT + 7: CATGCAATACATGAGAT + 8: CATCTATCAGGCGAT + 9: CGAGCATCAGGTGAT + 10: CAAGACTGATATGAT + 11: CATGCGGAGTGAGAT + 12: CAGATATAGCCTGAT + 13: CAAGAGAGAGCTGAT + 14: CTATCAAGCTCAGAT + 15: CAGAATACATGCGAT + 16: CTCAAGCGCACAGAT + 17: CACACACAACATGAT + 18: CTTACATGTCATGAT + 19: CAGAAGAGATATGAT + 20: CAGAGCGCCGCAGAT + 21: CGCTCAACATGCGAT + 22: CATGACAGTAATGAT + 23: CATGCAATCACATCGAT + 24: CAGTTGATACATGAT + 25: CTGTGACATAATGAT + 26: CATAGTGCTGCAATGAT + 27: CACGAGCGCAATGAT + 28: CACAATGCAGATGTGAT + 29: CTGCGCAGCATCCTGAT + 30: CAAGACATCAGAGAT + 31: CATGCTTGTGTCATGAT + 32: CAGTGCGACAATGAT + 33: CATATGGTGACAGAT + 34: CAATGATAGTGTGAT + 35: CATAGCAGCAACATGAT + 36: CATGACACCAGTGAT + 37: CAATCTCAGCAGATGAT + 38: CTCGATGAGCCTGAT + 39: CAACACTCTGCAGAT + 40: CTTAGCATCACTGAT + 41: CTGTTGTGATCAGAT + 42: CAAGTGTGCGCAGAT + 43: CATGAGCATGGTCTGAT + 44: CACTGATGCATGGAGAT + 45: CTTATATCTCATGAT + 46: CATCAGCTGAAGATGAT + 47: CACACATCCAGCGAT + 48: CAACATGTGATCGAT + 49: CGGCTATGCATCGAT + 50: CAAGTGAGCTGCGAT + 51: CAATAGACAGCTGAT + 52: CAGTCTGATCCTGAT + 53: CTTGCATAGCGTGAT + 54: CTCGTGGCATGCATGAT + 55: CGCTGCCTGCTGCTGAT + 56: CATAGTTCATCTGAT + 57: CAATCAGCAGACGAT + 58: CATGAGCATGAGGCGAT + 59: CACATCAGTGGCGAT + 60: CTGCATGCACACCTGAT + 61: CTACTGGCAGCTGAT + 62: CTATTGATATGAGAT + 63: CTCATAGCCTGCATGAT + 64: CACATGCTCAATGCGAT + 65: CAACTCATATGCGAT + 66: CATGGCTGTCTCGAT + 67: CATCTAGCCATAGAT + 68: CAGTCAACAGCAGAT + 69: CTAGCGCAATGAGAT + 70: CATCATTAGCGCGAT + 71: CGCTTCATGCTGCAGAT + 72: CTGATGGCGCGCATGAT + 73: CTGATCTATGCAATGAT + 74: CACTGAATGTGCGAT + 75: CGCAATCTATCTGAT + 76: CGGCATGCTGTAGAT + 77: CTCATGTGTGGTGAT + 78: CTGCATATTGTGATGAT + 79: CGCATGTATGGCGAT + 80: CATATAACATGCATGAT + 81: CATATCTCCTGTGAT + 82: CTGATACAGAATGAT + 83: CTGTGCGCCATAGAT + 84: CAGAGCATGCTAATGAT + 85: CATAAGATGCGTGAT + 86: CATACAAGAGCTGAT + 87: CATGCTGAATGTGTGAT + 88: CTCATGATTACTGAT + 89: CATGACGCATTAGAT + 90: CGTGTGCAACATGAT + 91: CGGTGCATATCAGAT + 92: CTTCGATGCTGTGAT + 93: CAATAGATGTGAGAT + 94: CGATGCCATCTCATGAT + 95: CGATCACAAGCTGAT + 96: CTGTGATGTAATGAT diff --git a/config/default_records/tag_layout_templates/003_ultima_ug200.wip.yml b/config/default_records/tag_layout_templates/003_ultima_ug200.wip.yml new file mode 100644 index 0000000000..e2f1d0fb31 --- /dev/null +++ b/config/default_records/tag_layout_templates/003_ultima_ug200.wip.yml @@ -0,0 +1,11 @@ +--- +# Ultima UG200 tag layout templates + +Ultima P3: + :tag_group_name: Ultima P3 + :direction_algorithm: TagLayout::InColumns + :walking_algorithm: TagLayout::WalkWellsOfPlate +UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4): + :tag_group_name: UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4) + :direction_algorithm: TagLayout::InColumns + :walking_algorithm: TagLayout::WalkWellsOfPlate diff --git a/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml b/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml new file mode 100644 index 0000000000..a72c05029d --- /dev/null +++ b/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml @@ -0,0 +1,14 @@ +--- +# Section names must be unique inside the record loader folder. +Opentrons UG200: + name: Opentrons + workflow: Ultima UG200 + sorted: 0 + lab_activity: true + sti_type: SetDescriptorsTask +Amp UG200: + name: Amp + workflow: Ultima UG200 + sorted: 1 + lab_activity: true + sti_type: SetDescriptorsTask diff --git a/config/default_records/tube_purposes/011_ultima_ug200_tube_purposes.wip.yml b/config/default_records/tube_purposes/011_ultima_ug200_tube_purposes.wip.yml new file mode 100644 index 0000000000..3cb41b6b30 --- /dev/null +++ b/config/default_records/tube_purposes/011_ultima_ug200_tube_purposes.wip.yml @@ -0,0 +1,6 @@ +--- +UPF2 EqVol Norm: + # Even though this is Ultima, not Illumina, the MX tube has the same behaviour + type: IlluminaHtp::MxTubePurpose + target_type: MultiplexedLibraryTube + stock_plate: false diff --git a/config/environment.rb b/config/environment.rb index 12ea62f886..cac5315775 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,6 +1,5 @@ -# frozen_string_literal: true # Load the Rails application. -require_relative 'application' +require_relative "application" # Initialize the Rails application. Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index 060adcff1c..05b2c161ce 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,5 +1,4 @@ -# frozen_string_literal: true -require 'active_support/core_ext/integer/time' +require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -25,14 +24,14 @@ # Enable server timing. config.server_timing = true - # Enable/disable caching. By default caching is disabled. - # Run rails dev:cache to toggle caching. - if Rails.root.join('tmp/caching-dev.txt').exist? + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store - config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false @@ -45,11 +44,11 @@ # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false - # Disable caching for Action Mailer templates even if Action Controller - # caching is enabled. + # Make template changes take effect immediately. config.action_mailer.perform_caching = false - config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log @@ -76,6 +75,9 @@ # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true @@ -110,7 +112,7 @@ config.action_controller.allow_forgery_protection = false # Raise error when a before_action's only/except options reference missing actions. - config.action_controller.raise_on_missing_callback_actions = false + config.action_controller.raise_on_missing_callback_actions = true # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. # config.generators.apply_rubocop_autocorrect_after_generate! diff --git a/config/environments/test.rb b/config/environments/test.rb index 36b27692b9..17adc64030 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,6 +1,3 @@ -# frozen_string_literal: true -require 'active_support/core_ext/integer/time' - # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped @@ -22,15 +19,13 @@ # this is usually not necessary, and can slow down your test suite. However, it's # recommended that you enable it in continuous integration systems to ensure eager # loading is working properly before deploying your code. - config.eager_load = ENV['CI'].present? + config.eager_load = ENV["CI"].present? - # Configure public file server for tests with Cache-Control for performance. - config.public_file_server.enabled = true - config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{1.hour.to_i}" } + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } # Show full error reports and disable caching. config.consider_all_requests_local = true - config.action_controller.perform_caching = false config.cache_store = :null_store # Render exception templates for rescuable exceptions and raise for other exceptions. @@ -45,18 +40,13 @@ # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test - # Disable caching for Action Mailer templates even if Action Controller - # caching is enabled. - config.action_mailer.perform_caching = false - # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test - # Unlike controllers, the mailer instance doesn't have any context about the - # incoming request so you'll need to provide the :host parameter yourself. - config.action_mailer.default_url_options = { host: 'www.example.com' } + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr @@ -74,7 +64,7 @@ # config.action_view.annotate_rendered_view_with_filenames = true # Raise error when a before_action's only/except options reference missing actions. - # config.action_controller.raise_on_missing_callback_actions = true + config.action_controller.raise_on_missing_callback_actions = true # disable UI animations to avoid potential test failures config.disable_animations = true diff --git a/config/feature_flags.yml b/config/feature_flags.yml index 0132f40714..f03307dba8 100644 --- a/config/feature_flags.yml +++ b/config/feature_flags.yml @@ -9,6 +9,8 @@ y24_052_enable_data_release_timing_validation: Enables server-side validation th y25_442_make_api_key_mandatory: Makes API key mandatory for all API requests +y25_105_validate_descriptor_required_field: Enables validation of batch workflow task descriptor required field + # Accessioning y25_706_enable_accessioning: Enables the accessioning feature in the application y25_714_skip_accessioning_tag_validation: Skips internal validation of accessioning tags prior to sample accessioning diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index cbb205de02..1a4c755430 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -1,57 +1,39 @@ -# frozen_string_literal: true - # Be sure to restart your server when you modify this file. -# Define an application-wide content security policy -# For further information see the following documentation -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy - -Rails.application.config.content_security_policy do |policy| - # policy.default_src :self, :https - # policy.font_src :self, :https, :data - # policy.img_src :self, :https, :data - # policy.object_src :none - # policy.script_src :self, :https - # policy.style_src :self, :https - # Specify URI for violation reports - # policy.report_uri "/csp-violation-report-endpoint" +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header - # Snippet provided after running - # `bundle exec rails webpacker:install:vue` - # > You need to enable unsafe-eval rule. - # > This can be done in Rails 5.2+ for development environment in the CSP initializer - # > config/initializers/content_security_policy.rb with a snippet like this: - if Rails.env.development? - # Also allow @vite/client to hot reload javascript changes in development - policy.script_src :self, :https, :unsafe_eval, "http://#{ViteRuby.config.host_with_port}" - else - policy.script_src :self, :https - end +Rails.application.configure do + config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https + # Allow @vite/client to hot reload javascript changes in development + policy.script_src *policy.script_src, :unsafe_eval, "http://#{ ViteRuby.config.host_with_port }" if Rails.env.development? # You may need to enable this in production as well depending on your setup. - policy.script_src(*policy.script_src, :blob) if Rails.env.test? + policy.script_src *policy.script_src, :blob if Rails.env.test? - # policy.style_src :self, :https +# policy.style_src :self, :https # Allow @vite/client to hot reload style changes in development - policy.style_src(:self, :https, :unsafe_inline) if Rails.env.development? - - # Allow @vite/client to hot reload changes in development - policy.connect_src(:self, "ws://#{ViteRuby.config.host_with_port}") if Rails.env.development? + policy.style_src *policy.style_src, :unsafe_inline if Rails.env.development? - # # Specify URI for violation reports - # # policy.report_uri "/csp-violation-report-endpoint" +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" + end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. + config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } + config.content_security_policy_nonce_directives = %w(script-src style-src) + + # Report CSP violations to a specified URI + # For further information see the following documentation: + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only + # Report only for now because we have some inline JS that can't use nonce values e.g. inline onclick event handlers + # (see ajax_handling.js for an example) + # Report violations without enforcing the policy. + config.content_security_policy_report_only = true end - -# If you are using UJS then enable automatic nonce generation -Rails.application.config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) } -Rails.application.config.content_security_policy_nonce_directives = %w[script-src] - -# Set the nonce only to specific directives -# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) - -# Report CSP violations to a specified URI -# For further information see the following documentation: -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only -# Report only for now because we have some inline JS that can't use nonce values e.g. inline onclick event handlers -# (see ajax_handling.js for an example) -Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 5a36c5322d..0520f05a65 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,10 +1,12 @@ -# frozen_string_literal: true - # Be sure to restart your server when you modify this file. # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. -Rails.application.config.filter_parameters += %i[ - passw email secret token _key crypt salt certificate otp ssn +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] +# Sequencescape specific parameters to filter +Rails.application.config.filter_parameters += [ + :uploaded_data ] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 55a6a2b9bd..d3ad112180 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections @@ -18,9 +16,10 @@ inflect.acronym 'ENA' # European Nucleotide Archive inflect.acronym 'HTTP' # HyperText Transfer Protocol inflect.acronym 'EBI' # European Bioinformatics Institute + inflect.acronym 'UG200' # Ultima UG200 end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' +# inflect.acronym "RESTful" # end diff --git a/config/locales/metadata/en.yml b/config/locales/metadata/en.yml index 08f1fbd67e..da8fa0683b 100644 --- a/config/locales/metadata/en.yml +++ b/config/locales/metadata/en.yml @@ -79,6 +79,9 @@ en: ot_recipe: label: OT Recipe + wafer_size: + label: Wafer Size + library_creation_request: <<: *REQUEST sequencing_request: @@ -107,6 +110,9 @@ en: ultima_sequencing_request: <<: *REQUEST + ultima_ug200_sequencing_request: + <<: *REQUEST + pulldown: requests: wgs_library_request: diff --git a/config/puma.rb b/config/puma.rb index ce21fcba9d..4895e74ea6 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,15 +1,17 @@ -# frozen_string_literal: true - # This configuration file will be evaluated by Puma. The top-level methods that # are invoked here are part of Puma's configuration DSL. For more information # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. - +# # Puma starts a configurable number of processes (workers) and each process # serves each request in a thread from an internal thread pool. # +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# # The ideal number of threads per worker depends both on how much time the # application spends waiting for IO operations and on how much you wish to -# to prioritize throughput over latency. +# prioritize throughput over latency. # # As a rule of thumb, increasing the number of threads will increase how much # traffic a given process can handle (throughput), but due to CRuby's @@ -22,37 +24,15 @@ # Any libraries that use a connection pool or another resource pool should # be configured to provide at least as many connections as the number of # threads. This includes Active Record's `pool` parameter in `database.yml`. -threads_count = ENV.fetch('RAILS_MAX_THREADS', 3) +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. -port ENV.fetch('PORT', 3000) +port ENV.fetch("PORT", 3000) # Specifies the `environment` that Puma will run in. environment ENV.fetch('RAILS_ENV', 'development') -# Specifies the number of `workers` to boot in clustered mode. -# Workers are forked webserver processes. If using threads and workers together -# the concurrency of the application would be max `threads` * `workers`. -# Workers do not work on JRuby or Windows (both of which do not support -# processes). -# -# workers ENV.fetch("WEB_CONCURRENCY") { 2 } - -# Use the `preload_app!` method when specifying a `workers` number. -# This directive tells Puma to first boot the application and load code -# before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. -# -# preload_app! - -# The code in the `before_worker_boot` will be called if you are using -# clustered mode by specifying a number of `workers`. After each worker -# process is booted this block will be run, if you are using `preload_app!` -# option you will want to use this block to reconnect to any threads -# or connections that may have been created at application boot, Ruby -# cannot share connections between processes. -# # The `before_fork` and `before_worker_boot` blocks have been disabled as # are only used when using clustered mode (i.e. workers > 1) and we are # currently only using a single worker. @@ -64,9 +44,12 @@ # Warren.handler.connect # end -# Allow puma to be restarted by `rails restart` command. +# Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart +# Run the Solid Queue supervisor inside of Puma for single-server deployments +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + # Specify the PID file. Defaults to tmp/pids/server.pid in development. # In other environments, only set the PID file if requested. -pidfile ENV['PIDFILE'] if ENV['PIDFILE'] +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/routes.rb b/config/routes.rb index e176e1843a..db4beaa56d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -438,7 +438,7 @@ member { get :show } end - resources :pipelines, except: [:delete] do + resources :pipelines, except: [:destroy] do collection { post :update_priority } member do get :reception @@ -485,7 +485,7 @@ resources :asset_audits - resources :qc_reports, except: %i[delete update] do + resources :qc_reports, except: %i[destroy update] do collection { post :qc_file } end diff --git a/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb new file mode 100644 index 0000000000..f1e811d8f0 --- /dev/null +++ b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# This migration adds a wafer_size column to the request_metadata table, which is used to store +# the wafer size for Ultima sequencing requests. This is stored as a string, with possible +# values of 5TB, 10TB, and 20TB at time of writing. +class AddWaferSizeToRequestMetadata < ActiveRecord::Migration[7.1] + def change + add_column :request_metadata, :wafer_size, :string + end +end diff --git a/db/migrate/20260410121704_add_automated_submission_template_column.rb b/db/migrate/20260410121704_add_automated_submission_template_column.rb new file mode 100644 index 0000000000..924a69e893 --- /dev/null +++ b/db/migrate/20260410121704_add_automated_submission_template_column.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Adds an 'automated' column to submission templates to differentiate templates that are used for automated submissions. +class AddAutomatedSubmissionTemplateColumn < ActiveRecord::Migration[8.0] + def change + add_column :submission_templates, :automated, :boolean, default: false, null: false + + # Automated submission templates at the time of this migration + existing_automated_template_names = [ + 'Limber-Htp - Bioscan Library Prep - Automated', + 'Limber-Htp - BGE Transition - Automated', + 'Limber-Htp - Ultima PCR Free - Ultima sequencing Automated', + 'Limber - Heron LTHR - Automated', + 'Limber - Heron LTHR V2 - Automated' + ] + + SubmissionTemplate.where(name: existing_automated_template_names).find_each do |template| + template.update!(automated: true) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 34cce1cefe..c5d4afd060 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_03_17_142326) do +ActiveRecord::Schema[8.0].define(version: 2026_04_10_121704) do create_table "accession_sample_statuses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.integer "sample_id", null: false t.string "status", null: false @@ -1191,6 +1191,7 @@ t.boolean "low_diversity" t.integer "percent_phix_requested" t.integer "ot_recipe" + t.string "wafer_size" t.index ["request_id"], name: "index_request_metadata_on_request_id" end @@ -1635,6 +1636,7 @@ t.integer "superceded_by_id", default: -1, null: false t.datetime "superceded_at", precision: nil t.integer "product_catalogue_id" + t.boolean "automated", default: false, null: false t.index ["name", "superceded_by_id"], name: "name_and_superceded_by_unique_idx", unique: true t.index ["product_catalogue_id"], name: "fk_submission_templates_to_product_catalogues" end diff --git a/lib/lab_where_client.rb b/lib/lab_where_client.rb index 9638bcfa13..2e04600a4c 100644 --- a/lib/lab_where_client.rb +++ b/lib/lab_where_client.rb @@ -26,13 +26,13 @@ def parse_json(str) def get(instance, target) parse_json(RestClient.get(path_to(instance, target))) - rescue Errno::ECONNREFUSED, RestClient::NotFound => e + rescue Errno::ECONNREFUSED, RestClient::NotFound, RestClient::RequestTimeout => e raise LabwhereException.new(e), 'LabWhere service is down', e.backtrace end def post(instance, target, payload) parse_json(RestClient.post(path_to(instance, target), payload)) - rescue Errno::ECONNREFUSED, RestClient::NotFound => e + rescue Errno::ECONNREFUSED, RestClient::NotFound, RestClient::RequestTimeout => e raise LabwhereException.new(e), 'LabWhere service is down', e.backtrace rescue RestClient::UnprocessableEntity => e parse_json(e.response) @@ -40,7 +40,7 @@ def post(instance, target, payload) def put(instance, target, payload) parse_json(RestClient.put(path_to(instance, target), payload)) - rescue Errno::ECONNREFUSED, RestClient::NotFound => e + rescue Errno::ECONNREFUSED, RestClient::NotFound, RestClient::RequestTimeout => e raise LabwhereException.new(e), 'LabWhere service is down', e.backtrace end end diff --git a/lib/tasks/limber.rake b/lib/tasks/limber.rake index c161af0d33..dcdf45058e 100644 --- a/lib/tasks/limber.rake +++ b/lib/tasks/limber.rake @@ -206,7 +206,8 @@ namespace :limber do project_id: Limber::Helper.find_project('Project Heron').id }, product_line: ProductLine.find_by!(name: 'Illumina-HTP'), - product_catalogue: ProductCatalogue.find_by!(name: 'Generic') + product_catalogue: ProductCatalogue.find_by!(name: 'Generic'), + automated: true ) end @@ -222,7 +223,8 @@ namespace :limber do project_id: Limber::Helper.find_project('Project Heron').id }, product_line: ProductLine.find_by!(name: 'Illumina-HTP'), - product_catalogue: ProductCatalogue.find_by!(name: 'Generic') + product_catalogue: ProductCatalogue.find_by!(name: 'Generic'), + automated: true ) end diff --git a/package.json b/package.json index 2dd6ac2930..cd6430722a 100644 --- a/package.json +++ b/package.json @@ -22,15 +22,15 @@ "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^14.1.0", "postcss-preset-env": "^7.8.3", - "sass": "^1.98.0", + "sass": "^1.99.0", "select2": "^4.1.0-rc.0", "sortablejs": "^1.15.7", "terser": "^5.46.1" }, "devDependencies": { - "@vitejs/plugin-legacy": "^1.8", + "@vitejs/plugin-legacy": "^5.0", "eslint": "^9.19", - "eslint-config-prettier": "^8.10", + "eslint-config-prettier": "^9.0", "globals": "^15.14", "jsdom": "^26.0", "prettier": "^3.3", diff --git a/spec/controllers/api/v2/order_processor_spec.rb b/spec/controllers/api/v2/order_processor_spec.rb new file mode 100644 index 0000000000..bf50fef63d --- /dev/null +++ b/spec/controllers/api/v2/order_processor_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'rails_helper' +require_dependency Rails.root.join('app/controllers/api/v2/orders_controller').to_s + +RSpec.describe Api::V2::OrderProcessor do + describe '#find_project' do + subject(:find_project) { processor.send(:find_project) } + + let(:processor) { described_class.allocate } + let(:project_uuid) { nil } + let(:params) do + ActionController::Parameters.new( + data: { + attributes: { + project_uuid: + } + } + ) + end + + before do + allow(processor).to receive(:params).and_return(params) + end + + context 'when project_uuid is not provided' do + it 'returns nil' do + expect(find_project).to be_nil + end + end + + context 'when project_uuid does not exist' do + let(:project_uuid) { 'not-a-valid-uuid' } + + it 'raises an invalid field value error' do + expect { find_project }.to raise_error(JSONAPI::Exceptions::InvalidFieldValue) + end + end + + context 'when project_uuid exists' do + let(:project) { create(:project) } + let(:project_uuid) { project.uuid } + + it 'returns the project' do + expect(find_project).to eq(project) + end + end + end +end diff --git a/spec/controllers/batches_controller_spec.rb b/spec/controllers/batches_controller_spec.rb index 9dae4f9a15..56a4a24071 100644 --- a/spec/controllers/batches_controller_spec.rb +++ b/spec/controllers/batches_controller_spec.rb @@ -3,28 +3,52 @@ require 'rails_helper' RSpec.describe BatchesController do + let(:zip_data) { 'FAKE ZIP DATA' } + let(:run_manifest_filename) { "attachment; filename=\"batch_#{batch.id}_run_manifest.zip\"" } + + shared_examples 'returns a run manifest zip file' do + it 'returns a zip file with the correct filename and content', :aggregate_failures do + expect(response.content_type).to eq('application/zip') + expect(response.headers['Content-Disposition']).to include(run_manifest_filename) + expect(response.body).to eq(zip_data) + expect(response).to have_http_status(:ok) + end + end + describe '#generate_ultima_sample_sheet' do let(:current_user) { create(:user) } let(:pipeline) { create(:ultima_sequencing_pipeline) } let(:batch) { create(:batch, pipeline:) } - let(:zip_data) { 'FAKE ZIP DATA' } - - # Expected Content-Disposition header value - let(:content_disposition) do - "attachment; filename=\"batch_#{batch.id}_run_manifest.zip\"" - end before do allow(UltimaSampleSheet::SampleSheetGenerator).to receive(:generate).with(batch).and_return(zip_data) end - shared_examples 'returns a zip file' do - it 'returns a zip file with the correct filename' do # rubocop:disable RSpec/MultipleExpectations - expect(response.content_type).to eq('application/zip') - expect(response.headers['Content-Disposition']).to include(content_disposition) - expect(response.body).to eq(zip_data) - expect(response).to have_http_status(:ok) + context 'when downloading with login' do + before do + get :generate_sample_sheet, params: { id: batch.id }, session: { user: current_user.id } end + + it_behaves_like 'returns a run manifest zip file' + end + + context 'when downloading wihout login' do + # Test: be able to download the file through an url without auth. (Request from NPG team) + before do + get :generate_sample_sheet, params: { id: batch.id } + end + + it_behaves_like 'returns a run manifest zip file' + end + end + + describe '#generate_ultima_ug200_sample_sheet' do + let(:current_user) { create(:user) } + let(:pipeline) { create(:ultima_ug200_sequencing_pipeline) } + let(:batch) { create(:batch, pipeline:) } + + before do + allow(UltimaSampleSheet::UG200SampleSheetGenerator).to receive(:generate).with(batch).and_return(zip_data) end context 'when downloading with login' do @@ -32,16 +56,15 @@ get :generate_sample_sheet, params: { id: batch.id }, session: { user: current_user.id } end - it_behaves_like 'returns a zip file' + it_behaves_like 'returns a run manifest zip file' end - context 'when downloading wihout login' do - # Test: be able to download the file through an url without auth. (Request from NPG team) + context 'when downloading without login' do before do get :generate_sample_sheet, params: { id: batch.id } end - it_behaves_like 'returns a zip file' + it_behaves_like 'returns a run manifest zip file' end end end diff --git a/spec/controllers/npg_actions/assets_controller_spec.rb b/spec/controllers/npg_actions/assets_controller_spec.rb index 31d863944c..c44459ba04 100644 --- a/spec/controllers/npg_actions/assets_controller_spec.rb +++ b/spec/controllers/npg_actions/assets_controller_spec.rb @@ -336,7 +336,9 @@ it 'renders the exception page' do regexp = Regexp.new( - ['', 'param is missing or the value is empty: qc_information', ''].join, + ['', + 'param is missing or the value is empty or invalid: qc_information', + ''].join, Regexp::MULTILINE ) expect(response).to have_http_status(:bad_request) diff --git a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb index 923a12a490..5d22248184 100644 --- a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb +++ b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb @@ -219,7 +219,7 @@ def map_description(map_id) let(:csv2_samples) { csv_samples_for(request2) } it 'generates header sections' do # rubocop:disable RSpec/MultipleExpectations - expect(csv1[0].compact_blank).to eq(generator.class::HEADER_TITLE) + expect(csv1[0].compact_blank).to eq(generator.header_title_config) expect(csv1[1].compact_blank).to eq(["Batch #{batch.id} #{tube1.human_barcode}"]) # First CSV expect(csv1[2].compact_blank).to eq([]) expect(csv2[1].compact_blank).to eq(["Batch #{batch.id} #{tube2.human_barcode}"]) # Second CSV @@ -228,15 +228,15 @@ def map_description(map_id) it 'generates global sections' do # Test: Add the following hardcoded values, Application(WGS native gDNA), # sequencing_recipe(UG_116cycles_Baseline_1.8.5.2) and analysis_recipe(wgs1) - expect(csv1[3].compact_blank).to eq(generator.class::GLOBAL_TITLE) - expect(csv1[4].compact_blank).to eq(generator.class::GLOBAL_HEADERS) + expect(csv1[3].compact_blank).to eq(generator.global_title_config) + expect(csv1[4].compact_blank).to eq(generator.global_headers_config) expect(csv1[5].compact_blank).to eq(['WGS native gDNA', 'UG_116cycles_Baseline_1.8.5.2', 'wgs1']) expect(csv1[6].compact_blank).to eq([]) end it 'generates samples sections' do - expect(csv1[7].compact_blank).to eq(generator.class::SAMPLES_TITLE) - expect(csv1[8].compact_blank).to eq(generator.class::SAMPLES_HEADERS) + expect(csv1[7].compact_blank).to eq(generator.samples_title_config) + expect(csv1[8].compact_blank).to eq(generator.samples_headers_config) expect(csv1[9..]).to eq(csv1_samples) # First CSV expect(csv2[9..]).to eq(csv2_samples) # Second CSV end diff --git a/spec/controllers/ultima_sample_sheet/ug200_sample_sheet_generator_spec.rb b/spec/controllers/ultima_sample_sheet/ug200_sample_sheet_generator_spec.rb new file mode 100644 index 0000000000..be36e64150 --- /dev/null +++ b/spec/controllers/ultima_sample_sheet/ug200_sample_sheet_generator_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UltimaSampleSheet::UG200SampleSheetGenerator do + # First oligo sequences for the two UG200 tag groups. + let(:plate3_first_oligo) { 'CTGCACATTGTAGAT' } # Z0193 + let(:plate4_first_oligo) { 'CATCATGCTCCGCTGAT' } # Z0289 + let(:tag_group3_name) { 'Ultima P3' } + let(:tag_group4_name) { 'UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4)' } + + # Eagerly create tag groups and tags to get consistent IDs. + let!(:tag_group3) do + create(:tag_group_with_tags, tag_count: 96, name: tag_group3_name).tap do |tg| + # To test Z0193 matching with the oligo sequence. + tg.tags.first.update!(oligo: plate3_first_oligo) + end + end + + let!(:tag_group4) do + create(:tag_group_with_tags, tag_count: 96, name: tag_group4_name).tap do |tg| + # To test Z0289 matching with the oligo sequence. + tg.tags.first.update!(oligo: plate4_first_oligo) + end + end + let(:tag_groups) { [tag_group3, tag_group4] } + + let(:request_type) { create(:ultima_ug200_sequencing) } + let(:pipeline) { create(:ultima_ug200_sequencing_pipeline, request_types: [request_type]) } + let(:batch) { create(:ultima_sequencing_batch, pipeline:, requests:) } + let(:requests) { [request1, request2] } + let(:request1) { create(:ultima_sequencing_request, asset: tube1.receptacle, request_type: request_type) } + let(:request2) { create(:ultima_sequencing_request, asset: tube2.receptacle, request_type: request_type) } + + # Eagerly create tubes with aliquots to get consistent IDs. + let!(:tube1) do + receptacle = create(:receptacle) + create(:aliquot, tag: tag_group3.tags.first, receptacle: receptacle) + tube = create(:multiplexed_library_tube, receptacle:) + create(:event, content: Time.zone.today.to_s, message: 'scanned in', family: 'scanned_into_lab', eventful: tube) + tube + end + + let!(:tube2) do + receptacle = create(:receptacle) + create(:aliquot, tag: tag_group4.tags.first, receptacle: receptacle) + tube = create(:multiplexed_library_tube, receptacle:) + create(:event, content: Time.zone.today.to_s, message: 'scanned in', family: 'scanned_into_lab', eventful: tube) + tube + end + + # Expected mapping of tag groups to their respective 1-based plate numbers. + let(:tag_group_index_map) { { tag_group3 => 3, tag_group4 => 4 } } + + # Expected mapping of tags to their respective 1-based index numbers. + # Mirrors the generator's tag_index_map: config-value-based offsets. + let(:tag_index_map) do + tag_groups.each_with_object({}) do |tg, map| + start_index = generator.ultima_tag_groups_config[tg.name][:z_start] + tg.tags.sort_by(&:map_id).each_with_index { |tag, i| map[tag] = start_index + i } + end + end + + context 'with csv output' do + subject(:generator) { described_class::Generator.new(batch) } + + # Parse the generated CSV for the tubes into rows and columns. + let(:csv1) { CSV.parse(generator.csv_string(request1), row_sep: "\r\n", nil_value: '') } + let(:csv2) { CSV.parse(generator.csv_string(request2), row_sep: "\r\n", nil_value: '') } + + it 'generates UG200 global section', :aggregate_failures do + expect(generator.global_headers_config).to eq(['Application']) + expect(csv1[3].compact_blank).to eq(generator.global_title_config) + expect(csv1[4].compact_blank).to eq(['Application']) + expect(csv1[5].compact_blank).to eq(['WGS Native']) + end + + it 'uses UG200 tag group mapping config' do + expect(generator.ultima_tag_groups_config.slice(tag_group3_name, tag_group4_name)).to eq( + tag_group3_name => { plate_num: 3, z_start: 193 }, + tag_group4_name => { plate_num: 4, z_start: 289 } + ) + end + + it 'maps the first plate sample indexes and plate number', :aggregate_failures do + expect(csv1[9][2]).to eq('Z0193') + expect(csv1[9][3]).to eq(plate3_first_oligo) + expect(csv1[9][4]).to eq(tag_group_index_map[tag_group3].to_s) + expect(csv1[9][2]).to eq(format('Z%04d', tag_index_map[tag_group3.tags.first])) + end + + it 'maps the second plate sample indexes and plate number', :aggregate_failures do + expect(csv2[9][2]).to eq('Z0289') + expect(csv2[9][3]).to eq(plate4_first_oligo) + expect(csv2[9][4]).to eq(tag_group_index_map[tag_group4].to_s) + expect(csv2[9][2]).to eq(format('Z%04d', tag_index_map[tag_group4.tags.first])) + end + end +end diff --git a/spec/data/bulk_submission_excel/columns.yml b/spec/data/bulk_submission_excel/columns.yml index 03280783ee..ab5bd8739e 100644 --- a/spec/data/bulk_submission_excel/columns.yml +++ b/spec/data/bulk_submission_excel/columns.yml @@ -315,3 +315,22 @@ ot_recipe: range_name: :ot_recipe conditional_formattings: empty_cell: +wafer_size: + heading: "Wafer Size" + unlocked: true + attribute: :wafer_size + validation: + options: + type: :list + formula1: "$A$1:$A$2" + allowBlank: false + showInputMessage: true + showErrorMessage: true + errorStyle: :stop + errorTitle: "Wafer Size" + error: "You must enter a value from the list provided." + promptTitle: "Wafer Size" + prompt: "Select a value type from the approved list" + range_name: :wafer_size + conditional_formattings: + empty_cell: diff --git a/spec/data/bulk_submission_excel/ranges.yml b/spec/data/bulk_submission_excel/ranges.yml index a1c091015a..b211a116cb 100644 --- a/spec/data/bulk_submission_excel/ranges.yml +++ b/spec/data/bulk_submission_excel/ranges.yml @@ -27,3 +27,8 @@ ot_recipe: options: - "Free" - "Flex" +wafer_size: + options: + - "5TB" + - "10TB" + - "20TB" diff --git a/spec/factories/batch_factories.rb b/spec/factories/batch_factories.rb index 6221feffc5..86da9c7ea7 100644 --- a/spec/factories/batch_factories.rb +++ b/spec/factories/batch_factories.rb @@ -40,6 +40,10 @@ pipeline factory: %i[ultima_sequencing_pipeline] end + factory :ultima_ug200_sequencing_batch do + pipeline factory: %i[ultima_ug200_sequencing_pipeline] + end + factory :cherrypick_batch do transient do request_count { 1 } # We create one request by default as cherrypick pipelines have a minimum batch size diff --git a/spec/factories/pipelines_factories.rb b/spec/factories/pipelines_factories.rb index e3b150671b..880f8bcc6a 100644 --- a/spec/factories/pipelines_factories.rb +++ b/spec/factories/pipelines_factories.rb @@ -187,6 +187,18 @@ end end + factory :ultima_ug200_sequencing_pipeline do + name { generate(:pipeline_name) } + active { true } + + workflow { build(:lab_workflow_for_pipeline) } + + after(:build) do |pipeline| + pipeline.request_types << create(:ultima_ug200_sequencing) + pipeline.add_control_request_type + end + end + factory :library_completion, class: 'IlluminaHtp::Requests::LibraryCompletion' do request_type do create( diff --git a/spec/factories/plate_factories.rb b/spec/factories/plate_factories.rb index 23f9268e58..08f820fbd0 100644 --- a/spec/factories/plate_factories.rb +++ b/spec/factories/plate_factories.rb @@ -24,7 +24,7 @@ studies_cycle { studies.cycle } # Allow us to rotate through listed studies when building out wells projects_cycle { projects.cycle } # Allow us to rotate through listed studies when building out wells well_locations { maps.where(well_order => occupied_well_index) } - occupied_well_index { (0...well_count) } + occupied_well_index { 0...well_count } end after(:build) do |plate, evaluator| diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index 5a012985fc..ad180e5d76 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -124,6 +124,20 @@ end end + factory(:ultima_ug200_sequencing_request) do + request_type factory: %i[ultima_ug200_sequencing] + request_purpose { :standard } + sti_type { 'UltimaUG200SequencingRequest' } + request_metadata_attributes do + { + fragment_size_required_from: 150, + fragment_size_required_to: 400, + ot_recipe: 'Free', + wafer_size: '10TB' + } + end + end + factory(:library_creation_request, parent: :request, class: 'LibraryCreationRequest') do asset factory: %i[sample_tube] request_type factory: %i[library_creation_request_type] diff --git a/spec/factories/request_type_factories.rb b/spec/factories/request_type_factories.rb index 4d18e1eb3d..87b2fecfd4 100644 --- a/spec/factories/request_type_factories.rb +++ b/spec/factories/request_type_factories.rb @@ -139,6 +139,11 @@ request_class { UltimaSequencingRequest } end + factory :ultima_ug200_sequencing do + asset_type { 'LibraryTube' } + request_class { UltimaUG200SequencingRequest } + end + factory :miseq_sequencing_request_type do request_class { MiSeqSequencingRequest } asset_type { 'LibraryTube' } diff --git a/spec/helpers/submissions_helper_spec.rb b/spec/helpers/submissions_helper_spec.rb new file mode 100644 index 0000000000..5e5101c42c --- /dev/null +++ b/spec/helpers/submissions_helper_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe SubmissionsHelper do + describe '#field_input_tag' do + let(:field_info) do + double( + kind: 'Selection', + key: :ot_recipe, + selection: %w[Free Flex], + default_value: 'Free', + required: true + ) + end + + let(:wafer_size_field_info) do + double( + kind: 'Selection', + key: :wafer_size, + selection: %w[5TB 10TB], + default_value: '10TB', + required: true + ) + end + + it 'uses the default value for selection fields when no request option is set' do + html = helper.field_input_tag(field_info, values: {}, name_format: 'submission[order_params][%s]') + + expect(html).to include('') + end + + it 'uses the default value for selection fields when request option is blank' do + html = helper.field_input_tag(field_info, values: { ot_recipe: '' }, name_format: 'submission[order_params][%s]') + + expect(html).to include('') + end + + it 'uses the default wafer_size when request option is blank' do + html = helper.field_input_tag(wafer_size_field_info, + values: { wafer_size: '' }, + name_format: 'submission[order_params][%s]') + + expect(html).to include('') + end + end +end diff --git a/spec/lib/lab_where_client_spec.rb b/spec/lib/lab_where_client_spec.rb index 4f4a40b680..436312d388 100644 --- a/spec/lib/lab_where_client_spec.rb +++ b/spec/lib/lab_where_client_spec.rb @@ -4,6 +4,10 @@ RSpec.describe LabWhereClient do describe LabWhereClient::Scan do + before { configatron.labwhere_api = 'https://labwhere.example.com/api' } + # Reset the configatron value after the test to avoid affecting other tests + after { configatron.labwhere_api = nil } + let(:scan_params) { { 'message' => 'Scan successful', 'errors' => nil } } let(:scan) { described_class.new(scan_params) } @@ -41,10 +45,26 @@ ) end - it 'raises an error when Labwhere is down' do - labwhere = instance_double(LabWhereClient::LabWhere) - allow(LabWhereClient::LabWhere).to receive(:new).and_return(labwhere) - allow(labwhere).to receive(:post).and_raise(LabWhereClient::LabwhereException.new, 'LabWhere service is down') + it 'propagates labwhere errors when receieving unprocessible entity errors' do + error_response = RestClient::UnprocessableEntity.new + allow(error_response).to receive(:response).and_return({ errors: 'Invalid data' }.to_json) + allow(RestClient).to receive(:post).and_raise(error_response) + + scan = described_class.create(location_barcode: '123', user_code: '456', labware_barcodes: ['789']) + expect(scan.valid?).to be false + expect(scan.errors).to eq('Invalid data') + end + + it 'raises an error when Labwhere is unreachable' do + allow(RestClient).to receive(:post).and_raise(Errno::ECONNREFUSED) + + expect do + described_class.create(location_barcode: '123', user_code: '456', labware_barcodes: ['789']) + end.to raise_error(LabWhereClient::LabwhereException, 'LabWhere service is down') + end + + it 'raises an error on other rest client error types' do + allow(RestClient).to receive(:post).and_raise(RestClient::Exceptions::OpenTimeout) expect do described_class.create(location_barcode: '123', user_code: '456', labware_barcodes: ['789']) diff --git a/spec/models/cherrypick_task/control_locator_spec.rb b/spec/models/cherrypick_task/control_locator_spec.rb index 575e43a1eb..4e65fbd16e 100644 --- a/spec/models/cherrypick_task/control_locator_spec.rb +++ b/spec/models/cherrypick_task/control_locator_spec.rb @@ -83,7 +83,7 @@ let(:batch_id) { 1 } let(:total_wells) { 96 } let(:num_control_wells) { 8 } - let(:wells_to_leave_free) { (0...89) } + let(:wells_to_leave_free) { 0...89 } it_behaves_like 'an invalid ControlLocator', 0 end @@ -92,7 +92,7 @@ let(:batch_id) { 1 } let(:total_wells) { 96 } let(:num_control_wells) { 0 } - let(:wells_to_leave_free) { (0...100) } + let(:wells_to_leave_free) { 0...100 } it_behaves_like 'an invalid ControlLocator', 0, 'More wells left free than available' end @@ -112,7 +112,7 @@ let(:batch_id) { batch_id } let(:total_wells) { 96 } let(:num_control_wells) { 2 } - let(:wells_to_leave_free) { (0...8) } + let(:wells_to_leave_free) { 0...8 } it_behaves_like 'a generator of valid positions', (8...96) end @@ -139,7 +139,7 @@ context 'when over a range of batches' do skip 'This analysis is not required to be run every time, so we skip it by default' - let(:range) { (1...1000) } + let(:range) { 1...1000 } let(:control_positions) do range.map do |batch_id| described_class diff --git a/spec/models/descriptor_spec.rb b/spec/models/descriptor_spec.rb new file mode 100644 index 0000000000..c327c16f91 --- /dev/null +++ b/spec/models/descriptor_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Descriptor do + describe '#validate_value' do + subject(:errors) { descriptor.validate_value(value) } + + let(:feature_flag) { :y25_105_validate_descriptor_required_field } + + before do + # By default, disable the feature flag + allow(Flipper).to receive(:enabled?).with(feature_flag).and_return(false) + end + + context 'when kind is Date' do + context 'when required is true' do + let(:descriptor) { described_class.new(name: 'Some expiry', kind: 'Date', required: true) } + + context 'with a valid ISO 8601 date' do + let(:value) { '2026-06-01' } + + it { is_expected.to be_empty } + end + + context 'with a blank value' do + let(:value) { '' } + + context 'when the feature flag is enabled' do + before do + allow(Flipper).to receive(:enabled?).with(feature_flag).and_return(true) + end + + it { is_expected.to contain_exactly('Some expiry is required') } + end + + context 'when the feature flag is disabled' do + before do + allow(Flipper).to receive(:enabled?).with(feature_flag).and_return(false) + end + + it { is_expected.to be_empty } + end + end + + context 'with an invalid date string' do + let(:value) { 'not-a-date' } + + it { + is_expected.to contain_exactly( + "'not-a-date' is not a valid date for Some expiry (expected YYYY-MM-DD)" + ) + } + end + + context 'with a date in the wrong format (DD/MM/YYYY)' do + let(:value) { '01/06/2026' } + + it { + is_expected.to contain_exactly( + "'01/06/2026' is not a valid date for Some expiry (expected YYYY-MM-DD)" + ) + } + end + + context 'with a year too far in the past' do + let(:value) { '1989-06-01' } + + it { + is_expected.to contain_exactly( + 'Date year for Some expiry must be between 1990 and 2100 (got 1989)' + ) + } + end + + context 'with a year too far in the future' do + let(:value) { '2101-06-01' } + + it { + is_expected.to contain_exactly( + 'Date year for Some expiry must be between 1990 and 2100 (got 2101)' + ) + } + end + + context 'with a typo in the year (e.g., 62026 instead of 2026)' do + let(:value) { '62026-06-01' } + + it { + is_expected.to contain_exactly( + "'62026-06-01' is not a valid date for Some expiry (expected YYYY-MM-DD)" + ) + } + end + end + + context 'when required is false' do + let(:descriptor) { described_class.new(name: 'Some expiry', kind: 'Date', required: false) } + + context 'with a blank value' do + let(:value) { '' } + + it { is_expected.to be_empty } + end + + context 'with a valid ISO 8601 date' do + let(:value) { '2026-06-01' } + + it { is_expected.to be_empty } + end + + context 'with an invalid date string' do + let(:value) { 'not-a-date' } + + it { + is_expected.to contain_exactly( + "'not-a-date' is not a valid date for Some expiry (expected YYYY-MM-DD)" + ) + } + end + + context 'with a year sanity check failure' do + let(:value) { '62026-06-01' } + + it { + is_expected.to contain_exactly( + "'62026-06-01' is not a valid date for Some expiry (expected YYYY-MM-DD)" + ) + } + end + end + end + + context 'when kind is Text' do + let(:descriptor) { described_class.new(name: 'Comment', kind: 'Text', required: false) } + let(:value) { 'any free text' } + + it { is_expected.to be_empty } + end + + context 'when kind is Selection' do + let(:descriptor) { described_class.new(name: 'Workflow', kind: 'Selection', required: false) } + let(:value) { 'Standard' } + + it { is_expected.to be_empty } + end + end +end diff --git a/spec/models/presenters/batch_submenu_presenter_spec.rb b/spec/models/presenters/batch_submenu_presenter_spec.rb index 7388891a0b..8f9638541f 100644 --- a/spec/models/presenters/batch_submenu_presenter_spec.rb +++ b/spec/models/presenters/batch_submenu_presenter_spec.rb @@ -78,4 +78,16 @@ end end end + + context 'when we are in the Ultima UG200 sequencing pipeline' do + let(:current_user) { create(:user) } + let(:batch) { create(:ultima_ug200_sequencing_batch, state: 'released') } + let(:generate_sample_sheet_option) do + { label: 'Download Sample Sheet', url: "/batches/#{batch.id}/generate_sample_sheet" } + end + + it 'includes a link to download the sample sheet' do + expect(batch_submenu_presenter.each_option.to_a).to include(generate_sample_sheet_option) + end + end end diff --git a/spec/models/submission_template_spec.rb b/spec/models/submission_template_spec.rb new file mode 100644 index 0000000000..b1b2945a93 --- /dev/null +++ b/spec/models/submission_template_spec.rb @@ -0,0 +1,356 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SubmissionTemplate do + describe 'validations' do + it 'is valid with valid attributes' do + submission_template = build(:submission_template) + expect(submission_template).to be_valid + end + + context 'without a name' do + let(:submission_template) { build(:submission_template, name: nil) } + + before { submission_template.validate } + + it 'is not valid' do + expect(submission_template).not_to be_valid + end + + it 'has an error on name' do + expect(submission_template.errors[:name]).to include("can't be blank") + end + end + + context 'without a submission_class_name' do + let(:submission_template) { build(:submission_template, submission_class_name: nil) } + + before { submission_template.validate } + + it 'is not valid' do + expect(submission_template).not_to be_valid + end + + it 'has an error on submission_class_name' do + expect(submission_template.errors[:submission_class_name]).to include("can't be blank") + end + end + + context 'without a product_catalogue' do + let(:submission_template) { build(:submission_template, product_catalogue: nil) } + + before { submission_template.validate } + + it 'is not valid' do + expect(submission_template).not_to be_valid + end + + it 'has an error on product_catalogue' do + expect(submission_template.errors[:product_catalogue]).to include("can't be blank") + end + end + end + + describe 'scopes' do + describe '.visible' do + it 'includes templates that are not superceded and not automated' do + visible_template = create(:submission_template, superceded_by_id: SubmissionTemplate::LATEST_VERSION, + automated: false) + # Automated template + create(:submission_template, superceded_by_id: SubmissionTemplate::LATEST_VERSION, automated: true) + # Superceded templates + create(:submission_template, superceded_by_id: 1, automated: false) + # Superceded and automated template + create(:submission_template, superceded_by_id: 1, automated: true) + + expect(described_class.visible).to eq([visible_template]) + end + end + + describe '.hidden' do + it 'includes templates that are superceded' do + # Superceded templates + hidden_template1 = create(:submission_template, superceded_by_id: 1, automated: false) + hidden_template2 = create(:submission_template, superceded_by_id: 1, automated: true) + # Not superceded template + create(:submission_template, superceded_by_id: SubmissionTemplate::LATEST_VERSION, automated: false) + + expect(described_class.hidden).to eq([hidden_template1, hidden_template2]) + end + end + + describe '.include_product_line' do + it 'includes associated product line' do + product_line = create(:product_line) + create(:submission_template, product_line:) + + expect(described_class.include_product_line.first.product_line).to eq(product_line) + end + end + end + + describe '#grouped_by_product_lines' do + it 'groups visible templates by product line name' do + product_line = create(:product_line) + submission_list = create_list(:submission_template, 5, product_line:) + + expect(described_class.grouped_by_product_lines).to eq({ product_line.name => submission_list }) + end + + it 'groups templates without a product line under "General"' do + submission_list = create_list(:submission_template, 5, product_line: nil) + + expect(described_class.grouped_by_product_lines).to eq({ 'General' => submission_list }) + end + end + + describe '#visible' do + let(:submission_template) { described_class.new(name: 'test submission') } + + context 'when superceded_by_id is LATEST_VERSION and automated is false' do + it 'returns true' do + submission_template.superceded_by_id = SubmissionTemplate::LATEST_VERSION + submission_template.automated = false + expect(submission_template.visible).to be true + end + end + + context 'when superceded_by_id is LATEST_VERSION and automated is true' do + it 'returns false' do + submission_template.superceded_by_id = SubmissionTemplate::LATEST_VERSION + submission_template.automated = true + expect(submission_template.visible).to be false + end + end + + context 'when superceded_by_id is not LATEST_VERSION and automated is false' do + it 'returns false' do + submission_template.superceded_by_id = 1 + submission_template.automated = false + expect(submission_template.visible).to be false + end + end + + context 'when superceded_by_id is not LATEST_VERSION and automated is true' do + it 'returns false' do + submission_template.superceded_by_id = 1 + submission_template.automated = true + expect(submission_template.visible).to be false + end + end + end + + describe '#superceded_by_unknown!' do + it 'sets superceded_by_id to SUPERCEDED_BY_UNKNOWN_TEMPLATE' do + submission_template = create(:submission_template) + submission_template.superceded_by_unknown! + expect(submission_template.superceded_by_id).to eq(SubmissionTemplate::SUPERCEDED_BY_UNKNOWN_TEMPLATE) + end + end + + describe '#supercede' do + let(:original_template) { create(:submission_template, name: 'Original Template') } + let(:cloned_template) { described_class.find_by(name: 'Cloned Template') } + + before do + original_template.supercede do |cloned| + cloned.name = 'Cloned Template' + end + end + + it 'creates a new submission template' do + expect(described_class.count).to eq(2) + end + + it 'sets cloned template superceded_by_id to LATEST_VERSION' do + expect(cloned_template.superceded_by_id).to eq(SubmissionTemplate::LATEST_VERSION) + end + + it 'updates original template attributes' do + expect(original_template).to have_attributes( + superceded_by_id: cloned_template.id, + superceded_at: be_present + ) + end + end + + describe '#create_order!' do + subject(:order) { submission_template.create_order!(order_attributes) } + + let!(:request_types) { create_list(:request_type, 2) } + let!(:submission_template) do + create(:submission_template, submission_parameters: { + request_type_ids_list: request_types.map(&:id) + }) + end + let!(:user) { create(:user) } + let!(:study) { create(:study) } + let!(:project) { create(:project) } + let(:order_attributes) { { user:, study:, project: } } + + it 'creates a persisted order' do + expect(order).to be_persisted + end + + it 'sets the order attributes' do + expect(order).to have_attributes( + user:, + study:, + project: + ) + end + + context 'with a block' do + subject(:order_with_block) do + submission_template.create_order!(order_attributes) do |created_order| + created_order.study = block_study + end + end + + let(:block_study) { create(:study) } + + it 'returns a persisted order' do + expect(order_with_block).to be_persisted + end + + it 'allows the block to modify the order' do + expect(order_with_block.study).to eq(block_study) + end + end + end + + describe '#create_with_submission!' do + subject(:order) { submission_template.create_with_submission!(order_attributes) } + + let!(:request_types) { create_list(:request_type, 2, asset_type: 'Well') } + let!(:submission_template) do + create(:submission_template, submission_parameters: { + request_type_ids_list: request_types.map(&:id) + }) + end + let!(:user) { create(:user) } + let!(:study) { create(:study) } + let!(:project) { create(:project) } + let!(:plate) { create(:plate, well_count: 5) } + let(:order_attributes) do + { + user: user, + study: study, + project: project, + assets: plate.wells + } + end + + it 'creates a persisted order' do + expect(order).to be_persisted + end + + it 'sets the order attributes' do + expect(order).to have_attributes( + user: user, + study: study, + project: project, + assets: plate.wells + ) + end + + it 'creates an associated submission' do + expect(order.submission).to be_present + end + + it 'sets the submission user' do + expect(order.submission.user).to eq(user) + end + end + + describe '#new_order' do + subject(:order) { submission_template.new_order(order_attributes) } + + let!(:request_types) { create_list(:request_type, 2) } + let!(:submission_template) do + create(:submission_template, submission_parameters: { + request_type_ids_list: request_types.map(&:id) + }) + end + let(:order_attributes) { { user_id: 1, study_id: 1, project_id: 1 } } + + it 'returns an order of the submission class type' do + expect(order).to be_a(submission_template.submission_class) + end + + it 'sets the order attributes' do + expect(order).to have_attributes( + user_id: 1, + study_id: 1, + project_id: 1, + template_name: submission_template.name + ) + end + end + + describe '#submission_class' do + it 'returns the class specified by submission_class_name' do + submission_template = create(:submission_template, submission_class_name: 'LinearSubmission') + expect(submission_template.submission_class).to eq(LinearSubmission) + end + end + + describe '#sequencing?' do + it 'returns true if any request type is a sequencing request' do + sequencing_request_type = create(:sequencing_request_type) + non_sequencing_request_type = create(:request_type) + submission_template = create(:submission_template, submission_parameters: { + request_type_ids_list: [sequencing_request_type.id, non_sequencing_request_type.id] + }) + + expect(submission_template.sequencing?).to be true + end + + it 'returns false if no request types have sequencing set to true' do + request_types = create_list(:request_type, 2) + submission_template = create(:submission_template, submission_parameters: { + request_type_ids_list: request_types.map(&:id) + }) + + expect(submission_template.sequencing?).to be false + end + end + + describe '#input_asset_type' do + it 'returns the asset type of the first request type in the list' do + request_type1 = create(:request_type, asset_type: 'Plate') + request_type2 = create(:request_type, asset_type: 'Tube') + submission_template = create(:submission_template, submission_parameters: { + request_type_ids_list: [request_type1.id, request_type2.id] + }) + + expect(submission_template.input_asset_type).to eq('Plate') + end + end + + describe '#input_plate_purposes' do + it 'returns the acceptable purposes of the first request type in the list' do + purposes = create_list(:purpose, 2) + request_type1 = create(:request_type, asset_type: 'Plate', acceptable_purposes: purposes) + request_type2 = create(:request_type, asset_type: 'Tube') + submission_template = create(:submission_template, submission_parameters: { + request_type_ids_list: [request_type1.id, request_type2.id] + }) + + expect(submission_template.input_plate_purposes).to eq(purposes) + end + end + + describe '#request_type_keys' do + it 'returns the keys of the request types in the list' do + request_type1 = create(:request_type, key: 'request_type_1') + request_type2 = create(:request_type, key: 'request_type_2') + submission_template = create(:submission_template, submission_parameters: { + request_type_ids_list: [request_type1.id, request_type2.id] + }) + + expect(submission_template.request_type_keys).to eq(%w[request_type_1 request_type_2]) + end + end +end diff --git a/spec/models/tasks/set_descriptors_handler/handler_spec.rb b/spec/models/tasks/set_descriptors_handler/handler_spec.rb index 816d97a1ec..d247a7f66c 100644 --- a/spec/models/tasks/set_descriptors_handler/handler_spec.rb +++ b/spec/models/tasks/set_descriptors_handler/handler_spec.rb @@ -11,7 +11,7 @@ let(:request) { batch.requests.first } let(:controller) { instance_double(WorkflowsController) } let(:user) { create(:user) } - let(:task) { instance_double(SetDescriptorsTask, name: 'Step 1', id: 1) } + let(:task) { instance_double(SetDescriptorsTask, name: 'Step 1', id: 1, descriptors: []) } describe '#perform' do context 'with all requests selected' do @@ -46,5 +46,48 @@ ) end end + + # The handler delegates validation entirely to Descriptor#validate_value. + # Full Date validation rules are covered in spec/models/descriptor_spec.rb. + context 'when a descriptor returns no errors' do + let(:passing_descriptor) { instance_double(Descriptor, name: 'OTR carrier expiry') } + let(:task) { instance_double(SetDescriptorsTask, name: 'Step 1', id: 1, descriptors: [passing_descriptor]) } + let(:params) do + { + batch_id: batch.id.to_s, + descriptors: { 'OTR carrier expiry' => '2026-06-01' }, + request: { request.id.to_s => 'on' } + } + end + + before { allow(passing_descriptor).to receive(:validate_value).with('2026-06-01').and_return([]) } + + it 'returns true' do + expect(handler.perform).to be true + end + end + + context 'when a descriptor returns a validation error' do + let(:error_message) { "'not-a-date' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" } + let(:failing_descriptor) { instance_double(Descriptor, name: 'OTR carrier expiry') } + let(:task) { instance_double(SetDescriptorsTask, name: 'Step 1', id: 1, descriptors: [failing_descriptor]) } + let(:params) do + { + batch_id: batch.id.to_s, + descriptors: { 'OTR carrier expiry' => 'not-a-date' }, + request: { request.id.to_s => 'on' } + } + end + + before { allow(failing_descriptor).to receive(:validate_value).with('not-a-date').and_return([error_message]) } + + it 'returns [false, error_message]' do + expect(handler.perform).to eq([false, error_message]) + end + + it 'does not create any lab events' do + expect { handler.perform }.not_to change(LabEvent, :count) + end + end end end diff --git a/spec/models/ultima_sequencing_request_spec.rb b/spec/models/ultima_sequencing_request_spec.rb index d152679b4e..3cbaa4864f 100644 --- a/spec/models/ultima_sequencing_request_spec.rb +++ b/spec/models/ultima_sequencing_request_spec.rb @@ -33,7 +33,7 @@ end context 'when ot_recipe value is not assigned' do - it 'is invalid and displays required percent phix requested error message' do + it 'is invalid and displays required OT recipe error message' do request.request_metadata.ot_recipe = nil request.validate expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") diff --git a/spec/models/ultima_ug200_sequencing_pipeline_spec.rb b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb new file mode 100644 index 0000000000..361bce75a5 --- /dev/null +++ b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe UltimaUG200SequencingPipeline do + let(:pipeline) do + described_class.new( + workflow: Workflow.new, + request_types: [create(:ultima_ug200_sequencing)] + ) + end + + describe '#ot_recipe_consistent_for_batch?' do + it 'returns true when all requests have the same ot_recipe' do + batch = pipeline.batches.build + r1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + r2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + batch.requests << [r1, r2] + + expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be true + end + + it 'returns false when requests have different ot_recipes' do + batch = pipeline.batches.build + req1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + req2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Flex' }) + batch.requests << [req1, req2] + + expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be false + end + + it 'returns false when some requests are missing ot_recipe' do + batch = pipeline.batches.build + r1 = create(:sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + r2 = create(:sequencing_request, request_metadata_attributes: {}) # no ot_recipe + batch.requests << [r1, r2] + + expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be false + end + end + + describe '#wafer_size_consistent_for_batch?' do + it 'returns true when all requests have the same wafer_size' do + batch = pipeline.batches.build + r1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + r2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + batch.requests << [r1, r2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be true + end + + it 'returns false when requests have different wafer_sizes' do + batch = pipeline.batches.build + req1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' }) + req2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + batch.requests << [req1, req2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false + end + + # NB. Wafer size is a required field, so cannot be missing. Tested in the request spec. + end + + describe '#post_release_batch' do + let(:batch) { create(:batch) } + + it 'calls Messenger with UseqWaferIo template and useq_wafer root' do + allow(Messenger).to receive(:create!) + pipeline.post_release_batch(batch, create(:user)) + + expect(Messenger).to have_received(:create!).with( + hash_including(target: batch, template: 'UseqWaferIo', root: 'useq_wafer') + ) + end + end +end diff --git a/spec/models/ultima_ug200_sequencing_request_spec.rb b/spec/models/ultima_ug200_sequencing_request_spec.rb new file mode 100644 index 0000000000..a87bc38fd2 --- /dev/null +++ b/spec/models/ultima_ug200_sequencing_request_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UltimaUG200SequencingRequest do + let(:request) { create(:ultima_ug200_sequencing_request) } + + describe 'Validations' do + context 'when all attributes are valid' do + it 'is valid' do + expect(request).to be_valid + end + end + + context 'when fragment_size_required_from is less than 1' do + it 'is invalid and displays fragment size from error message' do + request.request_metadata.fragment_size_required_from = 0 + request.validate + expect(request.errors[:'request_metadata.fragment_size_required_from']).to include( + 'must be greater than or equal to 1' + ) + end + end + + context 'when fragment_size_required_to is less than 1' do + it 'is invalid and displays fragment size to error message' do + request.request_metadata.fragment_size_required_to = 0 + request.validate + expect(request.errors[:'request_metadata.fragment_size_required_to']).to include( + 'must be greater than or equal to 1' + ) + end + end + + context 'when wafer_size value is not assigned' do + it 'is invalid and displays required wafer size error message' do + request.request_metadata.wafer_size = nil + request.validate + expect(request.errors[:'request_metadata.wafer_size']).to include("can't be blank") + end + end + + context 'when ot_recipe value is not assigned' do + it 'is invalid and displays required OT recipe error message' do + request.request_metadata.ot_recipe = nil + request.validate + expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") + end + end + end +end diff --git a/spec/requests/api/v2/bait_library_layouts_spec.rb b/spec/requests/api/v2/bait_library_layouts_spec.rb index b27befdba5..265e9bdd72 100644 --- a/spec/requests/api/v2/bait_library_layouts_spec.rb +++ b/spec/requests/api/v2/bait_library_layouts_spec.rb @@ -319,7 +319,7 @@ def perform_post 'errors' => [ { 'title' => 'Missing parameter', - 'detail' => "param is missing or the value is empty: #{missing_parameter}", + 'detail' => "param is missing or the value is empty or invalid: #{missing_parameter}", 'code' => 400, 'status' => 400 } diff --git a/spec/resources/api/v2/order_resource_spec.rb b/spec/resources/api/v2/order_resource_spec.rb index 5428ac9f7e..71eacee316 100644 --- a/spec/resources/api/v2/order_resource_spec.rb +++ b/spec/resources/api/v2/order_resource_spec.rb @@ -16,6 +16,7 @@ it { is_expected.to have_readonly_attribute :request_options } it { is_expected.to have_readonly_attribute :request_types } it { is_expected.to have_readonly_attribute :uuid } + it { is_expected.to have_writeonly_attribute :project_uuid } # Relationships it { is_expected.to have_a_readonly_has_one(:project).with_class_name('Project') } @@ -39,11 +40,14 @@ end context 'with a template in the context' do - let(:context) { { template:, template_attributes: } } + let(:context) { { template:, template_attributes:, project: } } let(:template) { instance_double(SubmissionTemplate) } let(:template_attributes) { {} } + let(:project) { build_stubbed(:project) } - before { allow(template).to receive(:create_order!).with(template_attributes).and_return(resource_model) } + before do + allow(template).to receive(:create_order!).with(template_attributes, project).and_return(resource_model) + end it 'does not call create on the super class' do allow(described_class.superclass).to receive(:create) diff --git a/spec/validators/ultima_ug200_validator_spec.rb b/spec/validators/ultima_ug200_validator_spec.rb new file mode 100644 index 0000000000..34082981cb --- /dev/null +++ b/spec/validators/ultima_ug200_validator_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true +require 'rails_helper' + +describe UltimaUG200Validator do + describe '#validate' do + subject(:validator) { described_class.new } + + let(:pipeline) { UltimaUG200SequencingPipeline.new } + let(:batch) { create(:batch, pipeline:) } + let(:requests) { [request1, request2] } + let(:request1_metadata) { { ot_recipe: 'Free', wafer_size: '10TB' } } + let(:request2_metadata) { { ot_recipe: 'Free', wafer_size: '10TB' } } + let(:request1) { create(:ultima_sequencing_request, request_metadata_attributes: request1_metadata) } + let(:request2) { create(:ultima_sequencing_request, request_metadata_attributes: request2_metadata) } + + before do + batch.requests << requests + end + + context 'when batch contains two requests with the same OT Recipe' do + it 'is valid' do + validator.validate(batch) + expect(batch.errors[:base]).to be_empty + end + end + + context 'when batch contains two requests with different ot_recipe' do + let(:request2_metadata) { { ot_recipe: 'Flex', wafer_size: '10TB' } } + + it 'is invalid due to ot_recipe mismatch' do + validator.validate(batch) + expect(batch.errors[:base]).to include(described_class::OT_RECIPE_CONSISTENT_MSG) + end + end + + context 'when batch contains two requests with the same Wafer Size' do + it 'is valid' do + validator.validate(batch) + expect(batch.errors[:base]).not_to include(described_class::WAFER_SIZE_CONSISTENT_MSG) + end + end + + context 'when batch contains two requests with different wafer_size' do + let(:request1_metadata) { { ot_recipe: 'Free', wafer_size: '5TB' } } + + it 'is invalid due to wafer_size mismatch' do + validator.validate(batch) + expect(batch.errors[:base]).to include(described_class::WAFER_SIZE_CONSISTENT_MSG) + end + end + + context 'when batch contains a single request' do + let(:requests) { [request1] } + + it 'is invalid' do + validator.validate(batch) + expect(batch.errors[:base]).to include(described_class::TWO_REQUESTS_MSG) + end + end + end +end diff --git a/test/controllers/batches_controller_test.rb b/test/controllers/batches_controller_test.rb index a109e47d21..a7810bbefd 100644 --- a/test/controllers/batches_controller_test.rb +++ b/test/controllers/batches_controller_test.rb @@ -257,6 +257,9 @@ class BatchesControllerTest < ActionController::TestCase "request_group_#{@plate.id}_#{@submission.id}_size": '1', commit: 'Submit' } + + assert_redirected_to batch_path(assigns(:batch)) + assert_predicate assigns(:batch), :persisted? end end diff --git a/test/lib/label_printer/plate_to_tube_test.rb b/test/lib/label_printer/plate_to_tube_test.rb index 49bb24fbb4..6a289237cc 100644 --- a/test/lib/label_printer/plate_to_tube_test.rb +++ b/test/lib/label_printer/plate_to_tube_test.rb @@ -13,7 +13,12 @@ def setup # rubocop:todo Metrics/AbcSize @barcode1 = '1111' @asset_name = 'tube name' @tube1 = create(:sample_tube, :tube_barcode, barcode: barcode1, prefix: prefix, name: asset_name) - @sample_tubes = create_list(:sample_tube, 4, :tube_barcode) + @sample_tubes = [ + create(:sample_tube, :tube_barcode, barcode: '2222', prefix: prefix), + create(:sample_tube, :tube_barcode, barcode: '3333', prefix: prefix), + create(:sample_tube, :tube_barcode, barcode: '4444', prefix: prefix), + create(:sample_tube, :tube_barcode, barcode: '5555', prefix: prefix) + ] sample_tubes.unshift(tube1) options = { sample_tubes: } @tube_label = LabelPrinter::Label::PlateToTubes.new(options) diff --git a/test/unit/batch_test.rb b/test/unit/batch_test.rb index abfc223aef..e9d8ae10a5 100644 --- a/test/unit/batch_test.rb +++ b/test/unit/batch_test.rb @@ -647,7 +647,7 @@ class BatchTest < ActiveSupport::TestCase end should 'check that with the pipeline that the batch is valid' do - @batch.complete!(@user) + assert_nothing_raised { @batch.complete!(@user) } end end diff --git a/test/unit/event_test.rb b/test/unit/event_test.rb index 67e6a550e7..68fe62686c 100644 --- a/test/unit/event_test.rb +++ b/test/unit/event_test.rb @@ -18,7 +18,7 @@ class EventTest < ActiveSupport::TestCase setup { @event = Event.create(descriptor_key: '') } should 'be valid' do - @event.valid? + assert_predicate @event, :valid? end end end @@ -28,7 +28,7 @@ class EventTest < ActiveSupport::TestCase setup { @event = Event.create } should 'be valid' do - @event.valid? + assert_predicate @event, :valid? end end end diff --git a/test/unit/request_factory_test.rb b/test/unit/request_factory_test.rb index 5339e13433..538b6eaacc 100644 --- a/test/unit/request_factory_test.rb +++ b/test/unit/request_factory_test.rb @@ -42,7 +42,7 @@ class RequestcreateTest < ActiveSupport::TestCase setup { @project.update!(enforce_quotas: true) } should 'not fail' do - RequestFactory.copy_request(@request) + assert_nothing_raised { RequestFactory.copy_request(@request) } end end end diff --git a/test/unit/sequencing_qc_batch_test.rb b/test/unit/sequencing_qc_batch_test.rb index 89a123a9bb..eda3f8a081 100644 --- a/test/unit/sequencing_qc_batch_test.rb +++ b/test/unit/sequencing_qc_batch_test.rb @@ -38,6 +38,7 @@ class SequencingQcBatchTest < ActiveSupport::TestCase should 'do nothing if the next state is nil' do @batch.stubs(:qc_next_state).returns(nil) + @batch.expects(:update_attribute).never # no state update when there is no next state @batch.qc_submitted end end @@ -51,6 +52,7 @@ class SequencingQcBatchTest < ActiveSupport::TestCase should 'do nothing if the next state is nil' do @batch.stubs(:qc_next_state).returns(nil) + @batch.expects(:update_attribute).never # no state update when there is no next state @batch.qc_criteria_received end end @@ -64,6 +66,7 @@ class SequencingQcBatchTest < ActiveSupport::TestCase should 'do nothing if the next state is nil' do @batch.stubs(:qc_next_state).returns(nil) + @batch.expects(:update_attribute).never # no state update when there is no next state @batch.qc_complete end end diff --git a/test/unit/tasks/plate_transfer_task_test.rb b/test/unit/tasks/plate_transfer_task_test.rb index 9ed5ba55f8..669a0df93d 100644 --- a/test/unit/tasks/plate_transfer_task_test.rb +++ b/test/unit/tasks/plate_transfer_task_test.rb @@ -100,6 +100,7 @@ class PlateTransferTaskTest < ActiveSupport::TestCase end should 'find the existing plate' do + assert_equal 1, Plate.count - @plate_count end end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 126e8414d0..081be0ac56 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -123,7 +123,7 @@ class UserTest < ActiveSupport::TestCase should 'be able to have one assigned' do code = 'code' - @user.swipecard_code = code + assert_nothing_raised { @user.swipecard_code = code } end end diff --git a/yarn.lock b/yarn.lock index d526a1b99e..bdf05f03b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13,10 +13,787 @@ "@csstools/css-tokenizer" "^3.0.3" lru-cache "^10.4.3" -"@babel/standalone@^7.17.11": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.28.3.tgz#0e126deef7b88b08481c233c7ac0e94c9629d7cd" - integrity sha512-VHmaaU23OkxShTtkwXlte7/uHDK8v55J9YLMqlucjnYujeB9YgrYCHU6LREqUegTVq+/KlLgjoUu8lbeI3XQPA== +"@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.28.6", "@babel/compat-data@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== + +"@babel/core@^7.25.8": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.29.0": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== + dependencies: + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== + dependencies: + "@babel/types" "^7.27.3" + +"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz#611ff5482da9ef0db6291bcd24303400bca170fb" + integrity sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.28.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1", "@babel/helper-create-regexp-features-plugin@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz#7c1ddd64b2065c7f78034b25b43346a7e19ed997" + integrity sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + regexpu-core "^6.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.8": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz#cf1e4462b613f2b54c41e6ff758d5dfcaa2c85d1" + integrity sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA== + dependencies: + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + debug "^4.4.3" + lodash.debounce "^4.0.8" + resolve "^1.22.11" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== + dependencies: + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + +"@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== + dependencies: + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== + +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-replace-supers@^7.27.1", "@babel/helper-replace-supers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz#94aa9a1d7423a00aead3f204f78834ce7d53fe44" + integrity sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.28.6" + +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helper-wrap-function@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz#4e349ff9222dab69a93a019cc296cdd8442e279a" + integrity sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ== + dependencies: + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helpers@^7.28.6": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.2.tgz#9cfbccb02b8e229892c0b07038052cc1a8709c49" + integrity sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw== + dependencies: + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + +"@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.2.tgz#58bd50b9a7951d134988a1ae177a35ef9a703ba1" + integrity sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA== + dependencies: + "@babel/types" "^7.29.0" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz#fbde57974707bbfa0376d34d425ff4fa6c732421" + integrity sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz#0e8289cec28baaf05d54fd08d81ae3676065f69f" + integrity sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-import-assertions@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz#ae9bc1923a6ba527b70104dd2191b0cd872c8507" + integrity sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-import-attributes@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz#b71d5914665f60124e133696f17cd7669062c503" + integrity sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-async-generator-functions@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz#63ed829820298f0bf143d5a4a68fb8c06ffd742f" + integrity sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.29.0" + +"@babel/plugin-transform-async-to-generator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz#bd97b42237b2d1bc90d74bcb486c39be5b4d7e77" + integrity sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-remap-async-to-generator" "^7.27.1" + +"@babel/plugin-transform-block-scoped-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-block-scoping@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz#e1ef5633448c24e76346125c2534eeb359699a99" + integrity sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-class-properties@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz#d274a4478b6e782d9ea987fda09bdb6d28d66b72" + integrity sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-class-static-block@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz#1257491e8259c6d125ac4d9a6f39f9d2bf3dba70" + integrity sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-classes@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz#8f6fb79ba3703978e701ce2a97e373aae7dda4b7" + integrity sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-globals" "^7.28.0" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-replace-supers" "^7.28.6" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-transform-computed-properties@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz#936824fc71c26cb5c433485776d79c8e7b0202d2" + integrity sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/template" "^7.28.6" + +"@babel/plugin-transform-destructuring@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz#b8402764df96179a2070bb7b501a1586cf8ad7a7" + integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" + +"@babel/plugin-transform-dotall-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz#def31ed84e0fb6e25c71e53c124e7b76a4ab8e61" + integrity sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-duplicate-keys@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz#8014b8a6cfd0e7b92762724443bf0d2400f26df1" + integrity sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-dynamic-import@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-explicit-resource-management@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz#dd6788f982c8b77e86779d1d029591e39d9d8be7" + integrity sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" + +"@babel/plugin-transform-exponentiation-operator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz#5e477eb7eafaf2ab5537a04aaafcf37e2d7f1091" + integrity sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-export-namespace-from@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-for-of@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-function-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== + dependencies: + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-transform-json-strings@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz#4c8c15b2dc49e285d110a4cf3dac52fd2dfc3038" + integrity sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-logical-assignment-operators@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz#53028a3d77e33c50ef30a8fce5ca17065936e605" + integrity sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-member-expression-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-amd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-commonjs@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz#c0232e0dfe66a734cc4ad0d5e75fc3321b6fdef1" + integrity sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA== + dependencies: + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-modules-systemjs@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz#e458a95a17807c415924106a3ff188a3b8dee964" + integrity sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ== + dependencies: + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.29.0" + +"@babel/plugin-transform-modules-umd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz#a26cd51e09c4718588fc4cce1c5d1c0152102d6a" + integrity sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-new-target@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz#9bc62096e90ab7a887f3ca9c469f6adec5679757" + integrity sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-numeric-separator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz#1310b0292762e7a4a335df5f580c3320ee7d9e9f" + integrity sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-object-rest-spread@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz#fdd4bc2d72480db6ca42aed5c051f148d7b067f7" + integrity sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA== + dependencies: + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-transform-object-super@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + +"@babel/plugin-transform-optional-catch-binding@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz#75107be14c78385978201a49c86414a150a20b4c" + integrity sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-optional-chaining@^7.27.1", "@babel/plugin-transform-optional-chaining@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz#926cf150bd421fc8362753e911b4a1b1ce4356cd" + integrity sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-parameters@^7.27.7": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz#1fd2febb7c74e7d21cf3b05f7aebc907940af53a" + integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-private-methods@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz#c76fbfef3b86c775db7f7c106fff544610bdb411" + integrity sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-private-property-in-object@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz#4fafef1e13129d79f1d75ac180c52aafefdb2811" + integrity sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-property-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-regenerator@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz#dec237cec1b93330876d6da9992c4abd42c9d18b" + integrity sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-regexp-modifiers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz#7ef0163bd8b4a610481b2509c58cf217f065290b" + integrity sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-reserved-words@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-shorthand-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-spread@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz#40a2b423f6db7b70f043ad027a58bcb44a9757b6" + integrity sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-sticky-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-template-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-typeof-symbol@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-escapes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-property-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz#63a7a6c21a0e75dae9b1861454111ea5caa22821" + integrity sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-unicode-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-sets-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz#924912914e5df9fe615ec472f88ff4788ce04d4e" + integrity sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/preset-env@^7.25.8": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.2.tgz#5a173f22c7d8df362af1c9fe31facd320de4a86c" + integrity sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw== + dependencies: + "@babel/compat-data" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.28.5" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.28.6" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.28.6" + "@babel/plugin-syntax-import-attributes" "^7.28.6" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.29.0" + "@babel/plugin-transform-async-to-generator" "^7.28.6" + "@babel/plugin-transform-block-scoped-functions" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.28.6" + "@babel/plugin-transform-class-properties" "^7.28.6" + "@babel/plugin-transform-class-static-block" "^7.28.6" + "@babel/plugin-transform-classes" "^7.28.6" + "@babel/plugin-transform-computed-properties" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" + "@babel/plugin-transform-dotall-regex" "^7.28.6" + "@babel/plugin-transform-duplicate-keys" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.29.0" + "@babel/plugin-transform-dynamic-import" "^7.27.1" + "@babel/plugin-transform-explicit-resource-management" "^7.28.6" + "@babel/plugin-transform-exponentiation-operator" "^7.28.6" + "@babel/plugin-transform-export-namespace-from" "^7.27.1" + "@babel/plugin-transform-for-of" "^7.27.1" + "@babel/plugin-transform-function-name" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.28.6" + "@babel/plugin-transform-literals" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.28.6" + "@babel/plugin-transform-member-expression-literals" "^7.27.1" + "@babel/plugin-transform-modules-amd" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.28.6" + "@babel/plugin-transform-modules-systemjs" "^7.29.0" + "@babel/plugin-transform-modules-umd" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.29.0" + "@babel/plugin-transform-new-target" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.28.6" + "@babel/plugin-transform-numeric-separator" "^7.28.6" + "@babel/plugin-transform-object-rest-spread" "^7.28.6" + "@babel/plugin-transform-object-super" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding" "^7.28.6" + "@babel/plugin-transform-optional-chaining" "^7.28.6" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/plugin-transform-private-methods" "^7.28.6" + "@babel/plugin-transform-private-property-in-object" "^7.28.6" + "@babel/plugin-transform-property-literals" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.29.0" + "@babel/plugin-transform-regexp-modifiers" "^7.28.6" + "@babel/plugin-transform-reserved-words" "^7.27.1" + "@babel/plugin-transform-shorthand-properties" "^7.27.1" + "@babel/plugin-transform-spread" "^7.28.6" + "@babel/plugin-transform-sticky-regex" "^7.27.1" + "@babel/plugin-transform-template-literals" "^7.27.1" + "@babel/plugin-transform-typeof-symbol" "^7.27.1" + "@babel/plugin-transform-unicode-escapes" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.28.6" + "@babel/plugin-transform-unicode-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.28.6" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.15" + babel-plugin-polyfill-corejs3 "^0.14.0" + babel-plugin-polyfill-regenerator "^0.6.6" + core-js-compat "^3.48.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + debug "^4.3.1" + +"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.29.0", "@babel/types@^7.4.4": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" "@csstools/color-helpers@^5.0.2": version "5.0.2" @@ -368,7 +1145,7 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== -"@jridgewell/gen-mapping@^0.3.5": +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== @@ -376,6 +1153,14 @@ "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" @@ -389,7 +1174,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" -"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== @@ -402,6 +1187,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@parcel/watcher-android-arm64@2.5.1": version "2.5.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" @@ -611,16 +1404,19 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@vitejs/plugin-legacy@^1.8": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-legacy/-/plugin-legacy-1.8.2.tgz#2f315bcb6685b12719813fb9412851445cca636f" - integrity sha512-NCOKU+pU+cxLMR9P9RTolEuOK+h+zYBXlknj+zGcKSj/NXBZYgA1GAH1FnO4zijoWRiTaiOm2ha9LQrELE7XHg== +"@vitejs/plugin-legacy@^5.0": + version "5.4.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-legacy/-/plugin-legacy-5.4.3.tgz#9ba634d25fb56e350cb0f576ce88099218b3b9b9" + integrity sha512-wsyXK9mascyplcqvww1gA1xYiy29iRHfyciw+a0t7qRNdzX6PdfSWmOoCi74epr87DujM+5J+rnnSv+4PazqVg== dependencies: - "@babel/standalone" "^7.17.11" - core-js "^3.22.3" - magic-string "^0.26.1" - regenerator-runtime "^0.13.9" - systemjs "^6.12.1" + "@babel/core" "^7.25.8" + "@babel/preset-env" "^7.25.8" + browserslist "^4.24.0" + browserslist-to-esbuild "^2.1.1" + core-js "^3.38.1" + magic-string "^0.30.12" + regenerator-runtime "^0.14.1" + systemjs "^6.15.1" acorn-jsx@^5.3.2: version "5.3.2" @@ -671,11 +1467,40 @@ autoprefixer@^10.4.13: picocolors "^1.1.1" postcss-value-parser "^4.2.0" +babel-plugin-polyfill-corejs2@^0.4.15: + version "0.4.17" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz#198f970f1c99a856b466d1187e88ce30bd199d91" + integrity sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-define-polyfill-provider" "^0.6.8" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.14.0: + version "0.14.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz#6ac08d2f312affb70c4c69c0fbba4cb417ee5587" + integrity sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.8" + core-js-compat "^3.48.0" + +babel-plugin-polyfill-regenerator@^0.6.6: + version "0.6.8" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz#8a6bfd5dd54239362b3d06ce47ac52b2d95d7721" + integrity sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.8" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +baseline-browser-mapping@^2.10.12: + version "2.10.19" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz#7697721c22f94f66195d0c34299b1a91e3299493" + integrity sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g== + bootstrap@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.2.tgz#8e0cd61611728a5bf65a3a2b8d6ff6c77d5d7479" @@ -696,6 +1521,13 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" +browserslist-to-esbuild@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/browserslist-to-esbuild/-/browserslist-to-esbuild-2.1.1.tgz#50dc4c55a6889ba22c7b1bd820032f81b822faf0" + integrity sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw== + dependencies: + meow "^13.0.0" + browserslist@^4.21.4, browserslist@^4.24.4: version "4.25.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.3.tgz#9167c9cbb40473f15f75f85189290678b99b16c5" @@ -706,6 +1538,17 @@ browserslist@^4.21.4, browserslist@^4.24.4: node-releases "^2.0.19" update-browserslist-db "^1.1.3" +browserslist@^4.24.0, browserslist@^4.28.1: + version "4.28.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.2.tgz#f50b65362ef48974ca9f50b3680566d786b811d2" + integrity sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg== + dependencies: + baseline-browser-mapping "^2.10.12" + caniuse-lite "^1.0.30001782" + electron-to-chromium "^1.5.328" + node-releases "^2.0.36" + update-browserslist-db "^1.2.3" + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -721,6 +1564,11 @@ caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001735: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001736.tgz#3710a99cf154b653590fb6a57f81ee34173c3b47" integrity sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw== +caniuse-lite@^1.0.30001782: + version "1.0.30001788" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz#31e97d1bfec332b3f2d7eea7781460c97629b3bf" + integrity sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ== + chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -763,10 +1611,22 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -core-js@^3.22.3: - version "3.45.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.45.1.tgz#5810e04a1b4e9bc5ddaa4dd12e702ff67300634d" - integrity sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js-compat@^3.48.0: + version "3.49.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.49.0.tgz#06145447d92f4aaf258a0c44f24b47afaeaffef6" + integrity sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA== + dependencies: + browserslist "^4.28.1" + +core-js@^3.38.1: + version "3.49.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.49.0.tgz#8b4d520ac034311fa21aa616f017ada0e0dbbddd" + integrity sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg== core-util-is@~1.0.0: version "1.0.3" @@ -965,6 +1825,13 @@ debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "^2.1.3" +debug@^4.1.0, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + decimal.js@^10.5.0: version "10.6.0" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" @@ -985,11 +1852,21 @@ electron-to-chromium@^1.5.204: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz#0fedde3eec615065ee95531c09a10578644c5552" integrity sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw== +electron-to-chromium@^1.5.328: + version "1.5.339" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz#d797bf5f222a7f6241a42b43a97bf52ff43947f1" + integrity sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg== + entities@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + esbuild@^0.21.3: version "0.21.5" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" @@ -1029,10 +1906,10 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-prettier@^8.10: - version "8.10.2" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz#0642e53625ebc62c31c24726b0f050df6bd97a2e" - integrity sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A== +eslint-config-prettier@^9.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz#90deb4fa0259592df774b600dbd1d2249a78ce91" + integrity sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ== eslint-scope@^8.4.0: version "8.4.0" @@ -1195,6 +2072,11 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" @@ -1292,7 +2174,7 @@ inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -is-core-module@^2.16.0: +is-core-module@^2.16.0, is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== @@ -1336,6 +2218,11 @@ jquery@>=1.7, jquery@^3.7.1: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -1374,6 +2261,11 @@ jsdom@^26.0: ws "^8.18.0" xml-name-validator "^5.0.0" +jsesc@^3.0.2, jsesc@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -1389,6 +2281,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + jszip@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" @@ -1428,6 +2325,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -1438,12 +2340,24 @@ lru-cache@^10.4.3: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== -magic-string@^0.26.1: - version "0.26.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f" - integrity sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow== +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +magic-string@^0.30.12: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== dependencies: - sourcemap-codec "^1.4.8" + "@jridgewell/sourcemap-codec" "^1.5.5" + +meow@^13.0.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" + integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== micromatch@^4.0.5: version "4.0.8" @@ -1485,6 +2399,11 @@ node-releases@^2.0.19: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== +node-releases@^2.0.36: + version "2.0.37" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.37.tgz#9bd4f10b77ba39c2b9402d4e8399c482a797f671" + integrity sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg== + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" @@ -1910,9 +2829,9 @@ prelude-ls@^1.2.1: integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prettier@^3.3: - version "3.8.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" - integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== + version "3.8.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.3.tgz#560f2de55bf01b4c0503bc629d5df99b9a1d09b0" + integrity sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw== process-nextick-args@~2.0.0: version "2.0.1" @@ -1949,10 +2868,46 @@ readdirp@^4.0.1: resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== -regenerator-runtime@^0.13.9: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz#aa113812ba899b630658c7623466be71e1f86f66" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regexpu-core@^6.3.1: + version "6.4.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.4.0.tgz#3580ce0c4faedef599eccb146612436b62a176e5" + integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.2" + regjsgen "^0.8.0" + regjsparser "^0.13.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.2.1" + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.13.0: + version "0.13.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.13.1.tgz#0593cbacb27527927692030928ae4d3b878d6f8d" + integrity sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw== + dependencies: + jsesc "~3.1.0" resolve-from@^4.0.0: version "4.0.0" @@ -1968,6 +2923,16 @@ resolve@^1.1.7: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.11: + version "1.22.12" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + rollup@^4.20.0: version "4.47.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.47.1.tgz#c40bce25b7140265dbe5467cd32871f71e9f9f0b" @@ -2012,10 +2977,10 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass@^1.98.0: - version "1.98.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.98.0.tgz#924ce85a3745ccaccd976262fdc1bc0c13aa8e57" - integrity sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A== +sass@^1.99.0: + version "1.99.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.99.0.tgz#ff9d1594da4886249dfaafabbeea2dea2dc74b26" + integrity sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q== dependencies: chokidar "^4.0.0" immutable "^5.1.5" @@ -2035,6 +3000,11 @@ select2@^4.1.0-rc.0: resolved "https://registry.yarnpkg.com/select2/-/select2-4.1.0-rc.0.tgz#ba3cd3901dda0155e1c0219ab41b74ba51ea22d8" integrity sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.5.4: version "7.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" @@ -2080,11 +3050,6 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -sourcemap-codec@^1.4.8: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -2114,7 +3079,7 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -systemjs@^6.12.1: +systemjs@^6.15.1: version "6.15.1" resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.15.1.tgz#74175b6810e27a79e1177d21db5f0e3057118cea" integrity sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA== @@ -2177,6 +3142,29 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz#65a7adfad8574c219890e219285ce4c64ed67eaa" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz#301d4f8a43d2b75c97adfad87c9dd5350c9475d1" + integrity sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ== + update-browserslist-db@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" @@ -2185,6 +3173,14 @@ update-browserslist-db@^1.1.3: escalade "^3.2.0" picocolors "^1.1.1" +update-browserslist-db@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -2198,9 +3194,9 @@ util-deprecate@^1.0.2, util-deprecate@~1.0.1: integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== "vite-plugin-ruby@>=5.1.0 <5.1.2 || ^5.1.3 || ^5.2": - version "5.2.0" - resolved "https://registry.yarnpkg.com/vite-plugin-ruby/-/vite-plugin-ruby-5.2.0.tgz#a033be47e0531e842d962df9ef394c6e7f940a6a" - integrity sha512-FoCaok2pV7GrcAqdxniI1r5XWBlSg9HwEwaxdQdXUVFfYkyINVakPeyrSK4PqOVhonBCuoc633g6bDTEC7wkcA== + version "5.2.1" + resolved "https://registry.yarnpkg.com/vite-plugin-ruby/-/vite-plugin-ruby-5.2.1.tgz#8422e2a69cd923b5d63f510744f0503a622bf4c6" + integrity sha512-wI3F/Yr4e4mEwiMff/cvNwGu8nZok5wrwUjHxO8we+h3y9+qCluO3Y5dzvz6vHJDBya9fKXkltoMwoJhaB2SRg== dependencies: obug "^2.0" tinyglobby "^0.2.12" @@ -2275,6 +3271,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"