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 = `
-
+
`;
});
@@ -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 = `
+ `;
+ });
+
+ 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==