From fa5fa9e2286554617996182424056f9f29569edd Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 29 Apr 2016 12:33:33 +1000 Subject: [PATCH] Auto-complete tags on customers page - new controller serving tags for an enterprise as JSON - customers page suggesting these tags - emphasising tags that have rules --- .../customers_controller.js.coffee | 12 +++++++- .../services/tags_resource.js.coffee | 9 ++++++ .../admin/filters/translate.js.coffee | 7 ----- .../shipping_methods.js.coffee | 2 +- .../tags_with_translation.js.coffee | 3 +- .../admin/utils/filters/translate.js.coffee | 7 +++++ .../javascripts/templates/admin/tag.html.haml | 8 +++++ .../admin/tag_autocomplete.html.haml | 11 +++++++ .../templates/admin/tags_input.html.haml | 7 +++++ .../stylesheets/admin/customers.css.scss | 3 ++ app/controllers/admin/tags_controller.rb | 28 ++++++++++++++++++ app/models/enterprise.rb | 14 +++++++++ app/models/spree/ability_decorator.rb | 9 ++---- .../api/admin/customer_serializer.rb | 6 +++- app/views/admin/customers/index.html.haml | 2 +- config/locales/en.yml | 4 +++ config/routes.rb | 2 ++ .../customers_controller_spec.js.coffee | 29 +++++++++++++++++++ .../admin/customer_serializer_spec.rb | 14 +++++++++ 19 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/admin/customers/services/tags_resource.js.coffee delete mode 100644 app/assets/javascripts/admin/filters/translate.js.coffee create mode 100644 app/assets/javascripts/admin/utils/filters/translate.js.coffee create mode 100644 app/assets/javascripts/templates/admin/tag.html.haml create mode 100644 app/assets/javascripts/templates/admin/tag_autocomplete.html.haml create mode 100644 app/assets/javascripts/templates/admin/tags_input.html.haml create mode 100644 app/assets/stylesheets/admin/customers.css.scss create mode 100644 app/controllers/admin/tags_controller.rb create mode 100644 spec/serializers/admin/customer_serializer_spec.rb diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index d2e6d58562..bfccfd3f4b 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, Columns, pendingChanges, shops) -> +angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, TagsResource, $q, Columns, pendingChanges, shops) -> $scope.shop = {} $scope.shops = shops $scope.submitAll = pendingChanges.submitAll @@ -12,6 +12,16 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerR if $scope.shop.id? $scope.customers = index {enterprise_id: $scope.shop.id} + $scope.findTags = (query) -> + defer = $q.defer() + params = + enterprise_id: $scope.shop.id + TagsResource.index params, (data) => + filtered = data.filter (tag) -> + tag.text.toLowerCase().indexOf(query.toLowerCase()) != -1 + defer.resolve filtered + defer.promise + $scope.add = (email) -> params = enterprise_id: $scope.shop.id diff --git a/app/assets/javascripts/admin/customers/services/tags_resource.js.coffee b/app/assets/javascripts/admin/customers/services/tags_resource.js.coffee new file mode 100644 index 0000000000..ad89511e32 --- /dev/null +++ b/app/assets/javascripts/admin/customers/services/tags_resource.js.coffee @@ -0,0 +1,9 @@ +angular.module("admin.customers").factory 'TagsResource', ($resource) -> + $resource('/admin/tags.json', {}, { + 'index': + method: 'GET' + isArray: true + cache: true + params: + enterprise_id: '@enterprise_id' + }) diff --git a/app/assets/javascripts/admin/filters/translate.js.coffee b/app/assets/javascripts/admin/filters/translate.js.coffee deleted file mode 100644 index 20becc147a..0000000000 --- a/app/assets/javascripts/admin/filters/translate.js.coffee +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('ofn.admin').filter "translate", -> - (key, options) -> - t(key, options) - -angular.module('ofn.admin').filter "t", -> - (key, options) -> - t(key, options) diff --git a/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee b/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee index 232eee7045..863163a9ef 100644 --- a/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee +++ b/app/assets/javascripts/admin/shipping_methods/shipping_methods.js.coffee @@ -1 +1 @@ -angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils']) +angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils', 'templates']) diff --git a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee index 6ce7953608..7a6ae9afff 100644 --- a/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/tags_with_translation.js.coffee @@ -1,10 +1,11 @@ angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) -> restrict: "E" - template: "" + templateUrl: "admin/tags_input.html" scope: object: "=" tagsAttr: "@?" tagListAttr: "@?" + findTags: "&" link: (scope, element, attrs) -> $timeout -> scope.tagsAttr ||= "tags" diff --git a/app/assets/javascripts/admin/utils/filters/translate.js.coffee b/app/assets/javascripts/admin/utils/filters/translate.js.coffee new file mode 100644 index 0000000000..1a0602a59d --- /dev/null +++ b/app/assets/javascripts/admin/utils/filters/translate.js.coffee @@ -0,0 +1,7 @@ +angular.module("admin.utils").filter "translate", -> + (key, options) -> + t(key, options) + +angular.module("admin.utils").filter "t", -> + (key, options) -> + t(key, options) diff --git a/app/assets/javascripts/templates/admin/tag.html.haml b/app/assets/javascripts/templates/admin/tag.html.haml new file mode 100644 index 0000000000..c4afcfa5ad --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag.html.haml @@ -0,0 +1,8 @@ +.tag-template + %div + %span.tag-with-rules{ ng: { if: "data.rules" }, "ofn-with-tip" => "{{ 'admin.tag_has_rules' | t:{num: data.rules} }}" } + {{$getDisplayText()}} + %span{ ng: { if: "!data.rules" } } + {{$getDisplayText()}} + %a.remove-button{ ng: {click: "$removeTag()"} } + ✖ diff --git a/app/assets/javascripts/templates/admin/tag_autocomplete.html.haml b/app/assets/javascripts/templates/admin/tag_autocomplete.html.haml new file mode 100644 index 0000000000..49ff00ff27 --- /dev/null +++ b/app/assets/javascripts/templates/admin/tag_autocomplete.html.haml @@ -0,0 +1,11 @@ +.autocomplete-template + %span.tag-with-rules{ ng: { if: "data.rules" } } + {{$getDisplayText()}} + %span.tag-with-rules{ ng: { if: "data.rules == 1" } } + — + = t 'admin.has_one_rule' + %span.tag-with-rules{ ng: { if: "data.rules > 1" } } + — + = t 'admin.has_n_rules', { num: '{{data.rules}}' } + %span{ ng: { if: "!data.rules" } } + {{$getDisplayText()}} diff --git a/app/assets/javascripts/templates/admin/tags_input.html.haml b/app/assets/javascripts/templates/admin/tags_input.html.haml new file mode 100644 index 0000000000..2dd75ce5ac --- /dev/null +++ b/app/assets/javascripts/templates/admin/tags_input.html.haml @@ -0,0 +1,7 @@ +%tags-input{ template: 'admin/tag.html', ng: { model: 'object[tagsAttr]' } } + %auto-complete{source: "findTags({query: $query})", + template: "admin/tag_autocomplete.html", + "min-length" => "0", + "load-on-focus" => "true", + "load-on-empty" => "true", + "max-results-to-show" => "32"} diff --git a/app/assets/stylesheets/admin/customers.css.scss b/app/assets/stylesheets/admin/customers.css.scss new file mode 100644 index 0000000000..e3c427649c --- /dev/null +++ b/app/assets/stylesheets/admin/customers.css.scss @@ -0,0 +1,3 @@ +.tag-with-rules { + color: black; +} diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb new file mode 100644 index 0000000000..3ab5685ffe --- /dev/null +++ b/app/controllers/admin/tags_controller.rb @@ -0,0 +1,28 @@ +module Admin + class TagsController < Spree::Admin::BaseController + respond_to :json + + def index + respond_to do |format| + format.json do + serialiser = ActiveModel::ArraySerializer.new(tags_of_enterprise) + render json: serialiser.to_json + end + end + end + + private + + def enterprise + Enterprise.managed_by(spree_current_user).find_by_id(params[:enterprise_id]) + end + + def tags_of_enterprise + return [] unless enterprise + tag_rule_map = enterprise.rules_per_tag + tag_rule_map.keys.map do |tag| + { text: tag, rules: tag_rule_map[tag] } + end + end + end +end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index dfc5050a38..d3e86ff3ca 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -352,6 +352,20 @@ class Enterprise < ActiveRecord::Base end end + def rules_per_tag + tag_rule_map = {} + tag_rules.each do |rule| + rule.preferred_customer_tags.split(",").each do |tag| + if tag_rule_map[tag] + tag_rule_map[tag] += 1 + else + tag_rule_map[tag] = 1 + end + end + end + tag_rule_map + end + protected def devise_mailer diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 867d3c9e2b..e250d1341d 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -101,11 +101,6 @@ class AbilityDecorator can [:print], Spree::Order do |order| order.user == user end - - can [:create], Customer - can [:destroy], Customer do |customer| - user.enterprises.include? customer.enterprise - end end def add_product_management_abilities(user) @@ -221,7 +216,9 @@ class AbilityDecorator # Reports page can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], :report - can [:admin, :index, :update], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id) + can [:create], Customer + can [:admin, :index, :update, :destroy], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id) + can [:admin, :index], :tag end diff --git a/app/serializers/api/admin/customer_serializer.rb b/app/serializers/api/admin/customer_serializer.rb index 3cb9518a9f..052f49a917 100644 --- a/app/serializers/api/admin/customer_serializer.rb +++ b/app/serializers/api/admin/customer_serializer.rb @@ -6,6 +6,10 @@ class Api::Admin::CustomerSerializer < ActiveModel::Serializer end def tags - object.tag_list.map{ |t| { text: t } } + tag_rule_map = object.enterprise.rules_per_tag + object.tag_list.map do |tag| + { text: tag, rules: tag_rule_map[tag] } + end end + end diff --git a/app/views/admin/customers/index.html.haml b/app/views/admin/customers/index.html.haml index e33b871df4..0a348d4a0a 100644 --- a/app/views/admin/customers/index.html.haml +++ b/app/views/admin/customers/index.html.haml @@ -56,7 +56,7 @@ %input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" } %td.tags{ 'ng-show' => 'columns.tags.visible' } .tag_watcher{ 'obj-for-update' => "customer", "attr-for-update" => "tag_list"} - %tags_with_translation{ object: 'customer' } + %tags_with_translation{ object: 'customer', 'find-tags' => 'findTags(query)' } %td.actions %a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" } %input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' } diff --git a/config/locales/en.yml b/config/locales/en.yml index 7b5f9b168f..e9de6c9abd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -80,6 +80,10 @@ en: whats_this: What's this? + tag_has_rules: "Existing rules for this tag: %{num}" + has_one_rule: "has one rule" + has_n_rules: "has %{num} rules" + customers: index: add_customer: "Add customer" diff --git a/config/routes.rb b/config/routes.rb index 8d8d6d27c3..e240f901a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -117,6 +117,8 @@ Openfoodnetwork::Application.routes.draw do resources :customers, only: [:index, :create, :update, :destroy] + resources :tags, only: [:index], format: :json + resource :content resource :accounts_and_billing_settings, only: [:edit, :update] do diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee index 215f2834ed..6981ef4e4f 100644 --- a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -47,3 +47,32 @@ describe "CustomersCtrl", -> http.flush() expect(scope.customers.length).toBe 1 expect(scope.customers[0]).not.toAngularEqual customer + + describe "scope.findTags", -> + tags = [ + { text: 'one' } + { text: 'two' } + { text: 'three' } + ] + beforeEach -> + http.expectGET('/admin/tags.json?enterprise_id=1').respond 200, tags + + it "retrieves the tag list", -> + promise = scope.findTags('') + result = null + promise.then (data) -> + result = data + http.flush() + expect(result).toAngularEqual tags + + it "filters the tag list", -> + filtered_tags = [ + { text: 'two' } + { text: 'three' } + ] + promise = scope.findTags('t') + result = null + promise.then (data) -> + result = data + http.flush() + expect(result).toAngularEqual filtered_tags diff --git a/spec/serializers/admin/customer_serializer_spec.rb b/spec/serializers/admin/customer_serializer_spec.rb new file mode 100644 index 0000000000..d63f00d4aa --- /dev/null +++ b/spec/serializers/admin/customer_serializer_spec.rb @@ -0,0 +1,14 @@ +describe Api::Admin::CustomerSerializer do + let(:customer) { create(:customer, tag_list: "one, two, three") } + let!(:tag_rule) { create(:tag_rule, enterprise: customer.enterprise, preferred_customer_tags: "two") } + + it "serializes a customer" do + serializer = Api::Admin::CustomerSerializer.new customer + result = JSON.parse(serializer.to_json) + expect(result['email']).to eq customer.email + tags = result['tags'] + expect(tags.length).to eq 3 + expect(tags[0]).to eq({ "text" => 'one', "rules" => nil }) + expect(tags[1]).to eq({ "text" => 'two', "rules" => 1 }) + end +end