diff --git a/app/assets/javascripts/admin/bulk_product_update.js b/app/assets/javascripts/admin/bulk_product_update.js index 19cb7bb3f3..382db20e11 100644 --- a/app/assets/javascripts/admin/bulk_product_update.js +++ b/app/assets/javascripts/admin/bulk_product_update.js @@ -74,6 +74,27 @@ productsApp.directive('ngTrackVariant', function(){ } }); +productsApp.directive('ngToggleVariants',function(){ + return { + link: function(scope,element,attrs){ + element.bind('click', function(){ + scope.$apply(function(){ + if (scope.displayProperties[scope.product.id].showVariants){ + scope.displayProperties[scope.product.id].showVariants = false; + element.removeClass('icon-chevron-down'); + element.addClass('icon-chevron-right'); + } + else { + scope.displayProperties[scope.product.id].showVariants = true; + element.removeClass('icon-chevron-right'); + element.addClass('icon-chevron-down'); + } + }); + }); + } + }; +}); + productsApp.controller('AdminBulkProductsCtrl', function($scope, $timeout, $http, dataFetcher) { $scope.dirtyProducts = {}; @@ -90,7 +111,11 @@ productsApp.controller('AdminBulkProductsCtrl', function($scope, $timeout, $http $scope.refreshProducts = function(){ dataFetcher('/admin/products/bulk_index.json').then(function(data){ - $scope.products = toObjectWithIDKeys(data); + $scope.products = data; + $scope.displayProperties = {}; + angular.forEach($scope.products,function(product){ + $scope.displayProperties[product.id] = { showVariants: false } + }); }); }; @@ -111,8 +136,9 @@ productsApp.controller('AdminBulkProductsCtrl', function($scope, $timeout, $http url: '/admin/products/'+product.permalink_live+".js" }) .success(function(data){ - delete $scope.products[product.id] - if ($scope.dirtyProducts.hasOwnProperty(product.id)) delete $scope.dirtyProducts[product.id] + $scope.products.splice($scope.products.indexOf(product),1); + if ($scope.dirtyProducts.hasOwnProperty(product.id)) delete $scope.dirtyProducts[product.id]; + $scope.displayDirtyProducts(); }) } } @@ -124,8 +150,9 @@ productsApp.controller('AdminBulkProductsCtrl', function($scope, $timeout, $http url: '/admin/products/'+product.permalink_live+"/variants/"+variant.id+".js" }) .success(function(data){ - delete $scope.products[product.id].variants[variant.id] - if ($scope.dirtyProducts.hasOwnProperty(product.id) && $scope.dirtyProducts[product.id].hasOwnProperty("variants") && $scope.dirtyProducts[product.id].variants.hasOwnProperty(variant.id)) delete $scope.dirtyProducts[product.id].variants[variant.id] + product.variants.splice(product.variants.indexOf(variant),1); + if ($scope.dirtyProducts.hasOwnProperty(product.id) && $scope.dirtyProducts[product.id].hasOwnProperty("variants") && $scope.dirtyProducts[product.id].variants.hasOwnProperty(variant.id)) delete $scope.dirtyProducts[product.id].variants[variant.id]; + $scope.displayDirtyProducts(); }) } } @@ -138,8 +165,7 @@ productsApp.controller('AdminBulkProductsCtrl', function($scope, $timeout, $http var id = data.product.id; dataFetcher("/admin/products/bulk_index.json?q[id_eq]="+id).then(function(data){ var newProduct = data[0]; - newProduct.variants = toObjectWithIDKeys(newProduct.variants) - $scope.products[newProduct.id] = newProduct; + $scope.products.push(newProduct); }); }); } @@ -156,7 +182,6 @@ productsApp.controller('AdminBulkProductsCtrl', function($scope, $timeout, $http data: productsToSubmit }) .success(function(data){ - data = toObjectWithIDKeys(data); if (angular.toJson($scope.products) == angular.toJson(data)){ $scope.products = data; $scope.dirtyProducts = {}; diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index 8cb9b9a778..ec719286df 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -1,3 +1,7 @@ #product_distributors_field span { display: block; } + +th.firstcol, td.firstcol { + border-left: 1px solid #cee1f4; +} diff --git a/app/views/spree/admin/products/bulk_index.html.haml b/app/views/spree/admin/products/bulk_index.html.haml index 4b5f2056f2..5d3664acae 100644 --- a/app/views/spree/admin/products/bulk_index.html.haml +++ b/app/views/spree/admin/products/bulk_index.html.haml @@ -11,11 +11,17 @@ %div#new_product(data-hook) -%div{ 'ng-app' => 'bulk_product_update', 'ng-controller' => 'AdminBulkProductsCtrl', 'ng-init' => 'refreshSuppliers(); refreshProducts()' } +%div{ 'ng-app' => 'bulk_product_update', 'ng-controller' => 'AdminBulkProductsCtrl', 'ng-init' => 'refreshSuppliers(); refreshProducts();' } + %div.options + Filter Results: + %input.search{ 'ng-model' => 'query', :type => 'text', 'placeholder' => 'Search Value' } + %br.clear + %br.clear %table.index#listing_products %thead %tr - %th Name + %th.actions + %th.firstcol Name %th Supplier %th Price %th On Hand @@ -23,7 +29,9 @@ %th.actions %tbody{ 'ng-repeat' => 'product in products | filter:query' } %tr - %td + %td.actions + %a{ 'ng-toggle-variants' => 'true', :class => "view-variants icon-chevron-right", 'ng-show' => 'hasVariants(product)' } + %td.firstcol %input{ 'ng-model' => "product.name", :name => 'product_name', 'ng-track-product' => 'name', :type => 'text' } %td %select.select2{ 'ng-model' => 'product.supplier_id', :name => 'supplier_id', 'ng-track-product' => 'supplier_id', 'ng-options' => 's.id as s.name for s in suppliers' } @@ -38,8 +46,10 @@ %a{ 'ng-click' => 'editWarn(product)', :class => "edit-product icon-edit no-text" } %a{ 'ng-click' => 'cloneProduct(product)', :class => "clone-product icon-copy no-text" } %a{ 'ng-click' => 'deleteProduct(product)', :class => "delete-product icon-trash no-text" } - %tr{ 'ng-repeat' => 'variant in product.variants' } - %td + %tr{ 'ng-repeat' => 'variant in product.variants', 'ng-show' => 'displayProperties[product.id].showVariants' } + %td.actions + %a{ :class => "variant-item icon-caret-right" } + %td.firstcol {{ variant.options_text }} %td %td diff --git a/app/views/spree/admin/products/bulk_index.rep b/app/views/spree/admin/products/bulk_index.rep index 9071e4abff..cf850100c0 100644 --- a/app/views/spree/admin/products/bulk_index.rep +++ b/app/views/spree/admin/products/bulk_index.rep @@ -1,4 +1,4 @@ -r.list_of :products, @collection do +r.list_of :products, @collection.sort_by(&:id) do r.element :id r.element :name r.element :supplier_id diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 72616ded47..0898f57a07 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -110,6 +110,8 @@ feature %q{ v2 = FactoryGirl.create(:variant) visit '/admin/products/bulk_index' + page.should have_selector "a.view-variants" + first("a.view-variants").click page.should have_field "product_name", with: v1.product.name page.should have_field "product_name", with: v2.product.name @@ -213,6 +215,8 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_index' + page.should have_selector "a.view-variants" + first("a.view-variants").click page.should have_field "variant_price", with: "3.0" page.should have_field "variant_on_hand", with: "9" @@ -227,6 +231,8 @@ feature %q{ page.find("span#update-status-message").should have_content "Update complete" visit '/admin/products/bulk_index' + page.should have_selector "a.view-variants" + first("a.view-variants").click page.should have_field "variant_price", with: "4.0" page.should have_field "variant_on_hand", with: "10" @@ -239,6 +245,8 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_index' + page.should have_selector "a.view-variants" + first("a.view-variants").click page.should have_field "variant_price", with: "3.0" @@ -248,6 +256,8 @@ feature %q{ page.find("span#update-status-message").should have_content "Update complete" visit '/admin/products/bulk_index' + page.should have_selector "a.view-variants" + first("a.view-variants").click page.should have_field "variant_price", with: "10.0" end @@ -291,6 +301,7 @@ feature %q{ first("a.delete-product").click page.driver.browser.switch_to.alert.accept + page.should have_selector "a.delete-product" page.should have_selector "a.delete-product", :count => 2 #page.should have_selector "div.flash.notice", text: "Product has been deleted." @@ -306,6 +317,8 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_index' + page.should have_selector "a.view-variants" + all("a.view-variants").each{ |e| e.click } page.should have_selector "a.delete-variant", :count => 3 @@ -316,6 +329,8 @@ feature %q{ #page.should have_selector "div.flash.notice", text: "Product has been deleted." visit '/admin/products/bulk_index' + page.should have_selector "a.view-variants" + all("a.view-variants").select{ |e| e.visible? }.each{ |e| e.click } page.should have_selector "a.delete-variant", :count => 2 end @@ -344,6 +359,8 @@ feature %q{ login_to_admin_section visit '/admin/products/bulk_index' + page.should have_selector "a.view-variants" + first("a.view-variants").click page.should have_selector "a.edit-variant", :count => 3 @@ -368,6 +385,11 @@ feature %q{ page.should have_selector "a.clone-product", :count => 4 page.should have_field "product_name", with: "COPY OF #{p1.name}" + + visit '/admin/products/bulk_index' + + page.should have_selector "a.clone-product", :count => 4 + page.should have_field "product_name", with: "COPY OF #{p1.name}" end end end diff --git a/spec/javascripts/unit/bulk_product_update_spec.js b/spec/javascripts/unit/bulk_product_update_spec.js index f90289db01..64552cffcc 100644 --- a/spec/javascripts/unit/bulk_product_update_spec.js +++ b/spec/javascripts/unit/bulk_product_update_spec.js @@ -206,12 +206,11 @@ describe("AdminBulkProductsCtrl", function(){ expect(scope.suppliers).toEqual("list of suppliers"); }); - it("gets a list of products which is then passed to toObjectWithIDKeys()", function(){ + it("gets a list of products", function(){ httpBackend.expectGET('/admin/products/bulk_index.json').respond("list of products"); - spyOn(window, "toObjectWithIDKeys").andReturn("product object with ids as keys") scope.refreshProducts(); httpBackend.flush(); - expect(scope.products).toEqual("product object with ids as keys"); + expect(scope.products).toEqual("list of products"); }); }); @@ -289,7 +288,7 @@ describe("AdminBulkProductsCtrl", function(){ it("runs displaySuccess() when post returns success",function(){ spyOn(scope, "displaySuccess"); - scope.products = { 1: { id: 1, name: "P1" }, 2: { id: 2, name: "P2" } }; + scope.products = [ { id: 1, name: "P1" }, { id: 2, name: "P2" } ]; httpBackend.expectPOST('/admin/products/bulk_update').respond(200, [ { id: 1, name: "P1" }, { id: 2, name: "P2" } ] ); scope.updateProducts("list of dirty products"); httpBackend.flush(); @@ -323,20 +322,20 @@ describe("AdminBulkProductsCtrl", function(){ it("deletes products with a http delete request to /admin/products/(permalink).js", function(){ spyOn(window, "confirm").andReturn(true); - scope.products = { 9: { id: 9, permalink_live: "apples" }, 13: { id: 13, permalink_live: "oranges" } }; + scope.products = [ { id: 9, permalink_live: "apples" }, { id: 13, permalink_live: "oranges" } ]; httpBackend.expectDELETE('/admin/products/oranges.js').respond(200, "data"); - scope.deleteProduct(scope.products[13]); + scope.deleteProduct(scope.products[1]); httpBackend.flush(); }); it("removes the specified product from both scope.products and scope.dirtyProducts (if it exists there)", function(){ spyOn(window, "confirm").andReturn(true); - scope.products = { 9: { id: 9, permalink_live: "apples" }, 13: { id: 13, permalink_live: "oranges" } }; + scope.products = [ { id: 9, permalink_live: "apples" }, { id: 13, permalink_live: "oranges" } ]; scope.dirtyProducts = { 9: { id: 9, someProperty: "something" }, 13: { id: 13, name: "P1" } }; httpBackend.expectDELETE('/admin/products/oranges.js').respond(200, "data"); - scope.deleteProduct(scope.products[13]); + scope.deleteProduct(scope.products[1]); httpBackend.flush(); - expect(scope.products).toEqual( { 9: { id: 9, permalink_live: "apples" } } ); + expect(scope.products).toEqual( [ { id: 9, permalink_live: "apples" } ] ); expect(scope.dirtyProducts).toEqual( { 9: { id: 9, someProperty: "something" } } ); }); }); @@ -348,20 +347,20 @@ describe("AdminBulkProductsCtrl", function(){ it("deletes variants with a http delete request to /admin/products/(permalink)/variants/(variant_id).js", function(){ spyOn(window, "confirm").andReturn(true); - scope.products = { 9: { id: 9, permalink_live: "apples", variants: { 3: { id: 3, price: 12 } } }, 13: { id: 13, permalink_live: "oranges" } }; + scope.products = [ { id: 9, permalink_live: "apples", variants: [ { id: 3, price: 12 } ] }, { id: 13, permalink_live: "oranges" } ]; httpBackend.expectDELETE('/admin/products/apples/variants/3.js').respond(200, "data"); - scope.deleteVariant(scope.products[9],scope.products[9].variants[3]); + scope.deleteVariant(scope.products[0],scope.products[0].variants[0]); httpBackend.flush(); }); it("removes the specified variant from both the variants object and scope.dirtyProducts (if it exists there)", function(){ spyOn(window, "confirm").andReturn(true); - scope.products = { 9: { id: 9, permalink_live: "apples", variants: { 3: { id: 3, price: 12.0 }, 4: { id: 4, price: 6.0 } } }, 13: { id: 13, permalink_live: "oranges" } }; + scope.products = [ { id: 9, permalink_live: "apples", variants: [ { id: 3, price: 12.0 }, { id: 4, price: 6.0 } ] }, { id: 13, permalink_live: "oranges" } ]; scope.dirtyProducts = { 9: { id: 9, variants: { 3: { id: 3, price: 12.0 }, 4: { id: 4, price: 6.0 } } }, 13: { id: 13, name: "P1" } }; httpBackend.expectDELETE('/admin/products/apples/variants/3.js').respond(200, "data"); - scope.deleteVariant(scope.products[9],scope.products[9].variants[3]); + scope.deleteVariant(scope.products[0],scope.products[0].variants[0]); httpBackend.flush(); - expect(scope.products[9].variants).toEqual( { 4: { id: 4, price: 6.0 } } ); + expect(scope.products[0].variants).toEqual( [ { id: 4, price: 6.0 } ] ); expect(scope.dirtyProducts).toEqual( { 9: { id: 9, variants: { 4: { id: 4, price: 6.0 } } }, 13: { id: 13, name: "P1" } } ); }); }); @@ -372,22 +371,20 @@ describe("AdminBulkProductsCtrl", function(){ }); it("clones products using a http get request to /admin/products/(permalink)/clone.json", function(){ - scope.products = { 13: { id: 13, permalink_live: "oranges" } } + scope.products = [ { id: 13, permalink_live: "oranges" } ]; httpBackend.expectGET('/admin/products/oranges/clone.json').respond(200, { product: { id: 17, name: "new_product" } } ); httpBackend.expectGET('/admin/products/bulk_index.json?q[id_eq]=17').respond(200, [ { id: 17, name: "new_product" } ] ); - scope.cloneProduct(scope.products[13]); + scope.cloneProduct(scope.products[0]); httpBackend.flush(); }); - it("adds the newly created product to scope.products, sending variants to toObjectWithIDKeys()", function(){ - spyOn(window, "toObjectWithIDKeys").andCallThrough(); - scope.products = { 13: { id: 13, permalink_live: "oranges" } }; + it("adds the newly created product to scope.products", function(){ + scope.products = [ { id: 13, permalink_live: "oranges" } ]; httpBackend.expectGET('/admin/products/oranges/clone.json').respond(200, { product: { id: 17, name: "new_product", variants: [ { id: 3, name: "V1" } ] } } ); httpBackend.expectGET('/admin/products/bulk_index.json?q[id_eq]=17').respond(200, [ { id: 17, name: "new_product", variants: [ { id: 3, name: "V1" } ] } ] ); - scope.cloneProduct(scope.products[13]); + scope.cloneProduct(scope.products[0]); httpBackend.flush(); - expect(toObjectWithIDKeys).toHaveBeenCalledWith([ { id: 3, name: "V1" } ]) - expect(scope.products).toEqual( { 13: { id: 13, permalink_live: "oranges" }, 17: { id: 17, name: "new_product", variants: { 3: { id: 3, name: "V1" } } } } ); + expect(scope.products).toEqual( [ { id: 13, permalink_live: "oranges" }, { id: 17, name: "new_product", variants: [ { id: 3, name: "V1" } ] } ] ); }); }); });