diff --git a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee index 38e12182a0..45f98a2b57 100644 --- a/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee +++ b/app/assets/javascripts/admin/tag_rules/controllers/tag_rules_controller.js.coffee @@ -1,16 +1,16 @@ -angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, enterprise) -> +angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, $filter, enterprise) -> $scope.tagGroups = enterprise.tag_groups $scope.defaultTagGroup = enterprise.default_tag_group $scope.visibilityOptions = [ { id: "visible", name: "VISIBLE" }, { id: "hidden", name: "NOT VISIBLE" } ] - updateRuleCounts = -> + $scope.updateRuleCounts = -> index = $scope.defaultTagGroup.rules.length - for tagGroup in $scope.tagGroups + for tagGroup in $filter('orderBy')($scope.tagGroups, 'position') tagGroup.startIndex = index index = index + tagGroup.rules.length - updateRuleCounts() + $scope.updateRuleCounts() $scope.updateTagsRulesFor = (tagGroup) -> for tagRule in tagGroup.rules @@ -38,17 +38,17 @@ angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, ente newRule.peferred_exchange_tags = [] newRule.preferred_matched_order_cycles_visibility = "visible" tagGroup.rules.push(newRule) - updateRuleCounts() + $scope.updateRuleCounts() $scope.addNewTag = -> - $scope.tagGroups.push { tags: [], rules: [] } + $scope.tagGroups.push { tags: [], rules: [], position: $scope.tagGroups.length + 1 } $scope.deleteTagRule = (tagGroup, tagRule) -> index = tagGroup.rules.indexOf(tagRule) return unless index >= 0 if tagRule.id is null tagGroup.rules.splice(index, 1) - updateRuleCounts() + $scope.updateRuleCounts() else if confirm("Are you sure?") $http @@ -56,4 +56,4 @@ angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, ente url: "/admin/enterprises/#{enterprise.id}/tag_rules/#{tagRule.id}.json" .success -> tagGroup.rules.splice(index, 1) - updateRuleCounts() + $scope.updateRuleCounts() diff --git a/app/assets/javascripts/admin/utils/directives/sortable.js.coffee b/app/assets/javascripts/admin/utils/directives/sortable.js.coffee new file mode 100644 index 0000000000..2f2f100e31 --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/sortable.js.coffee @@ -0,0 +1,36 @@ +angular.module("admin.utils").directive "ofnSortable", ($timeout, $parse) -> + restrict: "E" + scope: + items: '@' + position: '@' + afterSort: '&' + handle: "@" + axis: "@" + link: (scope, element, attrs) -> + $timeout -> + scope.axis ||= "y" + scope.handle ||= ".handle" + getScopePos = $parse(scope.position) + setScopePos = getScopePos.assign + + element.sortable + handle: scope.handle + helper: 'clone' + axis: scope.axis + items: scope.items + appendTo: element + update: (event, ui) -> + sortableSiblings = ($(ss) for ss in ui.item.siblings(scope.items)) + offset = Math.min(ui.item.index(), sortableSiblings[0].index()) + newPos = ui.item.index() - offset + 1 + oldPos = getScopePos(ui.item.scope()) + if newPos < oldPos + for sibScope in sortableSiblings.map((ss) -> ss.scope()) + pos = getScopePos(sibScope) + setScopePos(sibScope, pos + 1) if pos >= newPos && pos < oldPos + else if newPos > oldPos + for sibScope in sortableSiblings.map((ss) -> ss.scope()) + pos = getScopePos(sibScope) + setScopePos(sibScope, pos - 1) if pos > oldPos && pos <= newPos + setScopePos(ui.item.scope(), newPos) + scope.afterSort() diff --git a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml index 0a752ae17b..f3018927c4 100644 --- a/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml +++ b/app/assets/javascripts/templates/admin/tag_rules/tag_rule.html.haml @@ -16,6 +16,11 @@ name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]", ng: { value: "rule.type" } } + %input{ type: "hidden", + id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_priority", + name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][priority]", + ng: { value: "tagGroup.startIndex + $index" } } + %input{ type: "hidden", id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_is_default", name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][is_default]", diff --git a/app/assets/stylesheets/admin/tag_rules.css.scss b/app/assets/stylesheets/admin/tag_rules.css.scss index 0540dfa89f..7feedb7c50 100644 --- a/app/assets/stylesheets/admin/tag_rules.css.scss +++ b/app/assets/stylesheets/admin/tag_rules.css.scss @@ -5,7 +5,14 @@ font-weight: bold; } +.customer_tag { + .header { + cursor: move; + } +} + .customer_tag, .default_rules { + background-color: #ffffff; border: 1px solid #cee1f4; margin-bottom: 40px; diff --git a/app/models/tag_rule.rb b/app/models/tag_rule.rb index ee1ef105a3..a5f2d93dee 100644 --- a/app/models/tag_rule.rb +++ b/app/models/tag_rule.rb @@ -5,9 +5,11 @@ class TagRule < ActiveRecord::Base validates :enterprise, presence: true - attr_accessible :enterprise, :enterprise_id, :is_default, :preferred_customer_tags + attr_accessible :enterprise, :enterprise_id, :is_default, :priority + attr_accessible :preferred_customer_tags scope :for, ->(enterprise) { where(enterprise_id: enterprise) } + scope :prioritised, -> { order('priority ASC') } def self.mapping_for(enterprises) self.for(enterprises).inject({}) do |mapping, rule| diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 86b40c90ad..49a49b3250 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -9,9 +9,12 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer has_many :users, serializer: Api::Admin::UserSerializer def tag_groups - object.tag_rules.reject(&:is_default).each_with_object([]) do |tag_rule, tag_groups| + object.tag_rules.prioritised.reject(&:is_default).each_with_object([]) do |tag_rule, tag_groups| tag_group = find_match(tag_groups, tag_rule.preferred_customer_tags.split(",").map{ |t| { text: t } }) - tag_groups << tag_group if tag_group[:rules].empty? + if tag_group[:rules].empty? + tag_groups << tag_group + tag_group[:position] = tag_groups.count + end tag_group[:rules] << Api::Admin::TagRuleSerializer.new(tag_rule).serializable_hash end end diff --git a/app/views/admin/enterprises/form/_tag_rules.html.haml b/app/views/admin/enterprises/form/_tag_rules.html.haml index 5752b25b06..fd34477c6c 100644 --- a/app/views/admin/enterprises/form/_tag_rules.html.haml +++ b/app/views/admin/enterprises/form/_tag_rules.html.haml @@ -1,11 +1,11 @@ .row{ ng: { controller: "TagRulesCtrl" } } .eleven.columns.alpha.omega - .eleven.columns.alpha.omega + %ofn-sortable{ axis: "y", handle: ".header", items: '.customer_tag', position: "tagGroup.position", after: { sort: "updateRuleCounts()" } } .no_tags{ ng: { show: "tagGroups.length == 0" } } No tags apply to this enterprise yet = render 'admin/enterprises/form/tag_rules/default_rules' -# = render 'customer_tags' - .customer_tag{ ng: { repeat: "tagGroup in tagGroups" } } + .customer_tag{ id: "tg_{{tagGroup.position}}", ng: { repeat: "tagGroup in tagGroups" } } .header %table %colgroup diff --git a/db/migrate/20160527012603_add_priority_to_tag_rule.rb b/db/migrate/20160527012603_add_priority_to_tag_rule.rb new file mode 100644 index 0000000000..7080d7a9ac --- /dev/null +++ b/db/migrate/20160527012603_add_priority_to_tag_rule.rb @@ -0,0 +1,5 @@ +class AddPriorityToTagRule < ActiveRecord::Migration + def change + add_column :tag_rules, :priority, :integer, default: 99, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index f68f6c6a0a..002e367dd6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20160520065217) do +ActiveRecord::Schema.define(:version => 20160527012603) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -1164,6 +1164,7 @@ ActiveRecord::Schema.define(:version => 20160520065217) do t.datetime "created_at", :null => false t.datetime "updated_at", :null => false t.boolean "is_default", :default => false, :null => false + t.integer "priority", :default => 99, :null => false end create_table "taggings", :force => true do |t| diff --git a/lib/open_food_network/tag_rule_applicator.rb b/lib/open_food_network/tag_rule_applicator.rb index 181624a210..5225dfdd86 100644 --- a/lib/open_food_network/tag_rule_applicator.rb +++ b/lib/open_food_network/tag_rule_applicator.rb @@ -40,7 +40,7 @@ module OpenFoodNetwork def rules return @rules unless @rules.nil? - @rules = rule_class.for(enterprise) + @rules = rule_class.prioritised.for(enterprise) end def customer_rules diff --git a/spec/features/admin/tag_rules_spec.rb b/spec/features/admin/tag_rules_spec.rb index 45b28f603c..7c1aa55f9e 100644 --- a/spec/features/admin/tag_rules_spec.rb +++ b/spec/features/admin/tag_rules_spec.rb @@ -170,6 +170,9 @@ feature 'Tag Rules', js: true do select2_select 'VISIBLE', from: "enterprise_tag_rules_attributes_4_preferred_matched_shipping_methods_visibility" end + # Moving the Shipping Methods to top priority + find(".customer_tag#tg_4 .header", ).drag_to find(".customer_tag#tg_1 .header") + # # DiscountOrder rule # within "#tr_2" do # expect(page).to have_field "enterprise_tag_rules_attributes_2_calculator_attributes_preferred_flat_percent", with: '0' @@ -178,27 +181,31 @@ feature 'Tag Rules', js: true do click_button 'Update' - # FilterShippingMethods rule - expect(default_fsm_tag_rule.preferred_customer_tags).to eq "" + # DEFAULT FilterShippingMethods rule + expect(default_fsm_tag_rule.reload.preferred_customer_tags).to eq "" expect(default_fsm_tag_rule.preferred_shipping_method_tags).to eq "local,volunteers-only" expect(default_fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "hidden" # FilterShippingMethods rule + expect(fsm_tag_rule.reload.priority).to eq 1 expect(fsm_tag_rule.preferred_customer_tags).to eq "local,volunteer" expect(fsm_tag_rule.preferred_shipping_method_tags).to eq "local,volunteers-only4" expect(fsm_tag_rule.preferred_matched_shipping_methods_visibility).to eq "visible" # FilterProducts rule + expect(fp_tag_rule.reload.priority).to eq 2 expect(fp_tag_rule.preferred_customer_tags).to eq "member,volunteer" expect(fp_tag_rule.preferred_variant_tags).to eq "member,volunteers-only1" expect(fp_tag_rule.preferred_matched_variants_visibility).to eq "hidden" # FilterPaymentMethods rule + expect(fpm_tag_rule.reload.priority).to eq 3 expect(fpm_tag_rule.preferred_customer_tags).to eq "trusted,volunteer" expect(fpm_tag_rule.preferred_payment_method_tags).to eq "trusted,volunteers-only2" expect(fpm_tag_rule.preferred_matched_payment_methods_visibility).to eq "visible" - # FilterPaymentMethods rule + # FilterOrderCycles rule + expect(foc_tag_rule.reload.priority).to eq 4 expect(foc_tag_rule.preferred_customer_tags).to eq "wholesale,volunteer" expect(foc_tag_rule.preferred_exchange_tags).to eq "wholesale,volunteers-only3" expect(foc_tag_rule.preferred_matched_order_cycles_visibility).to eq "hidden" diff --git a/spec/lib/open_food_network/tag_rule_applicator_spec.rb b/spec/lib/open_food_network/tag_rule_applicator_spec.rb index 0b560a3e0a..97551df96e 100644 --- a/spec/lib/open_food_network/tag_rule_applicator_spec.rb +++ b/spec/lib/open_food_network/tag_rule_applicator_spec.rb @@ -3,12 +3,12 @@ require 'open_food_network/tag_rule_applicator' module OpenFoodNetwork describe TagRuleApplicator do let!(:enterprise) { create(:distributor_enterprise) } - let!(:oc_tag_rule) { create(:filter_order_cycles_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_exchange_tags: "tag1", preferred_matched_order_cycles_visibility: "visible" )} - let!(:product_tag_rule1) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "visible" ) } - let!(:product_tag_rule2) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_variant_tags: "tag3", preferred_matched_variants_visibility: "hidden" ) } - let!(:product_tag_rule3) { create(:filter_products_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag2", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "visible" ) } - let!(:default_product_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, is_default: true, preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "hidden" ) } - let!(:sm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, preferred_customer_tags: "tag1", preferred_shipping_method_tags: "tag1", preferred_matched_shipping_methods_visibility: "visible" )} + let!(:oc_tag_rule) { create(:filter_order_cycles_tag_rule, enterprise: enterprise, priority: 6, preferred_customer_tags: "tag1", preferred_exchange_tags: "tag1", preferred_matched_order_cycles_visibility: "visible" )} + let!(:product_tag_rule1) { create(:filter_products_tag_rule, enterprise: enterprise, priority: 5, preferred_customer_tags: "tag1", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "visible" ) } + let!(:product_tag_rule2) { create(:filter_products_tag_rule, enterprise: enterprise, priority: 4, preferred_customer_tags: "tag1", preferred_variant_tags: "tag3", preferred_matched_variants_visibility: "hidden" ) } + let!(:product_tag_rule3) { create(:filter_products_tag_rule, enterprise: enterprise, priority: 3, preferred_customer_tags: "tag2", preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "visible" ) } + let!(:default_product_tag_rule) { create(:filter_products_tag_rule, enterprise: enterprise, priority: 2, is_default: true, preferred_variant_tags: "tag1", preferred_matched_variants_visibility: "hidden" ) } + let!(:sm_tag_rule) { create(:filter_shipping_methods_tag_rule, enterprise: enterprise, priority: 1, preferred_customer_tags: "tag1", preferred_shipping_method_tags: "tag1", preferred_matched_shipping_methods_visibility: "visible" )} describe "initialisation" do context "when enterprise is nil" do @@ -68,19 +68,16 @@ module OpenFoodNetwork expect(applicator.customer_tags).to eq ["tag1"] end - it "selects only rules of the specified type" do - expect(rules).to include product_tag_rule1, product_tag_rule2, product_tag_rule3, default_product_tag_rule - expect(rules).not_to include oc_tag_rule, sm_tag_rule + it "selects only rules of the specified type, in order of priority" do + expect(rules).to eq [default_product_tag_rule, product_tag_rule3, product_tag_rule2, product_tag_rule1] end - it "splits rules into those which match customer tags and those which don't" do - expect(customer_rules).to include product_tag_rule1, product_tag_rule2 - expect(customer_rules).not_to include default_product_tag_rule, product_tag_rule3, oc_tag_rule, sm_tag_rule + it "splits rules into those which match customer tags and those which don't, in order of priority" do + expect(customer_rules).to eq [product_tag_rule2, product_tag_rule1] end it "splits out default rules" do - expect(default_rules).to include default_product_tag_rule - expect(default_rules).not_to include product_tag_rule1, product_tag_rule2, product_tag_rule3, oc_tag_rule, sm_tag_rule + expect(default_rules).to eq [default_product_tag_rule] end end end