diff --git a/app/components/tag_list_input_component.rb b/app/components/tag_list_input_component.rb index 2f415cf0b1..2e9265f5bd 100644 --- a/app/components/tag_list_input_component.rb +++ b/app/components/tag_list_input_component.rb @@ -5,16 +5,19 @@ class TagListInputComponent < ViewComponent::Base placeholder: I18n.t("components.tag_list_input.default_placeholder"), only_one: false, aria_label: nil, - hidden_field_data_options: {}) + hidden_field_data_options: {}, + autocomplete_url: "") @name = name @tags = tags @placeholder = placeholder @only_one = only_one @aria_label_option = aria_label ? { 'aria-label': aria_label } : {} @hidden_field_data_options = hidden_field_data_options + @autocomplete_url = autocomplete_url end - attr_reader :name, :tags, :placeholder, :only_one, :aria_label_option, :hidden_field_data_options + attr_reader :name, :tags, :placeholder, :only_one, :aria_label_option, + :hidden_field_data_options, :autocomplete_url private diff --git a/app/components/tag_list_input_component/tag_list_input_component.html.haml b/app/components/tag_list_input_component/tag_list_input_component.html.haml index 9f67ebdbfa..65aeddd278 100644 --- a/app/components/tag_list_input_component/tag_list_input_component.html.haml +++ b/app/components/tag_list_input_component/tag_list_input_component.html.haml @@ -1,4 +1,4 @@ -%div{ "data-controller": "tag-list-input", "data-tag-list-input-only-one-value": "#{only_one}" } +%div{ "data-controller": "tag-list-input", "data-tag-list-input-only-one-value": "#{only_one}", "data-tag-list-input-url-value": autocomplete_url, "data-action": "autocomplete.change->tag-list-input#addTag" } .tags-input .tags - # We use display:none instead of hidden field, so changes to the value can be picked up by the bulkFormController @@ -16,4 +16,12 @@ %span=tag %a.remove-button{ "data-action": "click->tag-list-input#removeTag" } × - = text_field_tag "variant_add_tag", nil, class: "input", placeholder: placeholder, "data-action": "keydown.enter->tag-list-input#addTag keyup->tag-list-input#filterInput blur->tag-list-input#addTag", "data-tag-list-input-target": "newTag", **aria_label_option, style: "display: #{display};" + = text_field_tag("variant_add_tag", + nil, + { class: "input", + placeholder: placeholder, + "data-action": "keydown.enter->tag-list-input#keyboardAddTag keyup->tag-list-input#filterInput blur->tag-list-input#onBlur focus->tag-list-input#onInputChange", + "data-tag-list-input-target": "input", + **aria_label_option, + style: "display: #{display};"}) + %ul.suggestion-list{ "data-tag-list-input-target": "results" , hidden: true } diff --git a/app/components/tag_list_input_component/tag_list_input_component.scss b/app/components/tag_list_input_component/tag_list_input_component.scss index ddfbe81942..0c1c8241c7 100644 --- a/app/components/tag_list_input_component/tag_list_input_component.scss +++ b/app/components/tag_list_input_component/tag_list_input_component.scss @@ -67,4 +67,35 @@ font-size: 14px; } } + + ul.suggestion-list { + margin-top: 5px; + padding: 5px 0; + z-index: $tag-drop-down-z-index; + width: fit-content; + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.2); + box-shadow: $shadow-dropdown; + list-style-type: none; + max-height: 280px; + overflow-y: auto; + position: relative; + + li.suggestion-item { + padding: 5px 10px; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #000; + background-color: #fff; + width: stretch; + + &.active, + &:hover { + color: #fff; + background-color: $color-link-visited; + } + } + } } diff --git a/app/components/tag_list_input_component/tag_list_input_controller.js b/app/components/tag_list_input_component/tag_list_input_controller.js index 094e3cca4a..5c91413a34 100644 --- a/app/components/tag_list_input_component/tag_list_input_controller.js +++ b/app/components/tag_list_input_component/tag_list_input_controller.js @@ -1,14 +1,23 @@ -import { Controller } from "stimulus"; +import { Autocomplete } from "stimulus-autocomplete"; -export default class extends Controller { - static targets = ["tagList", "newTag", "template", "list"]; +// Extend the stimulus-autocomplete controller, so we can load tag with existing rules +// The autocomplete functionality is only loaded if the url value is set +// For more informatioon on "stimulus-autocomplete", see: +// https://github.com/afcapel/stimulus-autocomplete/tree/main +// +export default class extends Autocomplete { + static targets = ["tagList", "input", "template", "list"]; static values = { onlyOne: Boolean }; - addTag(event) { - // prevent hotkey form submitting the form (default action for "enter" key) - event.preventDefault(); + connect() { + // Don't start autocomplete controller if we don't have an url + if (this.urlValue.length == 0) return; - const newTagName = this.newTagTarget.value.trim().replaceAll(" ", "-"); + super.connect(); + } + + addTag(event) { + const newTagName = this.inputTarget.value.trim().replaceAll(" ", "-"); if (newTagName.length == 0) { return; } @@ -18,7 +27,7 @@ export default class extends Controller { const index = tags.indexOf(newTagName); if (index != -1) { // highlight the value in red - this.newTagTarget.classList.add("tag-error"); + this.inputTarget.classList.add("tag-error"); return; } @@ -38,14 +47,23 @@ export default class extends Controller { this.listTarget.appendChild(newTagElement); // Clear new tag value - this.newTagTarget.value = ""; + this.inputTarget.value = ""; // hide tag input if limited to one tag if (this.tagListTarget.value.split(",").length == 1 && this.onlyOneValue == true) { - this.newTagTarget.style.display = "none"; + this.inputTarget.style.display = "none"; } } + keyboardAddTag(event) { + // prevent hotkey form submitting the form (default action for "enter" key) + if (event) { + event.preventDefault(); + } + + this.addTag(); + } + removeTag(event) { // Text to remove const tagName = event.srcElement.previousElementSibling.textContent; @@ -62,14 +80,14 @@ export default class extends Controller { // Make sure the tag input is displayed if (this.tagListTarget.value.length == 0) { - this.newTagTarget.style.display = "block"; + this.inputTarget.style.display = "block"; } } filterInput(event) { // clear error class if key is not enter if (event.key !== "Enter") { - this.newTagTarget.classList.remove("tag-error"); + this.inputTarget.classList.remove("tag-error"); } // Strip comma from tag name @@ -77,4 +95,53 @@ export default class extends Controller { event.srcElement.value = event.srcElement.value.replace(",", ""); } } + + // Add tag if we don't have an autocomplete list open + onBlur() { + // check if we have any autocomplete results + if (this.resultsTarget.childElementCount == 0) this.addTag(); + } + + // Override original to add tag filtering + replaceResults(html) { + const filteredHtml = this.#filterResults(html); + + // Don't show result if we don't have anything to show + if (filteredHtml.length == 0) return; + + super.replaceResults(filteredHtml); + } + + // Override original to all empty query, which will return all existing tags + onInputChange = () => { + if (this.urlValue.length == 0) return; + + if (this.hasHiddenTarget) this.hiddenTarget.value = ""; + + const query = this.inputTarget.value.trim(); + if (query.length >= this.minLengthValue) { + this.fetchResults(query); + } else { + this.hideAndRemoveOptions(); + } + }; + + //private + + #filterResults(html) { + const existingTags = this.tagListTarget.value.split(","); + // Parse the HTML + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + const lis = doc.getElementsByTagName("li"); + // Filter + let filteredHtml = ""; + for (let li of lis) { + if (!existingTags.includes(li.dataset.autocompleteValue)) { + filteredHtml += li.outerHTML; + } + } + + return filteredHtml; + } } diff --git a/app/components/tag_rule_form_component.rb b/app/components/tag_rule_form_component.rb index 8771bd2a64..c64a14f8b8 100644 --- a/app/components/tag_rule_form_component.rb +++ b/app/components/tag_rule_form_component.rb @@ -47,6 +47,13 @@ class TagRuleFormComponent < ViewComponent::Base taggable: "variant", visibility_field: "preferred_matched_variants_visibility", } + when "TagRule::FilterVariants" + { + text_top: t('components.tag_rule_form.tag_rules.variant_tagged_top'), + text_bottom: t('components.tag_rule_form.tag_rules.variant_tagged_bottom'), + taggable: "variant", + visibility_field: "preferred_matched_variants_visibility", + } end end diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 779a9e8065..71ae5c2f08 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -51,6 +51,7 @@ module Admin load_tag_rule_types + load_tag_rules return unless params[:stimulus] @enterprise.is_primary_producer = params[:is_primary_producer] @@ -84,6 +85,7 @@ module Admin end else load_tag_rule_types + load_tag_rules respond_with(@object) do |format| format.json { render json: { errors: @object.errors.messages }, status: :unprocessable_entity @@ -398,9 +400,26 @@ module Admin [t(".form.tag_rules.show_hide_order_cycles"), "FilterOrderCycles"] ] - return unless helpers.feature?(:inventory, @object) + if helpers.feature?(:variant_tag, @object) + @tag_rule_types.prepend([t(".form.tag_rules.show_hide_variants"), "FilterVariants"]) + elsif helpers.feature?(:inventory, @object) + @tag_rule_types.prepend([t(".form.tag_rules.show_hide_variants"), "FilterProducts"]) + end + end - @tag_rule_types.prepend([t(".form.tag_rules.show_hide_variants"), "FilterProducts"]) + def load_tag_rules + if helpers.feature?(:variant_tag, @object) + @default_rules = @enterprise.tag_rules.exclude_inventory.select(&:is_default) + @rules = @enterprise.tag_rules.exclude_inventory.prioritised.reject(&:is_default) + elsif helpers.feature?(:inventory, @object) + @default_rules = @enterprise.tag_rules.exclude_variant.select(&:is_default) + @rules = @enterprise.tag_rules.exclude_variant.prioritised.reject(&:is_default) + else + @default_rules = + @enterprise.tag_rules.exclude_inventory.exclude_variant.select(&:is_default) + @rules = + @enterprise.tag_rules.exclude_inventory.exclude_variant.prioritised.reject(&:is_default) + end end def setup_property diff --git a/app/controllers/admin/tag_rules_controller.rb b/app/controllers/admin/tag_rules_controller.rb index 360aea9f6d..620de16ee3 100644 --- a/app/controllers/admin/tag_rules_controller.rb +++ b/app/controllers/admin/tag_rules_controller.rb @@ -41,6 +41,7 @@ module Admin end end + # Used by the tag input autocomplete def map_by_tag respond_to do |format| format.json do @@ -50,6 +51,26 @@ module Admin end end + # Use to populate autocomplete with available rule for the given tag/enterprise + def variant_tag_rules + tag_rules = + TagRule.matching_variant_tag_rules_by_enterprises(params[:enterprise_id], params[:q]) + + @formatted_tag_rules = tag_rules.each_with_object({}) do |rule, mapping| + rule.preferred_variant_tags.split(",").each do |tag| + if mapping[tag] + mapping[tag][:rules] += 1 + else + mapping[tag] = { tag:, rules: 1 } + end + end + end.values + + respond_with do |format| + format.html { render :variant_tag_rules, layout: false } + end + end + private def collection_actions @@ -78,7 +99,7 @@ module Admin end def permitted_tag_rule_type - %w{FilterOrderCycles FilterPaymentMethods FilterProducts FilterShippingMethods} + %w{FilterOrderCycles FilterPaymentMethods FilterProducts FilterShippingMethods FilterVariants} end end end diff --git a/app/controllers/api/v0/order_cycles_controller.rb b/app/controllers/api/v0/order_cycles_controller.rb index df96577339..95302236ed 100644 --- a/app/controllers/api/v0/order_cycles_controller.rb +++ b/app/controllers/api/v0/order_cycles_controller.rb @@ -23,7 +23,8 @@ module Api order_cycle, customer, search_params, - inventory_enabled: + inventory_enabled:, + variant_tag_enabled: ).products_json render plain: products @@ -96,13 +97,17 @@ module Api def distributed_products OrderCycles::DistributedProductsService.new( - distributor, order_cycle, customer, inventory_enabled: + distributor, order_cycle, customer, inventory_enabled:, variant_tag_enabled:, ).products_relation.pluck(:id) end def inventory_enabled OpenFoodNetwork::FeatureToggle.enabled?(:inventory, distributor) end + + def variant_tag_enabled + OpenFoodNetwork::FeatureToggle.enabled?(:variant_tag, distributor) + end end end end diff --git a/app/models/spree/ability.rb b/app/models/spree/ability.rb index 949d2df281..155f611d56 100644 --- a/app/models/spree/ability.rb +++ b/app/models/spree/ability.rb @@ -143,7 +143,7 @@ module Spree can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty can :new, TagRule - can [:admin, :map_by_tag, :destroy], TagRule do |tag_rule| + can [:admin, :map_by_tag, :destroy, :variant_tag_rules], TagRule do |tag_rule| user.enterprises.include? tag_rule.enterprise end diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index 790e844334..989952e363 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -7,6 +7,8 @@ class TagRule < ApplicationRecord scope :for, ->(enterprise) { where(enterprise_id: enterprise) } scope :prioritised, -> { order('priority ASC') } + scope :exclude_inventory, -> { where.not(type: "TagRule::FilterProducts") } + scope :exclude_variant, -> { where.not(type: "TagRule::FilterVariants") } def self.mapping_for(enterprises) self.for(enterprises).each_with_object({}) do |rule, mapping| @@ -20,6 +22,14 @@ class TagRule < ApplicationRecord end end + def self.matching_variant_tag_rules_by_enterprises(enterprise_id, tag) + rules = where(type: "TagRule::FilterVariants").for(enterprise_id) + + return [] if rules.empty? + + rules.select { |r| r.preferred_variant_tags =~ /#{tag}/ } + end + # The following method must be overriden in a concrete tagRule def tags raise NotImplementedError, 'please use concrete TagRule' diff --git a/app/models/tag_rule/filter_variants.rb b/app/models/tag_rule/filter_variants.rb new file mode 100644 index 0000000000..fa93fdc5dc --- /dev/null +++ b/app/models/tag_rule/filter_variants.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class TagRule + class FilterVariants < TagRule + preference :matched_variants_visibility, :string, default: "visible" + preference :variant_tags, :string, default: "" + + def tags_match?(variant) + variant_tags = variant&.[]("tag_list") || [] + preferred_tags = preferred_variant_tags.split(",") + variant_tags.intersect?(preferred_tags) + end + + def reject_matched? + preferred_matched_variants_visibility != "visible" + end + + def tags + preferred_variant_tags + end + end +end diff --git a/app/services/order_cycles/distributed_products_service.rb b/app/services/order_cycles/distributed_products_service.rb index 71b0574d1e..bbaba0d1aa 100644 --- a/app/services/order_cycles/distributed_products_service.rb +++ b/app/services/order_cycles/distributed_products_service.rb @@ -118,13 +118,22 @@ module OrderCycles end def variants - options[:inventory_enabled] ? stocked_variants_and_overrides : stocked_variants + return tag_rule_filtered_variants if options[:variant_tag_enabled] + + return stocked_variants_and_overrides if options[:inventory_enabled] + + stocked_variants end def stocked_variants Spree::Variant.joins(:stock_items).where(query_stock) end + def tag_rule_filtered_variants + VariantTagRulesFilterer.new(distributor:, customer:, + variants_relation: stocked_variants).call + end + def stocked_variants_and_overrides stocked_variants = Spree::Variant. joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.variant_id = spree_variants.id diff --git a/app/services/product_tag_rules_filterer.rb b/app/services/product_tag_rules_filterer.rb index 36255e256d..6be63f57f3 100644 --- a/app/services/product_tag_rules_filterer.rb +++ b/app/services/product_tag_rules_filterer.rb @@ -9,7 +9,7 @@ # ( despite the use of `TagRule::FilterProducts.prioritised` ). It will apply the "show rule" # if any # * When there is no default rule, the order of customer related rules doesn't matter, it will -# apply the "hide rule" if any +# apply the "show rule" over any hide rule # class ProductTagRulesFilterer def initialize(distributor, customer, variants_relation) diff --git a/app/services/variant_tag_rules_filterer.rb b/app/services/variant_tag_rules_filterer.rb new file mode 100644 index 0000000000..d9cf2582c0 --- /dev/null +++ b/app/services/variant_tag_rules_filterer.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +# Takes a Spree::Variant AR object and filters results based on applicable tag rules. +# Tag rules exists in the context of enterprise, customer, and variants. +# Returns a Spree::Variant AR object. + +# The filtering is somewhat not intuitive when they are conflicting rules in play: +# * When a variant is hidden by a default rule, It will apply the "show rule" if any +# * When there is no default rule, it will apply the "show rule" over any "hide rule" +# +class VariantTagRulesFilterer + def initialize(distributor:, customer:, variants_relation: ) + @distributor = distributor + @customer = customer + @variants_relation = variants_relation + end + + def call + return variants_relation unless distributor_rules.any? + + filter(variants_relation) + end + + private + + attr_accessor :distributor, :customer, :variants_relation + + def distributor_rules + @distributor_rules ||= TagRule::FilterVariants.for(distributor).all + end + + def filter(variants_relation) + return variants_relation unless variants_to_hide.any? + + variants_relation.where(query_with_tag_rules) + end + + def query_with_tag_rules + "#{variant_not_hidden_by_rule} OR #{variant_shown_by_rule}" + end + + def variant_not_hidden_by_rule + return "FALSE" unless variants_to_hide.any? + + "spree_variants.id NOT IN (#{variants_to_hide.join(',')})" + end + + def variant_shown_by_rule + return "FALSE" unless variants_to_show.any? + + "spree_variants.id IN (#{variants_to_show.join(',')})" + end + + def variants_to_hide + @variants_to_hide ||= Spree::Variant.where(supplier: distributor) + .tagged_with(default_rule_tags + hide_rule_tags, any: true) + .pluck(:id) + end + + def variants_to_show + @variants_to_show ||= Spree::Variant.where(supplier: distributor) + .tagged_with(show_rule_tags, any: true) + .pluck(:id) + end + + def default_rule_tags + default_rules.map(&:preferred_variant_tags) + end + + def hide_rule_tags + hide_rules.map(&:preferred_variant_tags) + end + + def show_rule_tags + show_rules.map(&:preferred_variant_tags) + end + + def default_rules + # These rules hide a variant with tag X and apply to all customers + distributor_rules.select(&:is_default?) + end + + def non_default_rules + # These rules show or hide a variant with tag X for customer with tag Y + distributor_rules.reject(&:is_default?) + end + + def customer_applicable_rules + # Rules which apply specifically to the current customer + @customer_applicable_rules ||= non_default_rules.select{ |rule| customer_tagged?(rule) } + end + + def hide_rules + @hide_rules ||= customer_applicable_rules + .select{ |rule| rule.preferred_matched_variants_visibility == 'hidden' } + end + + def show_rules + customer_applicable_rules - hide_rules + end + + def customer_tagged?(rule) + customer_tag_list.include? rule.preferred_customer_tags + end + + def customer_tag_list + customer&.tag_list || [] + end +end diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index c5e0f98d01..24fc13747c 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -5,15 +5,14 @@ - # We use a high enough index increment so that the default tag rule should not overlap with the tag rules - # Rails will deal with non continous numbered tag_rules_attributes just fine, it saves us from having to manage the index state in javascript - current_rule_index = 1000 - - rules = @enterprise.tag_rules.prioritised.reject(&:is_default) - - if rules.empty? + - if @rules.empty? .no_tags = t('.no_tags_yet') = render 'admin/enterprises/form/tag_rules/default_rules', f:, current_rule_index: #customer-tag-rule - - tag_groups(rules).each_with_index do |group, group_index| + - tag_groups(@rules).each_with_index do |group, group_index| - current_group_index = group_index + 1 = render TagRuleGroupFormComponent.new(group:, index: group_index, customer_rule_index: current_rule_index, tag_rule_types: @tag_rule_types) - # Same as above, We use a high enough increcment so that the previous tag rule group does not overlap with the next tag rule group diff --git a/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml b/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml index 5f20fd9435..960b8b13d5 100644 --- a/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml +++ b/app/views/admin/enterprises/form/tag_rules/_default_rules.html.haml @@ -9,13 +9,12 @@ = t('.by_default') %i.text-big.icon-question-sign{ "data-controller": "help-modal-link", "data-action": "click->help-modal-link#open", "data-help-modal-link-target-value": "tag_rule_help_modal" } #default-tag-rule - - default_rules = @enterprise.tag_rules.select(&:is_default) - current_rule_index = 0 - - if default_rules.empty? + - if @default_rules.empty? .no_rules = t('.no_rules_yet') - else - - default_rules.each_with_index do |default_rule, index| + - @default_rules.each_with_index do |default_rule, index| - current_rule_index = index + 1 = render TagRuleFormComponent.new(rule: default_rule, index: index) %hr diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 4235c69a11..9d8fbeb999 100644 --- a/app/views/admin/products_v3/_variant_row.html.haml +++ b/app/views/admin/products_v3/_variant_row.html.haml @@ -80,7 +80,7 @@ = error_message_on variant, :tax_category - if feature?(:variant_tag, spree_current_user) %td.col-tags.field.naked_inputs - = render TagListInputComponent.new(name: f.field_name(:tag_list), tags: variant.tag_list, placeholder: t('.add_a_tag'), aria_label: t('admin.products_page.columns.tags')) + = render TagListInputComponent.new(name: f.field_name(:tag_list), tags: variant.tag_list, autocomplete_url: variant_tag_rules_admin_tag_rules_path(enterprise_id: variant.supplier_id), placeholder: t('.add_a_tag'), aria_label: t('admin.products_page.columns.tags')) %td.col-inherits_properties.align-left -# empty %td.align-right diff --git a/app/views/admin/tag_rules/variant_tag_rules.html.haml b/app/views/admin/tag_rules/variant_tag_rules.html.haml new file mode 100644 index 0000000000..e0318a7a54 --- /dev/null +++ b/app/views/admin/tag_rules/variant_tag_rules.html.haml @@ -0,0 +1,3 @@ +- @formatted_tag_rules.each do |tag_rule| + %li.suggestion-item{ role: "option", "data-autocomplete-value": tag_rule[:tag], "data-autocomplete-label": tag_rule[:tag] } + = t("admin.products_v3.tag_rules.rules_per_tag", tag: tag_rule[:tag], count: tag_rule[:rules]) diff --git a/app/webpacker/controllers/index.js b/app/webpacker/controllers/index.js index b02807e3d8..643ab3ed0b 100644 --- a/app/webpacker/controllers/index.js +++ b/app/webpacker/controllers/index.js @@ -7,6 +7,7 @@ import consumer from "../channels/consumer"; import controller from "../controllers/application_controller"; import CableReady from "cable_ready"; import RailsNestedForm from "@stimulus-components/rails-nested-form/dist/stimulus-rails-nested-form.umd.js"; // the default module entry point is broken +import { Autocomplete } from "stimulus-autocomplete"; const application = Application.start(); const context = require.context("controllers", true, /_controller\.js$/); @@ -37,6 +38,7 @@ contextComponents.keys().forEach((path) => { }); application.register("nested-form", RailsNestedForm); +application.register("autocomplete", Autocomplete); application.consumer = consumer; StimulusReflex.initialize(application, { controller, isolate: true }); diff --git a/app/webpacker/css/admin_v3/globals/variables.scss b/app/webpacker/css/admin_v3/globals/variables.scss index 2af50728ec..ef5705694e 100644 --- a/app/webpacker/css/admin_v3/globals/variables.scss +++ b/app/webpacker/css/admin_v3/globals/variables.scss @@ -186,3 +186,4 @@ $btn-condensed-height: 26px !default; //-------------------------------------------------------------- $tos-banner-z-index: 1001; $flash-message-z-index: 1000; +$tag-drop-down-z-index: 999; diff --git a/config/locales/en.yml b/config/locales/en.yml index a7aa68ffd3..589257fbfb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1026,6 +1026,10 @@ en: clone: success: Successfully cloned the product error: Unable to clone the product + tag_rules: + rules_per_tag: + one: "%{tag} has 1 rule" + other: "%{tag} has %{count} rules" product_import: title: Product Import file_not_found: File not found or could not be opened @@ -5077,6 +5081,8 @@ en: order_cycle_tagged_bottom: "are:" inventory_tagged_top: "Inventory variants tagged" inventory_tagged_bottom: "are:" + variant_tagged_top: "Variants tagged" + variant_tagged_bottom: "are:" visible: VISIBLE not_visible: NOT VISIBLE tag_rule_group_form: diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 5a0875ee64..b80d340bd4 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -91,8 +91,9 @@ Openfoodnetwork::Application.routes.draw do resources :customers, only: [:index, :create, :update, :destroy, :show] - resources :tag_rules, only: [], format: :json do - get :map_by_tag, on: :collection + resources :tag_rules, only: [] do + get :map_by_tag, on: :collection, format: :json + get :variant_tag_rules, on: :collection end resource :contents diff --git a/jest.config.js b/jest.config.js index 1886371139..b2e68f6dfa 100644 --- a/jest.config.js +++ b/jest.config.js @@ -66,7 +66,7 @@ module.exports = { // maxWorkers: "50%", // An array of directory names to be searched recursively up from the requiring module's location - moduleDirectories: ["node_modules", "app/webpacker"], + moduleDirectories: ["node_modules", "app/webpacker", "app/components"], // An array of file extensions your modules use // moduleFileExtensions: [ @@ -173,7 +173,7 @@ module.exports = { // transform: undefined, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - transformIgnorePatterns: ["/node_modules/(?!(stimulus)/)"], + transformIgnorePatterns: ["/node_modules/(?!(stimulus.+)/)"], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, diff --git a/package.json b/package.json index a3ace802fa..e120ee0278 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "select2": "^4.0.13", "shortcut-buttons-flatpickr": "^0.4.0", "stimulus": "^3.2.2", + "stimulus-autocomplete": "^3.1.0", "stimulus-flatpickr": "^1.4.0", "stimulus_reflex": "3.5.5", "tom-select": "^2.4.3", @@ -37,6 +38,7 @@ "webpack": "~4" }, "devDependencies": { + "@testing-library/dom": "<10.0.0", "jasmine-core": "~5.12.1", "jest": "^27.4.7", "karma": "~6.4.4", diff --git a/spec/controllers/admin/tag_rules_controller_spec.rb b/spec/controllers/admin/tag_rules_controller_spec.rb index bd955bee91..f1dccc2c4e 100644 --- a/spec/controllers/admin/tag_rules_controller_spec.rb +++ b/spec/controllers/admin/tag_rules_controller_spec.rb @@ -69,4 +69,53 @@ RSpec.describe Admin::TagRulesController do end end end + + describe "#variant_tag_rules", feature: :variant_tag do + render_views + + let(:enterprise) { create(:distributor_enterprise) } + let(:q) { "" } + let!(:rule1) { + create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "Tag-1", + preferred_variant_tags: "variant-tag-1" ) + } + let!(:rule2) { + create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "Tag-1", + preferred_variant_tags: "variant2-tag-1" ) + } + let!(:rule3) { + create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "organic", + preferred_variant_tags: "variant-organic" ) + } + let!(:rule4) { + create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "organic", + preferred_variant_tags: "variant-tag-1" ) + } + + before do + controller_login_as_enterprise_user [enterprise] + end + + it "returns a list of tag rules and number of assiciated rules" do + spree_get(:variant_tag_rules, format: :html, enterprise_id: enterprise.id, q:) + + expect(response).to render_template :variant_tag_rules + expect(response.body).to include "variant-tag-1 has 2 rules" + expect(response.body).to include "variant2-tag-1 has 1 rule" + expect(response.body).to include "variant-organic has 1 rule" + end + + context "with search string" do + let(:q) { "org" } + + it "returns a list of tag rules matching the string" do + spree_get(:variant_tag_rules, format: :html, enterprise_id: enterprise.id, q:) + + expect(response).to render_template :variant_tag_rules + expect(response.body).not_to include "variant-tag-1 has 2 rules" + expect(response.body).not_to include "variant2-tag-1 has 1 rule" + expect(response.body).to include "variant-organic has 1 rule" + end + end + end end diff --git a/spec/controllers/api/v0/order_cycles_controller_spec.rb b/spec/controllers/api/v0/order_cycles_controller_spec.rb index 0a55c57c61..9d14edcab7 100644 --- a/spec/controllers/api/v0/order_cycles_controller_spec.rb +++ b/spec/controllers/api/v0/order_cycles_controller_spec.rb @@ -132,7 +132,7 @@ RSpec.describe Api::V0::OrderCyclesController do end end - context "when tag rules apply", feature: :inventory do + context "when inventory tag rules apply", feature: :inventory do let!(:vo1) { create(:variant_override, hub: distributor, @@ -201,6 +201,63 @@ RSpec.describe Api::V0::OrderCyclesController do end end + context "when variant tag rules apply", feature: :variant_tag do + let!(:variant1) { product1.variants.first.tap { |v| v.update(supplier: distributor) } } + let!(:variant2) { product2.variants.first.tap { |v| v.update(supplier: distributor) } } + let!(:variant3) { product3.variants.first.tap { |v| v.update(supplier: distributor) } } + let(:default_hide_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + is_default: true, + preferred_variant_tags: "hide_these_variants_from_everyone", + preferred_matched_variants_visibility: "hidden") + } + let(:hide_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + preferred_variant_tags: "hide_these_variants", + preferred_customer_tags: "hide_from_these_customers", + preferred_matched_variants_visibility: "hidden" ) + } + let(:show_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + preferred_variant_tags: "show_these_variants", + preferred_customer_tags: "show_for_these_customers", + preferred_matched_variants_visibility: "visible" ) + } + + it "does not return variants hidden by general rules" do + variant1.update_attribute(:tag_list, default_hide_rule.preferred_variant_tags) + + api_get :products, id: order_cycle.id, distributor: distributor.id + + expect(product_ids).not_to include product1.id + end + + it "does not return variants hidden for this specific customer" do + variant2.update_attribute(:tag_list, hide_rule.preferred_variant_tags) + customer.update_attribute(:tag_list, hide_rule.preferred_customer_tags) + + api_get :products, id: order_cycle.id, distributor: distributor.id + + expect(product_ids).not_to include product2.id + end + + it "returns hidden variants made visible for this specific customer" do + variant1.update_attribute(:tag_list, default_hide_rule.preferred_variant_tags) + variant3.update_attribute(:tag_list, + "#{show_rule.preferred_variant_tags}," \ + "#{default_hide_rule.preferred_variant_tags}") + customer.update_attribute(:tag_list, show_rule.preferred_customer_tags) + + api_get :products, id: order_cycle.id, distributor: distributor.id + + expect(product_ids).not_to include product1.id + expect(product_ids).to include product3.id + end + end + context "when the order cycle is closed" do before do allow(controller).to receive(:order_cycle) { order_cycle } diff --git a/spec/factories/tag_rule_factory.rb b/spec/factories/tag_rule_factory.rb index e5a4b9286d..fb67ce303a 100644 --- a/spec/factories/tag_rule_factory.rb +++ b/spec/factories/tag_rule_factory.rb @@ -13,6 +13,10 @@ FactoryBot.define do enterprise factory: :distributor_enterprise end + factory :filter_variants_tag_rule, class: TagRule::FilterVariants do + enterprise factory: :distributor_enterprise + end + factory :filter_payment_methods_tag_rule, class: TagRule::FilterPaymentMethods do enterprise factory: :distributor_enterprise end diff --git a/spec/javascripts/stimulus/tag_list_input_controller_test.js b/spec/javascripts/stimulus/tag_list_input_controller_test.js index 91ed014be9..8ebd9b515d 100644 --- a/spec/javascripts/stimulus/tag_list_input_controller_test.js +++ b/spec/javascripts/stimulus/tag_list_input_controller_test.js @@ -3,23 +3,61 @@ */ import { Application } from "stimulus"; -import tag_list_input_controller from "../../../app/components/tag_list_input_component/tag_list_input_controller"; +import { screen } from "@testing-library/dom"; + +import tag_list_input_controller from "tag_list_input_component/tag_list_input_controller"; + +// Mock jest to return an autocomplete list +global.fetch = jest.fn(() => { + const html = ` +
  • + tag-1 has 1 rule +
  • +
  • + rule-2 has 2 rules +
  • `; + + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(html), + }); +}); describe("TagListInputController", () => { beforeAll(() => { const application = Application.start(); application.register("tag-list-input", tag_list_input_controller); + jest.useFakeTimers(); }); beforeEach(() => { // Tag input with three existing tags document.body.innerHTML = ` -
    +
    @@ -68,10 +106,12 @@ describe("TagListInputController", () => { name="variant_add_tag" id="variant_add_tag" placeholder="Add a tag" - data-action="keydown.enter->tag-list-input#addTag keyup->tag-list-input#filterInput" data-tag-list-input-target="newTag" + data-action="keydown.enter->tag-list-input#keyboardAddTag keyup->tag-list-input#filterInput focus->tag-list-input#onInputChange blur->tag-list-input#onBlur" + data-tag-list-input-target="input" style="display: block;" >
    +
    `; }); @@ -162,6 +202,8 @@ describe("TagListInputController", () => { document.body.innerHTML = `
    { name="variant_add_tag" id="variant_add_tag" placeholder="Add a tag" - data-action="keydown.enter->tag-list-input#addTag keyup->tag-list-input#filterInput" data-tag-list-input-target="newTag" + data-action="keydown.enter->tag-list-input#keyboardAddTag keyup->tag-list-input#filterInput blur->tag-list-input#onBlur focus->tag-list-input#onInputChange" + data-tag-list-input-target="input" style="display: block;" >
    +
    `; }); @@ -229,6 +273,8 @@ describe("TagListInputController", () => { document.body.innerHTML = `
    { name="variant_add_tag" id="variant_add_tag" placeholder="Add a tag" - data-action="keydown.enter->tag-list-input#addTag keyup->tag-list-input#filterInput" data-tag-list-input-target="newTag" - style="display: none;" + data-action="keydown.enter->tag-list-input#keyboardAddTag keyup->tag-list-input#filterInput blur->tag-list-input#onBlur focus->tag-list-input#onInputChange" + data-tag-list-input-target="input" + style="display: block;" >
    + `; }); @@ -300,4 +348,130 @@ describe("TagListInputController", () => { expect(variant_add_tag.classList).not.toContain("tag-error"); }); }); + + describe("onBlur", () => { + it("adds the tag", () => { + variant_add_tag.value = "newer_tag"; + variant_add_tag.dispatchEvent(new FocusEvent("blur")); + + expect(variant_tag_list.value).toBe("tag-1,tag-2,tag-3,newer_tag"); + }); + + describe("with autocomplete results", () => { + beforeEach(() => { + document.body.innerHTML = ` +
    + +
    +
    +
      + +
    • +
      + tag-1 + +
      +
    • +
    • +
      + tag-2 + +
      +
    • +
    • +
      + tag-3 + +
      +
    • +
    + +
    + +
    +
    `; + }); + + it("doesn't add the tag", () => { + variant_add_tag.value = "newer_tag"; + variant_add_tag.dispatchEvent(new FocusEvent("blur")); + + expect(variant_tag_list.value).toBe("tag-1,tag-2,tag-3"); + }); + }); + }); + + describe("replaceResults", () => { + beforeEach(() => { + fetch.mockClear(); + }); + + it("filters out existing tags in the autocomplete dropdown", async () => { + variant_add_tag.dispatchEvent(new FocusEvent("focus")); + // onInputChange uses a debounce function implemented using setTimeout + jest.runAllTimers(); + + // findAll* will wait for all promises to be finished before returning a result, this ensure + // the dom has been updated with the autocomplete data + const items = await screen.findAllByTestId("item"); + expect(items.length).toBe(1); + expect(items[0].textContent.trim()).toBe("rule-2 has 2 rules"); + }); + }); }); diff --git a/spec/models/tag_rule/filter_order_cycles_spec.rb b/spec/models/tag_rule/filter_order_cycles_spec.rb index 7616453cdd..c4eded6125 100644 --- a/spec/models/tag_rule/filter_order_cycles_spec.rb +++ b/spec/models/tag_rule/filter_order_cycles_spec.rb @@ -3,56 +3,69 @@ require 'spec_helper' RSpec.describe TagRule::FilterOrderCycles do - let!(:tag_rule) { build_stubbed(:filter_order_cycles_tag_rule) } + let(:tag_rule) { + build(:filter_order_cycles_tag_rule, preferred_exchange_tags: order_cycle_tags, enterprise:) + } + let(:order_cycle_tags) { "" } + let(:enterprise) { build(:enterprise) } describe "#tags" do - it "return the exchange tags" do - tag_rule = create(:filter_order_cycles_tag_rule, preferred_exchange_tags: "my_tag") + let(:order_cycle_tags) { "my_tag" } + it "return the exchange tags" do expect(tag_rule.tags).to eq("my_tag") end end - describe "determining whether tags match for a given exchange" do + describe "#tags_match?" do context "when the exchange is nil" do before do allow(tag_rule).to receive(:exchange_for) { nil } end it "returns false" do - expect(tag_rule.__send__(:tags_match?, nil)).to be false + expect(tag_rule.tags_match?(nil)).to be false end end context "when the exchange is not nil" do - let(:exchange_object) { double(:exchange, tag_list: ["member", "local", "volunteer"]) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [enterprise]) } before do - allow(tag_rule).to receive(:exchange_for) { exchange_object } + exchange = order_cycle.exchanges.outgoing.first + exchange.tag_list = "member,local,volunteer" + exchange.save! end context "when the rule has no preferred exchange tags specified" do - before { allow(tag_rule).to receive(:preferred_exchange_tags) { "" } } - it { expect(tag_rule.__send__(:tags_match?, exchange_object)).to be false } + it { expect(tag_rule.tags_match?(order_cycle)).to be false } end context "when the rule has preferred exchange tags specified that match ANY exchange tags" do - before { - allow(tag_rule).to receive(:preferred_exchange_tags) { - "wholesale,some_tag,member" - } - } - it { expect(tag_rule.__send__(:tags_match?, exchange_object)).to be true } + let(:order_cycle_tags) { "wholesale,some_tag,member" } + + it { expect(tag_rule.tags_match?(order_cycle)).to be true } end context "when the rule has preferred exchange tags specified that match NO exchange tags" do - before { - allow(tag_rule).to receive(:preferred_exchange_tags) { - "wholesale,some_tag,some_other_tag" - } - } - it { expect(tag_rule.__send__(:tags_match?, exchange_object)).to be false } + let(:order_cycle_tags) { "wholesale,some_tag,some_other_tag" } + + it { expect(tag_rule.tags_match?(order_cycle)).to be false } end end end + + describe "#reject_matched?" do + it "return false with default visibility (visible)" do + expect(tag_rule.reject_matched?).to be false + end + + context "when visiblity is set to hidden" do + let(:tag_rule) { + build(:filter_order_cycles_tag_rule, preferred_matched_order_cycles_visibility: "hidden") + } + + it { expect(tag_rule.reject_matched?).to be true } + end + end end diff --git a/spec/models/tag_rule/filter_payment_methods_spec.rb b/spec/models/tag_rule/filter_payment_methods_spec.rb index a846c5d618..fe7b4056b2 100644 --- a/spec/models/tag_rule/filter_payment_methods_spec.rb +++ b/spec/models/tag_rule/filter_payment_methods_spec.rb @@ -3,48 +3,59 @@ require 'spec_helper' RSpec.describe TagRule::FilterPaymentMethods do - let!(:tag_rule) { build_stubbed(:filter_payment_methods_tag_rule) } + let(:tag_rule) { + build(:filter_payment_methods_tag_rule, preferred_payment_method_tags: payment_method_tags) + } + let(:payment_method_tags) { "" } describe "#tags" do - it "return the payment method tags" do - tag_rule = create(:filter_payment_methods_tag_rule, preferred_payment_method_tags: "my_tag") + let(:payment_method_tags) { "my_tag" } + it "return the payment method tags" do expect(tag_rule.tags).to eq("my_tag") end end - describe "determining whether tags match for a given payment method" do + describe "#tags_match?" do context "when the payment method is nil" do it "returns false" do - expect(tag_rule.__send__(:tags_match?, nil)).to be false + expect(tag_rule.tags_match?(nil)).to be false end end context "when the payment method is not nil" do - let(:payment_method) { create(:payment_method, tag_list: ["member", "local", "volunteer"]) } + let(:payment_method) { build(:payment_method, tag_list: ["member", "local", "volunteer"]) } context "when the rule has no preferred payment method tags specified" do - before { allow(tag_rule).to receive(:preferred_payment_method_tags) { "" } } - it { expect(tag_rule.__send__(:tags_match?, payment_method)).to be false } + it { expect(tag_rule.tags_match?(payment_method)).to be false } end context "when the rule has preferred customer tags specified that match ANY customer tags" do - before { - allow(tag_rule).to receive(:preferred_payment_method_tags) { - "wholesale,some_tag,member" - } - } - it { expect(tag_rule.__send__(:tags_match?, payment_method)).to be true } + let(:payment_method_tags) { "wholesale,some_tag,member" } + + it { expect(tag_rule.tags_match?(payment_method)).to be true } end context "when the rule has preferred customer tags specified that match NO customer tags" do - before { - allow(tag_rule).to receive(:preferred_payment_method_tags) { - "wholesale,some_tag,some_other_tag" - } - } - it { expect(tag_rule.__send__(:tags_match?, payment_method)).to be false } + let(:payment_method_tags) { "wholesale,some_tag,some_other_tag" } + + it { expect(tag_rule.tags_match?(payment_method)).to be false } end end end + + describe "#reject_matched?" do + it "return false with default visibility (visible)" do + expect(tag_rule.reject_matched?).to be false + end + + context "when visiblity is set to hidden" do + let(:tag_rule) { + build(:filter_payment_methods_tag_rule, + preferred_matched_payment_methods_visibility: "hidden") + } + + it { expect(tag_rule.reject_matched?).to be true } + end + end end diff --git a/spec/models/tag_rule/filter_products_spec.rb b/spec/models/tag_rule/filter_products_spec.rb index 41bfbaeba9..58e0ec7409 100644 --- a/spec/models/tag_rule/filter_products_spec.rb +++ b/spec/models/tag_rule/filter_products_spec.rb @@ -3,20 +3,23 @@ require 'spec_helper' RSpec.describe TagRule::FilterProducts do - let!(:tag_rule) { build_stubbed(:filter_products_tag_rule, preferred_variant_tags: "my_tag") } + let(:tag_rule) { build(:filter_products_tag_rule, preferred_variant_tags: variant_tags) } + let(:variant_tags) { "" } describe "#tags" do - it "return the variants tags" do - tag_rule = create(:filter_products_tag_rule, preferred_variant_tags: "my_tag") + let(:variant_tags) { "my_tag" } + it "return the variants tags" do expect(tag_rule.tags).to eq("my_tag") end end - describe "determining whether tags match for a given variant" do + describe "#tags_match?" do + let(:variant_tags) { "my_tag" } + context "when the variant is nil" do it "returns false" do - expect(tag_rule.__send__(:tags_match?, nil)).to be false + expect(tag_rule.tags_match?(nil)).to be false end end @@ -24,27 +27,34 @@ RSpec.describe TagRule::FilterProducts do let(:variant_object) { { "tag_list" => ["member", "local", "volunteer"] } } context "when the rule has no preferred variant tags specified" do - before { allow(tag_rule).to receive(:preferred_variant_tags) { "" } } - it { expect(tag_rule.__send__(:tags_match?, variant_object)).to be false } + it { expect(tag_rule.tags_match?(variant_object)).to be false } end context "when the rule has preferred variant tags specified that match ANY variant tags" do - before { - allow(tag_rule).to receive(:preferred_variant_tags) { - "wholesale,some_tag,member" - } - } - it { expect(tag_rule.__send__(:tags_match?, variant_object)).to be true } + let(:variant_tags) { "wholesale,some_tag,member" } + + it { expect(tag_rule.tags_match?(variant_object)).to be true } end context "when the rule has preferred variant tags specified that match NO variant tags" do - before { - allow(tag_rule).to receive(:preferred_variant_tags) { - "wholesale,some_tag,some_other_tag" - } - } - it { expect(tag_rule.__send__(:tags_match?, variant_object)).to be false } + let(:variant_tags) { "wholesale,some_tag,some_other_tag" } + + it { expect(tag_rule.tags_match?(variant_object)).to be false } end end end + + describe "#reject_matched?" do + it "return false with default visibility (visible)" do + expect(tag_rule.reject_matched?).to be false + end + + context "when visiblity is set to hidden" do + let(:tag_rule) { + build(:filter_products_tag_rule, preferred_matched_variants_visibility: "hidden") + } + + it { expect(tag_rule.reject_matched?).to be true } + end + end end diff --git a/spec/models/tag_rule/filter_shipping_methods_spec.rb b/spec/models/tag_rule/filter_shipping_methods_spec.rb index 0222507fcd..022df21ee0 100644 --- a/spec/models/tag_rule/filter_shipping_methods_spec.rb +++ b/spec/models/tag_rule/filter_shipping_methods_spec.rb @@ -3,50 +3,61 @@ require 'spec_helper' RSpec.describe TagRule::FilterShippingMethods do - let!(:tag_rule) { build_stubbed(:filter_shipping_methods_tag_rule) } + let(:tag_rule) { + build(:filter_shipping_methods_tag_rule, preferred_shipping_method_tags: shipping_method_tags) + } + let(:shipping_method_tags) { "" } describe "#tags" do - it "return the shipping method tags" do - tag_rule = create(:filter_shipping_methods_tag_rule, preferred_shipping_method_tags: "my_tag") + let(:shipping_method_tags) { "my_tag" } + it "return the shipping method tags" do expect(tag_rule.tags).to eq("my_tag") end end - describe "determining whether tags match for a given shipping method" do + describe "#tags_match?" do context "when the shipping method is nil" do it "returns false" do - expect(tag_rule.__send__(:tags_match?, nil)).to be false + expect(tag_rule.tags_match?(nil)).to be false end end context "when the shipping method is not nil" do let(:shipping_method) { - build_stubbed(:shipping_method, tag_list: ["member", "local", "volunteer"]) + build(:shipping_method, tag_list: ["member", "local", "volunteer"]) } context "when the rule has no preferred shipping method tags specified" do - before { allow(tag_rule).to receive(:preferred_shipping_method_tags) { "" } } - it { expect(tag_rule.__send__(:tags_match?, shipping_method)).to be false } + it { expect(tag_rule.tags_match?(shipping_method)).to be false } end context "when rule has preferred customer tags specified that match ANY customer tags" do - before { - allow(tag_rule).to receive(:preferred_shipping_method_tags) { - "wholesale,some_tag,member" - } - } - it { expect(tag_rule.__send__(:tags_match?, shipping_method)).to be true } + let(:shipping_method_tags) { "wholesale,some_tag,member" } + + it { expect(tag_rule.tags_match?(shipping_method)).to be true } end context "when rule has preferred customer tags specified that match NO customer tags" do - before { - allow(tag_rule).to receive(:preferred_shipping_method_tags) { - "wholesale,some_tag,some_other_tag" - } - } - it { expect(tag_rule.__send__(:tags_match?, shipping_method)).to be false } + let(:shipping_method_tags) { "wholesale,some_tag,some_other_tag" } + + it { expect(tag_rule.tags_match?(shipping_method)).to be false } end end end + + describe "#reject_matched?" do + it "return false with default visibility (visible)" do + expect(tag_rule.reject_matched?).to be false + end + + context "when visiblity is set to hidden" do + let(:tag_rule) { + build(:filter_shipping_methods_tag_rule, + preferred_matched_shipping_methods_visibility: "hidden") + } + + it { expect(tag_rule.reject_matched?).to be true } + end + end end diff --git a/spec/models/tag_rule/filter_variants_spec.rb b/spec/models/tag_rule/filter_variants_spec.rb new file mode 100644 index 0000000000..a9da63d407 --- /dev/null +++ b/spec/models/tag_rule/filter_variants_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe TagRule::FilterVariants do + let(:tag_rule) { build(:filter_variants_tag_rule, preferred_variant_tags: variant_tags) } + let(:variant_tags) { "" } + + describe "#tags" do + let(:variant_tags) { "my_tag" } + + it "return the variants tags" do + expect(tag_rule.tags).to eq("my_tag") + end + end + + describe "#tags_match?" do + let(:variant_tags) { "my_tag" } + + context "when the variant is nil" do + it "returns false" do + expect(tag_rule.tags_match?(nil)).to be false + end + end + + context "when the variant is not nil" do + let(:variant_object) { { "tag_list" => ["member", "local", "volunteer"] } } + + context "when the rule has no preferred variant tags specified" do + it { expect(tag_rule.tags_match?(variant_object)).to be false } + end + + context "when the rule has preferred variant tags specified that match ANY variant tags" do + let(:variant_tags) { "wholesale,some_tag,member" } + + it { expect(tag_rule.tags_match?(variant_object)).to be true } + end + + context "when the rule has preferred variant tags specified that match NO variant tags" do + let(:variant_tags) { "wholesale,some_tag,some_other_tag" } + + it { expect(tag_rule.tags_match?(variant_object)).to be false } + end + end + end + + describe "#reject_matched?" do + it "return false with default visibility (visible)" do + expect(tag_rule.reject_matched?).to be false + end + + context "when visiblity is set to hidden" do + let(:tag_rule) { + build(:filter_variants_tag_rule, preferred_matched_variants_visibility: "hidden") + } + + it { expect(tag_rule.reject_matched?).to be true } + end + end +end diff --git a/spec/models/tag_rule_spec.rb b/spec/models/tag_rule_spec.rb index b4b53310ae..eb27a088de 100644 --- a/spec/models/tag_rule_spec.rb +++ b/spec/models/tag_rule_spec.rb @@ -9,6 +9,40 @@ RSpec.describe TagRule do end end + describe ".matching_variant_tag_rules_by_enterprises" do + let(:enterprise) { create(:enterprise) } + let!(:rule1) { + create(:filter_variants_tag_rule, enterprise:, preferred_variant_tags: "filtered" ) + } + let!(:rule2) { + create(:filter_variants_tag_rule, enterprise:, preferred_variant_tags: "filtered" ) + } + let!(:rule3) { + create(:filter_variants_tag_rule, enterprise: create(:enterprise), + preferred_variant_tags: "filtered" ) + } + let!(:rule4) { + create(:filter_variants_tag_rule, enterprise:, preferred_variant_tags: "other-tag" ) + } + let!(:rule5) { + create(:filter_order_cycles_tag_rule, enterprise:, preferred_exchange_tags: "filtered" ) + } + + it "returns a list of rule partially matching the tag" do + rules = described_class.matching_variant_tag_rules_by_enterprises(enterprise.id, "filte") + + expect(rules).to include rule1, rule2 + expect(rules).not_to include rule3, rule4, rule5 + end + + context "when no matching rules" do + it "returns an empty array" do + rules = described_class.matching_variant_tag_rules_by_enterprises(enterprise.id, "no-tag") + expect(rules).to eq([]) + end + end + end + describe '#tags' do subject(:rule) { Class.new(TagRule).new } diff --git a/spec/services/order_cycles/distributed_products_service_spec.rb b/spec/services/order_cycles/distributed_products_service_spec.rb index ff6b9a55a4..2766219b0d 100644 --- a/spec/services/order_cycles/distributed_products_service_spec.rb +++ b/spec/services/order_cycles/distributed_products_service_spec.rb @@ -68,6 +68,20 @@ RSpec.describe OrderCycles::DistributedProductsService do expect(products_relation).not_to include product end + + context "with variant_tag enabled" do + subject(:products_relation) { + described_class.new( + distributor, order_cycle, customer, variant_tag_enabled: true + ).products_relation + } + + it "calls VariantTagRulesFilterer" do + expect(VariantTagRulesFilterer).to receive(:new).and_call_original + + products_relation + end + end end context "with variant overrides" do @@ -81,6 +95,12 @@ RSpec.describe OrderCycles::DistributedProductsService do create(:variant_override, hub: distributor, variant:, count_on_hand: 0) } + it "calls ProductTagRulesFilterer" do + expect(ProductTagRulesFilterer).to receive(:new).and_call_original + + products_relation + end + it "does not return product when an override is out of stock" do expect(products_relation).not_to include product end diff --git a/spec/services/variant_tag_rules_filterer_spec.rb b/spec/services/variant_tag_rules_filterer_spec.rb new file mode 100644 index 0000000000..ee74416509 --- /dev/null +++ b/spec/services/variant_tag_rules_filterer_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe VariantTagRulesFilterer do + subject(:filterer) { described_class.new(distributor:, customer:, variants_relation:) } + + let(:distributor) { create(:distributor_enterprise) } + let(:product) { create(:product) } + let!(:variant_hidden_by_default) { create(:variant, product:, supplier: distributor) } + let!(:variant_hidden_by_rule) { create(:variant, product:, supplier: distributor) } + let!(:variant_shown_by_rule) { create(:variant, product:, supplier: distributor) } + let!(:variant_hidden_for_another_customer) { create(:variant, product:, supplier: distributor) } + let(:customer) { create(:customer, enterprise: distributor) } + let(:variants_relation) { Spree::Variant.where(supplier: distributor) } + + describe "#call" do + let!(:hide_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + preferred_variant_tags: "hide_these_variants", + preferred_customer_tags: "hide_from_these_customers", + preferred_matched_variants_visibility: "hidden" ) + } + let!(:show_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + preferred_variant_tags: "show_these_variants", + preferred_customer_tags: "show_for_these_customers", + preferred_matched_variants_visibility: "visible" ) + } + let!(:non_applicable_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + preferred_variant_tags: "hide_these_other_variants", + preferred_customer_tags: "hide_from_other_customers", + preferred_matched_variants_visibility: "hidden" ) + } + + context "when the distributor has no rules" do + it "returns the relation unchanged" do + expect(filterer.call).to eq variants_relation + end + end + + context "with hide rule" do + it "hides the variant matching the rule" do + customer.update_attribute(:tag_list, hide_rule.preferred_customer_tags) + variant_hidden_by_rule.update_attribute(:tag_list, hide_rule.preferred_variant_tags) + + expect(filterer.call).not_to include(variant_hidden_by_rule) + end + + context "with mutiple conflicting rules" do + it "applies the show rule" do + # Customer has show rule tag and hide rule tag + customer.update_attribute( + :tag_list, [hide_rule.preferred_customer_tags, show_rule.preferred_customer_tags] + ) + # Variant has show rule tag and hide rule tag + variant_hidden_by_rule.update_attribute( + :tag_list, [hide_rule.preferred_variant_tags, show_rule.preferred_variant_tags,] + ) + expect(filterer.call).to include(variant_hidden_by_rule) + end + end + end + + context "with variant hidden by default" do + let(:default_hide_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + is_default: true, + preferred_variant_tags: "hide_these_variants_from_everyone", + preferred_matched_variants_visibility: "hidden") + } + + before do + variant_hidden_by_default.update_attribute( + :tag_list, default_hide_rule.preferred_variant_tags + ) + end + + it "excludes variant hidden by default" do + expect(filterer.call).not_to include(variant_hidden_by_default) + end + + context "with variant rule overriding default rule" do + it "includes variant hidden by default" do + customer.update_attribute(:tag_list, show_rule.preferred_customer_tags) + # Variant has default rule tag and show rule tag + variant_hidden_by_default.update_attribute( + :tag_list, [default_hide_rule.preferred_variant_tags, show_rule.preferred_variant_tags] + ) + + expect(filterer.call).to include(variant_hidden_by_default) + end + + context "with mutiple conflicting rules applying to same variant" do + it "applies the show rule" do + # customer has show rule and hide rule tag + customer.update_attribute( + :tag_list, [show_rule.preferred_customer_tags, hide_rule.preferred_customer_tags] + ) + + # Variant has default rule tag and show rule tag and hide rule tag + variant_hidden_by_default.update_attribute( + :tag_list, + [default_hide_rule.preferred_variant_tags, show_rule.preferred_variant_tags, + hide_rule.preferred_variant_tags] + ) + + expect(filterer.call).to include(variant_hidden_by_default) + end + end + end + end + end +end diff --git a/spec/system/admin/enterprises_spec.rb b/spec/system/admin/enterprises_spec.rb index f783cea3b3..a26d515bd3 100644 --- a/spec/system/admin/enterprises_spec.rb +++ b/spec/system/admin/enterprises_spec.rb @@ -733,19 +733,16 @@ RSpec.describe ' it_behaves_like "edit link with", "openfoodnetwork.org", "http://openfoodnetwork.org" end - shared_examples "edit link with invalid" do |url| - it "url: #{url}" do - fill_in "enterprise_white_label_logo_link", with: url + context "with an invalid link" do + it "can not edit white label logo link" do + fill_in "enterprise_white_label_logo_link", with: "invalid url" click_button 'Update' - expect(page) - .to have_content "Link for the logo used in shopfront '#{url}' is an invalid URL" + expect(page).to have_content( + "Link for the logo used in shopfront 'invalid url' is an invalid URL" + ) expect(distributor1.reload.white_label_logo_link).to be_nil end end - - context "can not edit white label logo link" do - it_behaves_like "edit link with invalid", "invalid url" - end end it "can check/uncheck the hide_groups_tab attribute" do diff --git a/spec/system/admin/tag_rules_spec.rb b/spec/system/admin/tag_rules_spec.rb index 124d8de7d6..8f7e0c5e37 100644 --- a/spec/system/admin/tag_rules_spec.rb +++ b/spec/system/admin/tag_rules_spec.rb @@ -8,6 +8,71 @@ RSpec.describe 'Tag Rules' do let!(:enterprise) { create(:distributor_enterprise) } + describe "loading rules" do + let!(:default_order_cycle_tag_rule) { + create(:filter_order_cycles_tag_rule, enterprise:, is_default: true) + } + let!(:inventory_order_cycle_rule) { create(:filter_order_cycles_tag_rule, enterprise:) } + let!(:default_inventory_tag_rule) { + create(:filter_products_tag_rule, enterprise:, is_default: true) + } + let!(:inventory_tag_rule) { create(:filter_products_tag_rule, enterprise:) } + let!(:default_variant_tag_rule) { + create(:filter_variants_tag_rule, enterprise:, is_default: true) + } + let!(:variant_tag_rule) { create(:filter_variants_tag_rule, enterprise:) } + + before do + visit_tag_rules + end + + it "displays all existing rules" do + within "#default-tag-rule" do + expect(page).to have_content "Order Cycles tagged" + expect(page).not_to have_content "Inventory variants tagged" + expect(page).not_to have_content "Variants tagged" + end + + within "#customer-tag-rule" do + expect(page).to have_content "Order Cycles tagged" + expect(page).not_to have_content "Inventory variants tagged" + expect(page).not_to have_content "Variants tagged" + end + end + + context "with inventory enabled", feature: :inventory do + it "does not display filter by variants rules" do + within "#default-tag-rule" do + expect(page).to have_content "Order Cycles tagged" + expect(page).to have_content "Inventory variants tagged" + expect(page).not_to have_content "Variants tagged" + end + + within "#customer-tag-rule" do + expect(page).to have_content "Order Cycles tagged" + expect(page).to have_content "Inventory variants tagged" + expect(page).not_to have_content "Variants tagged" + end + end + end + + context "with variant tag enabled", feature: :variant_tag do + it "does not display filter by inventory variants rules" do + within "#default-tag-rule" do + expect(page).to have_content "Order Cycles tagged" + expect(page).not_to have_content "Inventory variants tagged" + expect(page).to have_content "Variants tagged" + end + + within "#customer-tag-rule" do + expect(page).to have_content "Order Cycles tagged" + expect(page).not_to have_content "Inventory variants tagged" + expect(page).to have_content "Variants tagged" + end + end + end + end + context "creating" do before do visit_tag_rules @@ -90,6 +155,33 @@ RSpec.describe 'Tag Rules' do expect(tag_rule.preferred_matched_order_cycles_visibility).to eq "hidden" end + context "when variant_tag enabled", feature: :variant_tag do + it "allows creation of filter variant type" do + # Creating a new tag + expect(page).to have_content 'No tags apply to this enterprise yet' + click_button '+ Add A New Tag' + fill_in_tag "New-Product" + + # New FilterProducts Rule + click_button '+ Add A New Rule' + tomselect_select 'Show or Hide variants in my shop', from: 'rule_type_selector' + click_button "Add Rule" + within("#customer-tag-rule #tr_1001") do + fill_in_tag "new-product" + tomselect_select "VISIBLE", + from: "enterprise_tag_rules_attributes_1001_preferred_matched_" \ + "variants_visibility" + end + + click_button 'Update' + + tag_rule = TagRule::FilterVariants.last + expect(tag_rule.preferred_customer_tags).to eq "New-Product" + expect(tag_rule.preferred_variant_tags).to eq "new-product" + expect(tag_rule.preferred_matched_variants_visibility).to eq "visible" + end + end + context "when inventory enabled", feature: :inventory do it "allows creation of filter variant type" do # Creating a new tag @@ -118,7 +210,7 @@ RSpec.describe 'Tag Rules' do end end - context "updating" do + context "updating", feature: :inventory do let!(:default_fsm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise:, preferred_matched_shipping_methods_visibility: @@ -242,10 +334,10 @@ RSpec.describe 'Tag Rules' do context "deleting" do let!(:tag_rule) { - create(:filter_products_tag_rule, enterprise:, preferred_customer_tags: "member" ) + create(:filter_order_cycles_tag_rule, enterprise:, preferred_customer_tags: "member" ) } let!(:default_rule) { - create(:filter_products_tag_rule, is_default: true, enterprise: ) + create(:filter_order_cycles_tag_rule, is_default: true, enterprise: ) } before do diff --git a/yarn.lock b/yarn.lock index 730a6f4d5d..e6698b3a31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,15 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.10.4": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/code-frame@^7.22.13": version "7.22.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" @@ -339,6 +348,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + "@babel/helper-validator-option@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" @@ -1036,6 +1050,11 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== +"@babel/runtime@^7.12.5": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== + "@babel/runtime@^7.15.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" @@ -1502,11 +1521,30 @@ resolved "https://registry.yarnpkg.com/@stimulus-components/rails-nested-form/-/rails-nested-form-5.0.0.tgz#b443ad8ba5220328cfd704ca956ebf95ab8c4848" integrity sha512-qrmmurT+KBPrz9iBlyrgJa6Di8i0j328kSk2SUR53nK5W0kDhw1YxVC91aUR+7EsFKiwJT1iB7oDSwpDhDQPeA== +"@testing-library/dom@<10.0.0": + version "9.3.4" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.4.tgz#50696ec28376926fec0a1bf87d9dbac5e27f60ce" + integrity sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.1.3" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.1.15" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.15.tgz#2ccfb1ad55a02c83f8e0ad327cbc332f55eb1024" @@ -1959,6 +1997,13 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +aria-query@5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== + dependencies: + deep-equal "^2.0.5" + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -1974,6 +2019,14 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-buffer-byte-length@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== + dependencies: + call-bound "^1.0.3" + is-array-buffer "^3.0.5" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -2567,6 +2620,16 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.5, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -2578,17 +2641,7 @@ call-bind@^1.0.7: get-intrinsic "^1.2.4" set-function-length "^1.2.1" -call-bind@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - -call-bound@^1.0.3, call-bound@^1.0.4: +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== @@ -2667,6 +2720,14 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -3405,6 +3466,30 @@ deep-equal@^1.0.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" +deep-equal@^2.0.5: + version "2.2.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" + integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.5" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.2" + is-arguments "^1.1.1" + is-array-buffer "^3.0.2" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.13" + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -3423,7 +3508,7 @@ default-gateway@^4.2.0: execa "^1.0.0" ip-regex "^2.1.0" -define-data-property@^1.1.4: +define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== @@ -3447,6 +3532,15 @@ define-properties@^1.2.0: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" @@ -3564,6 +3658,11 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + dom-serialize@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" @@ -3803,6 +3902,21 @@ es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== +es-get-iterator@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" @@ -4370,7 +4484,7 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" -get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: +get-intrinsic@^1.2.2, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -4920,6 +5034,15 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -4972,6 +5095,23 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-arguments@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" + integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-array-buffer@^3.0.2, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -5061,6 +5201,14 @@ is-date-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== +is-date-object@^1.0.5: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -5130,6 +5278,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-map@^2.0.2, is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + is-negative-zero@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" @@ -5209,11 +5362,33 @@ is-regex@^1.1.2: call-bind "^1.0.2" has-symbols "^1.0.1" +is-regex@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + is-resolvable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== +is-set@^2.0.2, is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== + dependencies: + call-bound "^1.0.3" + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -5229,6 +5404,14 @@ is-string@^1.0.5: resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== +is-string@^1.0.7: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" @@ -5248,6 +5431,19 @@ is-typedarray@^1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakset@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== + dependencies: + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -6096,6 +6292,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + make-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -6601,6 +6802,11 @@ object-inspect@^1.13.1: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + object-inspect@^1.9.0: version "1.12.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" @@ -6614,6 +6820,14 @@ object-is@^1.0.1: call-bind "^1.0.2" define-properties "^1.1.3" +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -6636,6 +6850,18 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" +object.assign@^4.1.4: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + object.getownpropertydescriptors@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7" @@ -7675,7 +7901,7 @@ prettier@3.2.4: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.4.tgz#4723cadeac2ce7c9227de758e5ff9b14e075f283" integrity sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ== -pretty-format@^27.5.1: +pretty-format@^27.0.2, pretty-format@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== @@ -7965,6 +8191,18 @@ regexp.prototype.flags@^1.2.0: define-properties "^1.2.0" functions-have-names "^1.2.3" +regexp.prototype.flags@^1.5.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" + regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" @@ -8326,6 +8564,16 @@ set-function-length@^1.2.1, set-function-length@^1.2.2: gopd "^1.0.1" has-property-descriptors "^1.0.2" +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -8389,6 +8637,46 @@ shortcut-buttons-flatpickr@^0.4.0: resolved "https://registry.yarnpkg.com/shortcut-buttons-flatpickr/-/shortcut-buttons-flatpickr-0.4.0.tgz#a36e0a88a670ed2637b7b1adb5bee0914c29a7e7" integrity sha512-JKmT4my3Hm1e18OvG4Q6RcFhN4WRqqpTMkHrvZ7fup/dp6aTIWGVCHdRYtASkp/FCzDlJh6iCLQ/VcwwNpAMoQ== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.4, side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + side-channel@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" @@ -8635,6 +8923,11 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stimulus-autocomplete@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/stimulus-autocomplete/-/stimulus-autocomplete-3.1.0.tgz#7c9292706556ed0a87abf60ea2688bf0ea1176a8" + integrity sha512-SmVViCdA8yCl99oV2kzllNOqYjx7wruY+1OjAVsDTkZMNFZG5j+SqDKHMYbu+dRFy/SWq/PParzwZHvLAgH+YA== + stimulus-flatpickr@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/stimulus-flatpickr/-/stimulus-flatpickr-1.4.0.tgz#a41071a3e69cfc50b7eaaacf356fc0ab1ab0543c" @@ -8657,6 +8950,14 @@ stimulus_reflex@3.5.5: "@rails/actioncable" "^6 || ^7 || ^8" cable_ready "^5.0.6" +stop-iteration-iterator@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== + dependencies: + es-errors "^1.3.0" + internal-slot "^1.1.0" + stream-browserify@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" @@ -9619,12 +9920,22 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-collection@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which-typed-array@^1.1.16: +which-typed-array@^1.1.13, which-typed-array@^1.1.16: version "1.1.19" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==