Merge pull request #13592 from rioug/13266-tag-variant-tag-rule

[Variant tags] Add tag rules for variant
This commit is contained in:
Rachel Arnould
2025-11-07 14:14:50 +01:00
committed by GitHub
39 changed files with 1436 additions and 150 deletions

View File

@@ -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

View File

@@ -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 }

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View File

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

View File

@@ -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;

View File

@@ -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:

View File

@@ -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

View File

@@ -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,

View File

@@ -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",

View File

@@ -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

View File

@@ -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 }

View File

@@ -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

View File

@@ -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 = `
<li
data-testid="item"
class="suggestion-item"
data-autocomplete-label="tag-1"
data-autocomplete-value="tag-1"
role="option"
id="stimulus-autocomplete-option-4"
>
tag-1 has 1 rule
</li>
<li
data-testid="item"
class="suggestion-item"
data-autocomplete-label="rule-2"
data-autocomplete-value="rule-2"
role="option"
id="stimulus-autocomplete-option-5"
>
rule-2 has 2 rules
</li>`;
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 = `
<div data-controller="tag-list-input">
<div
data-controller="tag-list-input"
data-action="autocomplete.change->tag-list-input#addTag"
data-tag-list-input-url-value="/admin/tag_rules/variant_tag_rules?enterprise_id=3"
>
<input
value="tag-1,tag-2,tag-3"
data-tag-list-input-target="tagList"
type="hidden"
name="variant_tag_list" id="variant_tag_list"
name="variant_tag_list"
id="variant_tag_list"
>
<div class="tags-input">
<div class="tags">
@@ -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;"
>
</div>
<ul data-testid="suggestion-list" class="suggestion-list" data-tag-list-input-target="results" hidden></ul>
</div>
</div>`;
});
@@ -162,6 +202,8 @@ describe("TagListInputController", () => {
document.body.innerHTML = `
<div
data-controller="tag-list-input"
data-action="autocomplete.change->tag-list-input#addTag"
data-tag-list-input-url-value="/admin/tag_rules/variant_tag_rules?enterprise_id=3"
data-tag-list-input-only-one-value="true"
>
<input
@@ -190,10 +232,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 blur->tag-list-input#onBlur focus->tag-list-input#onInputChange"
data-tag-list-input-target="input"
style="display: block;"
>
</div>
<ul class="suggestion-list" data-tag-list-input-target="results" hidden></ul>
</div>
</div>`;
});
@@ -229,6 +273,8 @@ describe("TagListInputController", () => {
document.body.innerHTML = `
<div
data-controller="tag-list-input"
data-action="autocomplete.change->tag-list-input#addTag"
data-tag-list-input-url-value="/admin/tag_rules/variant_tag_rules?enterprise_id=3"
data-tag-list-input-only-one-value="true"
>
<input
@@ -266,10 +312,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"
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;"
>
</div>
<ul class="suggestion-list" data-tag-list-input-target="results" hidden></ul>
</div>
</div>`;
});
@@ -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 = `
<div
data-controller="tag-list-input"
data-action="autocomplete.change->tag-list-input#addTag"
data-tag-list-input-url-value="/admin/tag_rules/variant_tag_rules?enterprise_id=3"
>
<input
value="tag-1,tag-2,tag-3"
data-tag-list-input-target="tagList"
type="hidden"
name="variant_tag_list"
id="variant_tag_list"
>
<div class="tags-input">
<div class="tags">
<ul class="tag-list" data-tag-list-input-target="list">
<template data-tag-list-input-target="template">
<li class="tag-item">
<div class="tag-template">
<span></span>
<a
class="remove-button"
data-action="click->tag-list-input#removeTag"
>✖</a>
</div>
</li>
</template>
<li class="tag-item">
<div class="tag-template">
<span>tag-1</span>
<a
class="remove-button"
data-action="click->tag-list-input#removeTag"
>✖</a>
</div>
</li>
<li class="tag-item">
<div class="tag-template">
<span>tag-2</span>
<a
class="remove-button"
data-action="click->tag-list-input#removeTag"
>✖</a>
</div>
</li>
<li class="tag-item">
<div class="tag-template">
<span>tag-3</span>
<a
class="remove-button"
data-action="click->tag-list-input#removeTag"
>✖</a>
</div>
</li>
</ul>
<input
type="text"
name="variant_add_tag"
id="variant_add_tag"
placeholder="Add a tag"
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;"
>
</div>
<ul class="suggestion-list" data-tag-list-input-target="results">
<li
class="suggestion-item"
data-autocomplete-label="rule-1"
data-autocomplete-value="rule-1"
role="option"
id="stimulus-autocomplete-option-4"
>
rule-1 has 1 rule
</li>
<li
class="suggestion-item"
data-autocomplete-label="rule-2"
data-autocomplete-value="rule-2"
role="option"
id="stimulus-autocomplete-option-5"
>
rule-2 has 2 rules
</li>
</ul>
</div>
</div>`;
});
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");
});
});
});

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

341
yarn.lock
View File

@@ -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==