From af111a96257276d66d74093eaa34fb46926131f3 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 8 Sep 2025 16:58:12 +1000 Subject: [PATCH 01/30] Clean up tag rules specs Remove unnecessary use mocking and use of `__send__` --- .../tag_rule/filter_order_cycles_spec.rb | 54 +++++++++++-------- .../tag_rule/filter_payment_methods_spec.rb | 51 +++++++++++------- spec/models/tag_rule/filter_products_spec.rb | 46 +++++++++------- .../tag_rule/filter_shipping_methods_spec.rb | 51 +++++++++++------- 4 files changed, 123 insertions(+), 79 deletions(-) diff --git a/spec/models/tag_rule/filter_order_cycles_spec.rb b/spec/models/tag_rule/filter_order_cycles_spec.rb index 7616453cdd..0e84242b8f 100644 --- a/spec/models/tag_rule/filter_order_cycles_spec.rb +++ b/spec/models/tag_rule/filter_order_cycles_spec.rb @@ -3,56 +3,68 @@ 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 "#reject_matched?" 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" 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..f9776d4d36 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 "#tag_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..0595e6e0e3 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 + 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..ffae35cf15 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 "#tag_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 From 3f297a8afa19640f94a3cdb8d3e593f0ae7adb80 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 9 Sep 2025 09:38:17 +1000 Subject: [PATCH 02/30] Add tag rule to filter by variant --- app/models/tag_rule/filter_variants.rb | 22 +++++++ spec/factories/tag_rule_factory.rb | 4 ++ spec/models/tag_rule/filter_variants_spec.rb | 60 ++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 app/models/tag_rule/filter_variants.rb create mode 100644 spec/models/tag_rule/filter_variants_spec.rb 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/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/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 From b1d95cac7f1c76c9a2590c8b825a072d4bce3092 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 16 Sep 2025 11:46:34 +1000 Subject: [PATCH 03/30] Display filter by variant tag rule We only support one of filter by inventory variants or filter by variants at any given time, based on enabled feature. If both features inventory and variant tag are enabled, variant tag takes precedence. --- app/components/tag_rule_form_component.rb | 7 ++ .../admin/enterprises_controller.rb | 22 ++++- app/controllers/admin/tag_rules_controller.rb | 2 +- app/models/tag_rule.rb | 2 + .../enterprises/form/_tag_rules.html.haml | 5 +- .../form/tag_rules/_default_rules.html.haml | 5 +- config/locales/en.yml | 3 + spec/system/admin/tag_rules_spec.rb | 98 ++++++++++++++++++- 8 files changed, 132 insertions(+), 12 deletions(-) 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..811515fdbc 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] @@ -398,9 +399,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_new"), "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..0d5d06b35f 100644 --- a/app/controllers/admin/tag_rules_controller.rb +++ b/app/controllers/admin/tag_rules_controller.rb @@ -78,7 +78,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/models/tag_rule.rb b/app/models/tag_rule.rb index 790e844334..d1efddb9da 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| 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/config/locales/en.yml b/config/locales/en.yml index 67b83796d9..aba0f24f28 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1419,6 +1419,7 @@ en: no_tags_yet: No tags apply to this enterprise yet add_new_tag: '+ Add A New Tag' show_hide_variants: 'Show or Hide variants in my shopfront' + show_hide_variants_new: 'Show or Hide variants in my shopfront' show_hide_shipping: 'Show or Hide shipping methods at checkout' show_hide_payment: 'Show or Hide payment methods at checkout' show_hide_order_cycles: 'Show or Hide order cycles in my shopfront' @@ -5076,6 +5077,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/spec/system/admin/tag_rules_spec.rb b/spec/system/admin/tag_rules_spec.rb index 124d8de7d6..7b0490e690 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 From 7b3db4bae4f0784f181d8cbfe035ef725cd00991 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 17 Sep 2025 11:27:42 +1000 Subject: [PATCH 04/30] Add VariantTagRuleFilterer to filter variants by tag rule --- app/services/variant_tag_rules_filterer.rb | 109 ++++++++++++++++ .../variant_tag_rules_filterer_spec.rb | 119 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 app/services/variant_tag_rules_filterer.rb create mode 100644 spec/services/variant_tag_rules_filterer_spec.rb 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/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 From 94c0ebd897abffd37fa18ed46eebcf1782a0bbd3 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 17 Sep 2025 11:41:34 +1000 Subject: [PATCH 05/30] Fix error in the muliple tag rules spec --- app/services/product_tag_rules_filterer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 7633af8ff26e72ff7642e9a717777aa1c62d7805 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 17 Sep 2025 15:48:48 +1000 Subject: [PATCH 06/30] Call VariantTagRulesFilterer when variant_tag feature is enabled We only support either inventory or variant_tag feature, with the later taking precedence if both are turned on. --- .../distributed_products_service.rb | 11 +++++++++- .../distributed_products_service_spec.rb | 20 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) 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/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 From c38c8bcff29caf0562922cb91f3f03c838462256 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 17 Sep 2025 15:52:26 +1000 Subject: [PATCH 07/30] Pass the variant_tag_enbabled options to relevant services Plus add integration testing for variant tag rule filtering. --- .../api/v0/order_cycles_controller.rb | 9 ++- .../api/v0/order_cycles_controller_spec.rb | 59 ++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) 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/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 } From 81e16a9cdf692aadc6ad7c7ab343c99475d4f9bc Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Fri, 26 Sep 2025 10:23:32 +1000 Subject: [PATCH 08/30] Add stimulus-autocomplete package https://github.com/afcapel/stimulus-autocomplete/tree/main --- app/webpacker/controllers/index.js | 4 +++- package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/webpacker/controllers/index.js b/app/webpacker/controllers/index.js index b02807e3d8..707fc2a179 100644 --- a/app/webpacker/controllers/index.js +++ b/app/webpacker/controllers/index.js @@ -6,7 +6,8 @@ import StimulusReflex from "stimulus_reflex"; 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 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/package.json b/package.json index 61bd95e6f8..11dee2332e 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", diff --git a/yarn.lock b/yarn.lock index 525be4a719..b0474b286d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8635,6 +8635,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" From bd39595917662982488b282fb67ec047243e9b91 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 29 Sep 2025 14:06:12 +1000 Subject: [PATCH 09/30] Add ability to pass option to the tag input field And also render any content given to the component via block --- app/components/tag_list_input_component.rb | 7 +++++-- .../tag_list_input_component.html.haml | 11 ++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/components/tag_list_input_component.rb b/app/components/tag_list_input_component.rb index 2f415cf0b1..59c657304f 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: {}, + input_field_data_options: {}) @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 + @input_field_data_options = input_field_data_options 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, :input_field_data_options 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..20b60e5d3d 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 @@ -16,4 +16,13 @@ %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#addTag keyup->tag-list-input#filterInput blur->tag-list-input#addTag", + "data-tag-list-input-target": "newTag", + **aria_label_option, + style: "display: #{display};"}.merge(input_field_data_options)) + - if content? + = content From 3bb9eb9765da6d3ac1973be12c82d8859db03ead Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 29 Sep 2025 14:23:10 +1000 Subject: [PATCH 10/30] Add endpoint to provide autocomplete tag for variant It return a list of available tags and number of related rules, based on the given enterprise and a partial match on the given tag --- app/controllers/admin/tag_rules_controller.rb | 21 ++++++++++++ app/models/tag_rule.rb | 9 +++++ .../tag_rules/variant_tag_rules.html.haml | 3 ++ config/locales/en.yml | 4 +++ config/routes/admin.rb | 5 +-- spec/models/tag_rule_spec.rb | 33 +++++++++++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 app/views/admin/tag_rules/variant_tag_rules.html.haml diff --git a/app/controllers/admin/tag_rules_controller.rb b/app/controllers/admin/tag_rules_controller.rb index 0d5d06b35f..d5112abf74 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_customer_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 diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index d1efddb9da..3fbc131c48 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -10,6 +10,7 @@ class TagRule < ApplicationRecord scope :exclude_inventory, -> { where.not(type: "TagRule::FilterProducts") } scope :exclude_variant, -> { where.not(type: "TagRule::FilterVariants") } + # TODO doesn not exluce inventory and or variant tag rule def self.mapping_for(enterprises) self.for(enterprises).each_with_object({}) do |rule, mapping| rule.preferred_customer_tags.split(",").each do |tag| @@ -22,6 +23,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_customer_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/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/config/locales/en.yml b/config/locales/en.yml index aba0f24f28..8246b85d2b 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 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/spec/models/tag_rule_spec.rb b/spec/models/tag_rule_spec.rb index b4b53310ae..1282f27254 100644 --- a/spec/models/tag_rule_spec.rb +++ b/spec/models/tag_rule_spec.rb @@ -9,6 +9,39 @@ 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_customer_tags: "filtered" ) + } + let!(:rule2) { + create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "filtered" ) + } + let!(:rule3) { + create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "filtered" ) + } + let!(:rule4) { + create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "other-tag" ) + } + let!(:rule5) { + create(:filter_order_cycles_tag_rule, enterprise:, preferred_customer_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, rule3 + expect(rules).not_to include rule4 + 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 } From 965b34318f0a31606abb5fa657c301bf6b130be6 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 29 Sep 2025 14:27:45 +1000 Subject: [PATCH 11/30] Add new component to provide tag autocomplete for variant tag It uses composition and inject the TagListInputComponent as a depency, which should be more flexible that creating a sub class. This new component could potentially be made more generic if needed --- .../variant_tag_list_input_component.rb | 24 +++++++++++++++++++ ...variant_tag_list_input_component.html.haml | 4 ++++ .../admin/products_v3/_variant_row.html.haml | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 app/components/variant_tag_list_input_component.rb create mode 100644 app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml diff --git a/app/components/variant_tag_list_input_component.rb b/app/components/variant_tag_list_input_component.rb new file mode 100644 index 0000000000..7297f0b2e4 --- /dev/null +++ b/app/components/variant_tag_list_input_component.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class VariantTagListInputComponent < ViewComponent::Base + def initialize(name:, variant:, tag_list_input_component: TagListInputComponent, + placeholder: I18n.t("components.tag_list_input.default_placeholder"), + aria_label: nil) + @tag_list_input_component = tag_list_input_component + @variant = variant + @name = name + @tags = variant.tag_list + @placeholder = placeholder + @only_one = false + @aria_label = aria_label + end + + attr_reader :tag_list_input_component, :variant, :name, :tags, :placeholder, :only_one, + :aria_label + + private + + def autocomplete_url + "/admin/tag_rules/variant_tag_rules?enterprise_id=#{variant.supplier_id}" + end +end diff --git a/app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml b/app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml new file mode 100644 index 0000000000..02797dcdad --- /dev/null +++ b/app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml @@ -0,0 +1,4 @@ +%div{ "data-controller": "autocomplete", "data-autocomplete-url-value": autocomplete_url } + = render tag_list_input_component.new(name: , tags: , placeholder: , aria_label: , only_one: , input_field_data_options: {"data-autocomplete-target": "input"}) do + .autocomplete + %ul.suggestion-list{ "data-autocomplete-target": "results" } diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 4235c69a11..089061b72e 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 VariantTagListInputComponent.new(name: f.field_name(:tag_list), variant:, placeholder: t('.add_a_tag'), aria_label: t('admin.products_page.columns.tags')) %td.col-inherits_properties.align-left -# empty %td.align-right From 6d7908e1f8435adcf8c05f69aefab305ff993b32 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 1 Oct 2025 11:26:29 +1000 Subject: [PATCH 12/30] Style formatting --- app/webpacker/controllers/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/webpacker/controllers/index.js b/app/webpacker/controllers/index.js index 707fc2a179..643ab3ed0b 100644 --- a/app/webpacker/controllers/index.js +++ b/app/webpacker/controllers/index.js @@ -6,8 +6,8 @@ import StimulusReflex from "stimulus_reflex"; 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" +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$/); From 3cffc5538abb3cd68514430f128bf3c3d1daf154 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 1 Oct 2025 11:27:05 +1000 Subject: [PATCH 13/30] Add tag filtering for tag autocomplete --- ...variant_tag_list_input_component.html.haml | 13 +++++--- .../variant_tag_list_input_controller.js | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 app/components/variant_tag_list_input_component/variant_tag_list_input_controller.js diff --git a/app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml b/app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml index 02797dcdad..a6ec8065f7 100644 --- a/app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml +++ b/app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml @@ -1,4 +1,9 @@ -%div{ "data-controller": "autocomplete", "data-autocomplete-url-value": autocomplete_url } - = render tag_list_input_component.new(name: , tags: , placeholder: , aria_label: , only_one: , input_field_data_options: {"data-autocomplete-target": "input"}) do - .autocomplete - %ul.suggestion-list{ "data-autocomplete-target": "results" } +%div{ "data-controller": "variant-tag-list-input", "data-variant-tag-list-input-url-value": autocomplete_url } + = render tag_list_input_component.new(name:, + tags:, + placeholder:, + aria_label:, + only_one:, + input_field_data_options: { "data-variant-tag-list-input-target": "input" }, + hidden_field_data_options: { "data-variant-tag-list-input-target": "tags" }) do + %ul.suggestion-list{ "data-variant-tag-list-input-target": "results" } diff --git a/app/components/variant_tag_list_input_component/variant_tag_list_input_controller.js b/app/components/variant_tag_list_input_component/variant_tag_list_input_controller.js new file mode 100644 index 0000000000..ff187855bd --- /dev/null +++ b/app/components/variant_tag_list_input_component/variant_tag_list_input_controller.js @@ -0,0 +1,31 @@ +import { Autocomplete } from "stimulus-autocomplete"; + +// Extend the stimulus-autocomplete controller, so we can add the ability to filter out existing +// tag +export default class extends Autocomplete { + static targets = ["tags"]; + + replaceResults(html) { + const filteredHtml = this.#filterResults(html); + super.replaceResults(filteredHtml); + } + + //private + + #filterResults(html) { + const existingTags = this.tagsTarget.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; + } +} From 749944fc25724c6bd2af43795783fffdf74f518c Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 6 Oct 2025 16:12:33 +1100 Subject: [PATCH 14/30] Rework TagListInputComponent to integrate autocomplete The component now will try to load a list of existing tag if you give an `autocomplete_url`. I tried to keep the tag input and the autocomplete functionality decoupled but is wasn't really possible. Instead I opted to sub class the Autocomplete stimulus controller, but it only gets initialised if we pass an `autocomplete_url`. --- app/components/tag_list_input_component.rb | 6 +- .../tag_list_input_component.html.haml | 11 +-- .../tag_list_input_controller.js | 91 ++++++++++++++++--- .../variant_tag_list_input_component.rb | 24 ----- ...variant_tag_list_input_component.html.haml | 9 -- .../variant_tag_list_input_controller.js | 31 ------- .../admin/products_v3/_variant_row.html.haml | 2 +- 7 files changed, 88 insertions(+), 86 deletions(-) delete mode 100644 app/components/variant_tag_list_input_component.rb delete mode 100644 app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml delete mode 100644 app/components/variant_tag_list_input_component/variant_tag_list_input_controller.js diff --git a/app/components/tag_list_input_component.rb b/app/components/tag_list_input_component.rb index 59c657304f..2e9265f5bd 100644 --- a/app/components/tag_list_input_component.rb +++ b/app/components/tag_list_input_component.rb @@ -6,18 +6,18 @@ class TagListInputComponent < ViewComponent::Base only_one: false, aria_label: nil, hidden_field_data_options: {}, - input_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 - @input_field_data_options = input_field_data_options + @autocomplete_url = autocomplete_url end attr_reader :name, :tags, :placeholder, :only_one, :aria_label_option, - :hidden_field_data_options, :input_field_data_options + :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 20b60e5d3d..a2d72813ba 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 @@ -20,9 +20,8 @@ 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", + "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};"}.merge(input_field_data_options)) - - if content? - = content + style: "display: #{display};"}) + %ul.suggestion-list{ "data-tag-list-input-target": "results" } 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/variant_tag_list_input_component.rb b/app/components/variant_tag_list_input_component.rb deleted file mode 100644 index 7297f0b2e4..0000000000 --- a/app/components/variant_tag_list_input_component.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -class VariantTagListInputComponent < ViewComponent::Base - def initialize(name:, variant:, tag_list_input_component: TagListInputComponent, - placeholder: I18n.t("components.tag_list_input.default_placeholder"), - aria_label: nil) - @tag_list_input_component = tag_list_input_component - @variant = variant - @name = name - @tags = variant.tag_list - @placeholder = placeholder - @only_one = false - @aria_label = aria_label - end - - attr_reader :tag_list_input_component, :variant, :name, :tags, :placeholder, :only_one, - :aria_label - - private - - def autocomplete_url - "/admin/tag_rules/variant_tag_rules?enterprise_id=#{variant.supplier_id}" - end -end diff --git a/app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml b/app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml deleted file mode 100644 index a6ec8065f7..0000000000 --- a/app/components/variant_tag_list_input_component/variant_tag_list_input_component.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -%div{ "data-controller": "variant-tag-list-input", "data-variant-tag-list-input-url-value": autocomplete_url } - = render tag_list_input_component.new(name:, - tags:, - placeholder:, - aria_label:, - only_one:, - input_field_data_options: { "data-variant-tag-list-input-target": "input" }, - hidden_field_data_options: { "data-variant-tag-list-input-target": "tags" }) do - %ul.suggestion-list{ "data-variant-tag-list-input-target": "results" } diff --git a/app/components/variant_tag_list_input_component/variant_tag_list_input_controller.js b/app/components/variant_tag_list_input_component/variant_tag_list_input_controller.js deleted file mode 100644 index ff187855bd..0000000000 --- a/app/components/variant_tag_list_input_component/variant_tag_list_input_controller.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Autocomplete } from "stimulus-autocomplete"; - -// Extend the stimulus-autocomplete controller, so we can add the ability to filter out existing -// tag -export default class extends Autocomplete { - static targets = ["tags"]; - - replaceResults(html) { - const filteredHtml = this.#filterResults(html); - super.replaceResults(filteredHtml); - } - - //private - - #filterResults(html) { - const existingTags = this.tagsTarget.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/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 089061b72e..783eaef37e 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 VariantTagListInputComponent.new(name: f.field_name(:tag_list), variant:, 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: "/admin/tag_rules/variant_tag_rules?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 From ab194a0e80514991875e532e05f9a80d11677200 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 8 Oct 2025 12:11:01 +1100 Subject: [PATCH 15/30] Add styling for the dropdown It's mostly the same styling as the AngularJs version but with updated colors --- .../tag_list_input_component.html.haml | 2 +- .../tag_list_input_component.scss | 31 +++++++++++++++++++ .../css/admin_v3/globals/variables.scss | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) 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 a2d72813ba..7b9d66c5d1 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 @@ -24,4 +24,4 @@ "data-tag-list-input-target": "input", **aria_label_option, style: "display: #{display};"}) - %ul.suggestion-list{ "data-tag-list-input-target": "results" } + %ul.suggestion-list{ "data-tag-list-input-target": "results" } 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/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; From c5d5694f242d6de3f796d0304fed606c892161bf Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 8 Oct 2025 13:53:01 +1100 Subject: [PATCH 16/30] Tweaked jest configuration - include app/components in the directories to search for modules, ie we can require view component js controller like this : `import tag_list_input_controller from "tag_list_input_component/tag_list_input_controller";` - fixed the regexp to skip transformation so it skips any modules starting by "stimulus" --- jest.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, From 9bbe573335d3487a20b5606184b7b4f0f87b6fc6 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 8 Oct 2025 14:33:37 +1100 Subject: [PATCH 17/30] Fix test to match the improved controller --- .../tag_list_input_controller_test.js | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/spec/javascripts/stimulus/tag_list_input_controller_test.js b/spec/javascripts/stimulus/tag_list_input_controller_test.js index 91ed014be9..c1a1c83447 100644 --- a/spec/javascripts/stimulus/tag_list_input_controller_test.js +++ b/spec/javascripts/stimulus/tag_list_input_controller_test.js @@ -3,7 +3,7 @@ */ import { Application } from "stimulus"; -import tag_list_input_controller from "../../../app/components/tag_list_input_component/tag_list_input_controller"; +import tag_list_input_controller from "tag_list_input_component/tag_list_input_controller"; describe("TagListInputController", () => { beforeAll(() => { @@ -14,12 +14,17 @@ describe("TagListInputController", () => { beforeEach(() => { // Tag input with three existing tags document.body.innerHTML = ` -
+
@@ -68,10 +73,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;" >
+
`; }); @@ -162,6 +169,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 +240,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;" >
+ `; }); From 145764a9218279e37955fee755af3307d97a2eae Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 13 Oct 2025 13:53:54 +1100 Subject: [PATCH 18/30] Add testing library See :https://testing-library.com/docs/ It allows us to query DOM node in way that's similar to how a user would interect with element on the page. It's particularly usefull for elements that trigger AJAX request. --- package.json | 1 + yarn.lock | 336 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 322 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 11dee2332e..a5dc4499d8 100644 --- a/package.json +++ b/package.json @@ -38,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/yarn.lock b/yarn.lock index b0474b286d..d987bc08d1 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" @@ -8662,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" @@ -9624,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== From 34abca5ff1365786328e92bd975eda2c73818bdf Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 13 Oct 2025 13:59:53 +1100 Subject: [PATCH 19/30] Add missing js unit test got TagListInput component --- .../tag_list_input_controller_test.js | 163 +++++++++++++++++- 1 file changed, 161 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/stimulus/tag_list_input_controller_test.js b/spec/javascripts/stimulus/tag_list_input_controller_test.js index c1a1c83447..8ebd9b515d 100644 --- a/spec/javascripts/stimulus/tag_list_input_controller_test.js +++ b/spec/javascripts/stimulus/tag_list_input_controller_test.js @@ -3,12 +3,45 @@ */ import { Application } from "stimulus"; +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(() => { @@ -73,12 +106,12 @@ describe("TagListInputController", () => { 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-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;" > - + `; }); @@ -315,4 +348,130 @@ describe("TagListInputController", () => { expect(variant_add_tag.classList).not.toContain("tag-error"); }); }); + + describe("onBlur", () => { + it("adds the tag", () => { + variant_add_tag.value = "newer_tag"; + variant_add_tag.dispatchEvent(new FocusEvent("blur")); + + expect(variant_tag_list.value).toBe("tag-1,tag-2,tag-3,newer_tag"); + }); + + describe("with autocomplete results", () => { + beforeEach(() => { + document.body.innerHTML = ` +
    + +
    +
    +
      + +
    • +
      + tag-1 + +
      +
    • +
    • +
      + tag-2 + +
      +
    • +
    • +
      + tag-3 + +
      +
    • +
    + +
    +
      +
    • + rule-1 has 1 rule +
    • +
    • + rule-2 has 2 rules +
    • +
    +
    +
    `; + }); + + 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"); + }); + }); }); From 38f58b168afea0fd18837e56582a971c1a607f49 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 13 Oct 2025 15:09:17 +1100 Subject: [PATCH 20/30] Fix tag rules spec Make sure the autocomplete dropdown list is hidden by default --- .../tag_list_input_component.html.haml | 2 +- spec/system/admin/tag_rules_spec.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) 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 7b9d66c5d1..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 @@ -24,4 +24,4 @@ "data-tag-list-input-target": "input", **aria_label_option, style: "display: #{display};"}) - %ul.suggestion-list{ "data-tag-list-input-target": "results" } + %ul.suggestion-list{ "data-tag-list-input-target": "results" , hidden: true } diff --git a/spec/system/admin/tag_rules_spec.rb b/spec/system/admin/tag_rules_spec.rb index 7b0490e690..8f7e0c5e37 100644 --- a/spec/system/admin/tag_rules_spec.rb +++ b/spec/system/admin/tag_rules_spec.rb @@ -160,14 +160,14 @@ RSpec.describe 'Tag Rules' 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" + 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" + fill_in_tag "new-product" tomselect_select "VISIBLE", from: "enterprise_tag_rules_attributes_1001_preferred_matched_" \ "variants_visibility" @@ -176,8 +176,8 @@ RSpec.describe 'Tag Rules' do 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_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 From 07a3e83dc6754c2f627e80ab328c1ff5042eb4da Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 13 Oct 2025 15:11:47 +1100 Subject: [PATCH 21/30] Fix enterprise specs Plus small refactor --- app/controllers/admin/enterprises_controller.rb | 1 + spec/system/admin/enterprises_spec.rb | 15 ++++++--------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 811515fdbc..a32eaf4349 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -85,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 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 From d51e2579046bce8b2f872efb9476d6afd6eb0865 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 13 Oct 2025 15:29:38 +1100 Subject: [PATCH 22/30] Fix order cycle tag rule specs It works better when you actually save the changes to the tag_list... --- spec/models/tag_rule/filter_order_cycles_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/models/tag_rule/filter_order_cycles_spec.rb b/spec/models/tag_rule/filter_order_cycles_spec.rb index 0e84242b8f..76bbb17ef8 100644 --- a/spec/models/tag_rule/filter_order_cycles_spec.rb +++ b/spec/models/tag_rule/filter_order_cycles_spec.rb @@ -34,6 +34,7 @@ RSpec.describe TagRule::FilterOrderCycles do before do 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 From 307acdd9d1d93d424f1a0498a0b6bcefe0011a0b Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 15 Oct 2025 14:29:52 +1100 Subject: [PATCH 23/30] Per review, fixing specs descriptions --- spec/models/tag_rule/filter_order_cycles_spec.rb | 2 +- spec/models/tag_rule/filter_payment_methods_spec.rb | 2 +- spec/models/tag_rule/filter_products_spec.rb | 2 +- spec/models/tag_rule/filter_shipping_methods_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/models/tag_rule/filter_order_cycles_spec.rb b/spec/models/tag_rule/filter_order_cycles_spec.rb index 76bbb17ef8..c4eded6125 100644 --- a/spec/models/tag_rule/filter_order_cycles_spec.rb +++ b/spec/models/tag_rule/filter_order_cycles_spec.rb @@ -17,7 +17,7 @@ RSpec.describe TagRule::FilterOrderCycles do end end - describe "#reject_matched?" do + describe "#tags_match?" do context "when the exchange is nil" do before do allow(tag_rule).to receive(:exchange_for) { nil } diff --git a/spec/models/tag_rule/filter_payment_methods_spec.rb b/spec/models/tag_rule/filter_payment_methods_spec.rb index f9776d4d36..fe7b4056b2 100644 --- a/spec/models/tag_rule/filter_payment_methods_spec.rb +++ b/spec/models/tag_rule/filter_payment_methods_spec.rb @@ -16,7 +16,7 @@ RSpec.describe TagRule::FilterPaymentMethods do end end - describe "#tag_match?" do + describe "#tags_match?" do context "when the payment method is nil" do it "returns false" do expect(tag_rule.tags_match?(nil)).to be false diff --git a/spec/models/tag_rule/filter_products_spec.rb b/spec/models/tag_rule/filter_products_spec.rb index 0595e6e0e3..58e0ec7409 100644 --- a/spec/models/tag_rule/filter_products_spec.rb +++ b/spec/models/tag_rule/filter_products_spec.rb @@ -14,7 +14,7 @@ RSpec.describe TagRule::FilterProducts do 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 diff --git a/spec/models/tag_rule/filter_shipping_methods_spec.rb b/spec/models/tag_rule/filter_shipping_methods_spec.rb index ffae35cf15..022df21ee0 100644 --- a/spec/models/tag_rule/filter_shipping_methods_spec.rb +++ b/spec/models/tag_rule/filter_shipping_methods_spec.rb @@ -16,7 +16,7 @@ RSpec.describe TagRule::FilterShippingMethods do end end - describe "#tag_match?" do + describe "#tags_match?" do context "when the shipping method is nil" do it "returns false" do expect(tag_rule.tags_match?(nil)).to be false From ce60335a601e1789e74cc50a87d1c3baf32b3b0d Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 15 Oct 2025 14:31:11 +1100 Subject: [PATCH 24/30] Per review, fix leftover comment --- app/models/tag_rule.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index 3fbc131c48..93e72fa020 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -10,7 +10,6 @@ class TagRule < ApplicationRecord scope :exclude_inventory, -> { where.not(type: "TagRule::FilterProducts") } scope :exclude_variant, -> { where.not(type: "TagRule::FilterVariants") } - # TODO doesn not exluce inventory and or variant tag rule def self.mapping_for(enterprises) self.for(enterprises).each_with_object({}) do |rule, mapping| rule.preferred_customer_tags.split(",").each do |tag| From aebb18da99c561eb257f29bcfd0f056e61df1869 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 15 Oct 2025 15:14:08 +1100 Subject: [PATCH 25/30] Per review, improve specs --- spec/models/tag_rule_spec.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/models/tag_rule_spec.rb b/spec/models/tag_rule_spec.rb index 1282f27254..a7c35b832c 100644 --- a/spec/models/tag_rule_spec.rb +++ b/spec/models/tag_rule_spec.rb @@ -18,7 +18,8 @@ RSpec.describe TagRule do create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "filtered" ) } let!(:rule3) { - create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "filtered" ) + create(:filter_variants_tag_rule, enterprise: create(:enterprise), + preferred_customer_tags: "filtered" ) } let!(:rule4) { create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "other-tag" ) @@ -30,8 +31,8 @@ RSpec.describe TagRule do 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, rule3 - expect(rules).not_to include rule4 + expect(rules).to include rule1, rule2 + expect(rules).not_to include rule3, rule4, rule5 end context "when no matching rules" do From 59340c7cffa919b3e16b979d3791a8947db7c4c1 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Wed, 15 Oct 2025 15:19:37 +1100 Subject: [PATCH 26/30] Per review, remove unnecessary new translation --- app/controllers/admin/enterprises_controller.rb | 2 +- config/locales/en.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index a32eaf4349..71ae5c2f08 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -401,7 +401,7 @@ module Admin ] if helpers.feature?(:variant_tag, @object) - @tag_rule_types.prepend([t(".form.tag_rules.show_hide_variants_new"), "FilterVariants"]) + @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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 8246b85d2b..c537a28867 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1423,7 +1423,6 @@ en: no_tags_yet: No tags apply to this enterprise yet add_new_tag: '+ Add A New Tag' show_hide_variants: 'Show or Hide variants in my shopfront' - show_hide_variants_new: 'Show or Hide variants in my shopfront' show_hide_shipping: 'Show or Hide shipping methods at checkout' show_hide_payment: 'Show or Hide payment methods at checkout' show_hide_order_cycles: 'Show or Hide order cycles in my shopfront' From c057bab4934d6e7cc2747b3a4e39811b268721da Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou <40413322+rioug@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:20:31 +1100 Subject: [PATCH 27/30] Use route helpers for autocomplete url Co-authored-by: Maikel --- app/views/admin/products_v3/_variant_row.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/products_v3/_variant_row.html.haml b/app/views/admin/products_v3/_variant_row.html.haml index 783eaef37e..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, autocomplete_url: "/admin/tag_rules/variant_tag_rules?enterprise_id=#{variant.supplier_id}", 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 From 1a68236c3c4ab9d52dd469a1fb85a7eb352a314e Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 21 Oct 2025 12:25:14 +1100 Subject: [PATCH 28/30] Add `variant_tag_rule` ability It's needed to allow enterprise user to get a tag autocomplete. Classic mistake of not testing with a non superadmin user. --- app/models/spree/ability.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From ffd5817749178af3655867546c9d30be02e91967 Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Tue, 21 Oct 2025 12:27:40 +1100 Subject: [PATCH 29/30] Add spec for variant_tag_rules --- .../admin/tag_rules_controller_spec.rb | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/spec/controllers/admin/tag_rules_controller_spec.rb b/spec/controllers/admin/tag_rules_controller_spec.rb index bd955bee91..c4e5f8c4c3 100644 --- a/spec/controllers/admin/tag_rules_controller_spec.rb +++ b/spec/controllers/admin/tag_rules_controller_spec.rb @@ -69,4 +69,44 @@ 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" ) + } + let!(:rule2) { + create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "Tag-1" ) + } + let!(:rule3) { + create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "organic" ) + } + + 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 "Tag-1 has 2 rules" + expect(response.body).to include "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 "Tag-1 has 2 rules" + expect(response.body).to include "organic has 1 rule" + end + end + end end From bb8ecccc31ca615065057f7b099980a1bc97492d Mon Sep 17 00:00:00 2001 From: Gaetan Craig-Riou Date: Mon, 3 Nov 2025 15:32:41 +1100 Subject: [PATCH 30/30] Fix variant tag rules endpoint It now returns tag rules filtered on the preferred variant tags and not the prefered customer tags --- app/controllers/admin/tag_rules_controller.rb | 2 +- app/models/tag_rule.rb | 2 +- .../admin/tag_rules_controller_spec.rb | 23 +++++++++++++------ spec/models/tag_rule_spec.rb | 10 ++++---- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/controllers/admin/tag_rules_controller.rb b/app/controllers/admin/tag_rules_controller.rb index d5112abf74..620de16ee3 100644 --- a/app/controllers/admin/tag_rules_controller.rb +++ b/app/controllers/admin/tag_rules_controller.rb @@ -57,7 +57,7 @@ module Admin 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_customer_tags.split(",").each do |tag| + rule.preferred_variant_tags.split(",").each do |tag| if mapping[tag] mapping[tag][:rules] += 1 else diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index 93e72fa020..989952e363 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -27,7 +27,7 @@ class TagRule < ApplicationRecord return [] if rules.empty? - rules.select { |r| r.preferred_customer_tags =~ /#{tag}/ } + rules.select { |r| r.preferred_variant_tags =~ /#{tag}/ } end # The following method must be overriden in a concrete tagRule diff --git a/spec/controllers/admin/tag_rules_controller_spec.rb b/spec/controllers/admin/tag_rules_controller_spec.rb index c4e5f8c4c3..f1dccc2c4e 100644 --- a/spec/controllers/admin/tag_rules_controller_spec.rb +++ b/spec/controllers/admin/tag_rules_controller_spec.rb @@ -76,13 +76,20 @@ RSpec.describe Admin::TagRulesController do let(:enterprise) { create(:distributor_enterprise) } let(:q) { "" } let!(:rule1) { - create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "Tag-1" ) + 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" ) + 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" ) + 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 @@ -93,8 +100,9 @@ RSpec.describe Admin::TagRulesController 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 "Tag-1 has 2 rules" - expect(response.body).to include "organic has 1 rule" + 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 @@ -104,8 +112,9 @@ RSpec.describe Admin::TagRulesController 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 "Tag-1 has 2 rules" - expect(response.body).to include "organic has 1 rule" + 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 diff --git a/spec/models/tag_rule_spec.rb b/spec/models/tag_rule_spec.rb index a7c35b832c..eb27a088de 100644 --- a/spec/models/tag_rule_spec.rb +++ b/spec/models/tag_rule_spec.rb @@ -12,20 +12,20 @@ RSpec.describe TagRule do describe ".matching_variant_tag_rules_by_enterprises" do let(:enterprise) { create(:enterprise) } let!(:rule1) { - create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "filtered" ) + create(:filter_variants_tag_rule, enterprise:, preferred_variant_tags: "filtered" ) } let!(:rule2) { - create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "filtered" ) + create(:filter_variants_tag_rule, enterprise:, preferred_variant_tags: "filtered" ) } let!(:rule3) { create(:filter_variants_tag_rule, enterprise: create(:enterprise), - preferred_customer_tags: "filtered" ) + preferred_variant_tags: "filtered" ) } let!(:rule4) { - create(:filter_variants_tag_rule, enterprise:, preferred_customer_tags: "other-tag" ) + create(:filter_variants_tag_rule, enterprise:, preferred_variant_tags: "other-tag" ) } let!(:rule5) { - create(:filter_order_cycles_tag_rule, enterprise:, preferred_customer_tags: "filtered" ) + create(:filter_order_cycles_tag_rule, enterprise:, preferred_exchange_tags: "filtered" ) } it "returns a list of rule partially matching the tag" do