mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
BPUR: change to live tracking of dirty properties (variant price update still failing)
This commit is contained in:
@@ -36,29 +36,69 @@ productsApp.directive('ngDecimal', function () {
|
||||
}
|
||||
});
|
||||
|
||||
productsApp.directive('ngTrackProduct', function(){
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, ngModel) {
|
||||
var property = attrs.ngTrackProduct;
|
||||
var clean_value = angular.copy(scope.product[property]);
|
||||
element.bind('blur', function() {
|
||||
if (scope.product[property] == clean_value) removeCleanProperty(scope.dirtyProducts, scope.product.id, property);
|
||||
else addDirtyProperty(scope.dirtyProducts, scope.product.id, property, scope.product[property]);
|
||||
scope.$apply(scope.displayDirtyProducts());
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
productsApp.directive('ngTrackVariant', function(){
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, ngModel) {
|
||||
var property = attrs.ngTrackVariant;
|
||||
var clean_value = angular.copy(scope.variant[property]);
|
||||
element.bind('blur', function() {
|
||||
var dirtyVariants = {};
|
||||
if (scope.dirtyProducts.hasOwnProperty(scope.product.id) && scope.dirtyProducts[scope.product.id].hasOwnProperty("variants")) dirtyVariants = scope.dirtyProducts[scope.product.id].variants;
|
||||
if (scope.variant[property] == clean_value){
|
||||
removeCleanProperty(dirtyVariants, scope.variant.id, property);
|
||||
if (dirtyVariants == {}) removeCleanProperty(scope.dirtyProducts, scope.product.id, "variants");
|
||||
}
|
||||
else {
|
||||
addDirtyProperty(dirtyVariants, scope.variant.id, property, scope.variant[property]);
|
||||
addDirtyProperty(scope.dirtyProducts, scope.product.id, "variants", dirtyVariants);
|
||||
}
|
||||
scope.$apply(scope.displayDirtyProducts());
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
productsApp.controller('AdminBulkProductsCtrl', function($scope, $timeout, $http, dataFetcher) {
|
||||
$scope.dirtyProducts = {};
|
||||
|
||||
$scope.updateStatusMessage = {
|
||||
text: "",
|
||||
style: {}
|
||||
}
|
||||
|
||||
$scope.refreshSuppliers = function(){
|
||||
dataFetcher('/enterprises/suppliers.json').then(function(data){
|
||||
$scope.suppliers = data;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
$scope.refreshProducts = function(){
|
||||
dataFetcher('/admin/products/bulk_index.json').then(function(data){
|
||||
$scope.products = angular.copy(data);
|
||||
$scope.cleanProducts = angular.copy(data);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
$scope.updateOnHand = function(product){
|
||||
product.on_hand = onHand(product);
|
||||
}
|
||||
|
||||
$scope.updateStatusMessage = {
|
||||
text: "",
|
||||
style: {}
|
||||
}
|
||||
|
||||
$scope.updateProducts = function(productsToSubmit){
|
||||
$scope.displayUpdating();
|
||||
$http({
|
||||
@@ -68,6 +108,7 @@ productsApp.controller('AdminBulkProductsCtrl', function($scope, $timeout, $http
|
||||
})
|
||||
.success(function(data){
|
||||
if (angular.toJson($scope.products) == angular.toJson(data)){
|
||||
$scope.products = angular.copy(data);
|
||||
$scope.cleanProducts = angular.copy(data);
|
||||
$scope.displaySuccess();
|
||||
}
|
||||
@@ -79,13 +120,12 @@ productsApp.controller('AdminBulkProductsCtrl', function($scope, $timeout, $http
|
||||
$scope.displayFailure("Server returned with error status: "+status);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$scope.prepareProductsForSubmit = function(){
|
||||
var productsToSubmit = getDirtyObjects($scope.products,$scope.cleanProducts);
|
||||
productsToSubmit = filterSubmitProducts(productsToSubmit);
|
||||
var productsToSubmit = filterSubmitProducts($scope.dirtyProducts);
|
||||
$scope.updateProducts(productsToSubmit);
|
||||
}
|
||||
|
||||
|
||||
$scope.setMessage = function(model,text,style,timeout){
|
||||
model.text = text;
|
||||
model.style = style;
|
||||
@@ -93,18 +133,24 @@ productsApp.controller('AdminBulkProductsCtrl', function($scope, $timeout, $http
|
||||
$timeout(function() { $scope.setMessage(model,"",{},false); }, timeout, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$scope.displayUpdating = function(){
|
||||
$scope.setMessage($scope.updateStatusMessage,"Updating...",{ color: "orange" },false);
|
||||
}
|
||||
|
||||
|
||||
$scope.displaySuccess = function(){
|
||||
$scope.setMessage($scope.updateStatusMessage,"Update complete",{ color: "green" },3000);
|
||||
}
|
||||
|
||||
|
||||
$scope.displayFailure = function(failMessage){
|
||||
$scope.setMessage($scope.updateStatusMessage,"Updating failed. "+failMessage,{ color: "red" },10000);
|
||||
}
|
||||
|
||||
$scope.displayDirtyProducts = function(){
|
||||
var changedProductCount = Object.keys($scope.dirtyProducts).length;
|
||||
if (changedProductCount > 0) $scope.setMessage($scope.updateStatusMessage,"Changes to "+Object.keys($scope.dirtyProducts).length+" products remain unsaved.",{ color: "gray" },false);
|
||||
else $scope.setMessage($scope.updateStatusMessage,"",{},false);
|
||||
}
|
||||
});
|
||||
|
||||
productsApp.factory('dataFetcher', function($http,$q){
|
||||
@@ -221,31 +267,33 @@ function getDirtyObjects(testObjects, cleanObjects){
|
||||
function filterSubmitProducts(productsToFilter){
|
||||
var filteredProducts= [];
|
||||
|
||||
if (productsToFilter instanceof Array){
|
||||
for (i in productsToFilter) {
|
||||
if (productsToFilter[i].hasOwnProperty("id")){
|
||||
if (productsToFilter instanceof Object){
|
||||
var productKeys = Object.keys(productsToFilter);
|
||||
for (i in productKeys) {
|
||||
if (productsToFilter[productKeys[i]].hasOwnProperty("id")){
|
||||
var filteredProduct = {};
|
||||
var filteredVariants = [];
|
||||
|
||||
if (productsToFilter[i].hasOwnProperty("variants")){
|
||||
for (j in productsToFilter[i].variants){
|
||||
if (productsToFilter[i].variants[j].deleted_at == null && productsToFilter[i].variants[j].hasOwnProperty("id")){
|
||||
if (productsToFilter[productKeys[i]].hasOwnProperty("variants")){
|
||||
var variantKeys = Object.keys(productsToFilter[productKeys[i]].variants);
|
||||
for (j in variantKeys){
|
||||
if (productsToFilter[productKeys[i]].variants[variantKeys[j]].deleted_at == null && productsToFilter[productKeys[i]].variants[variantKeys[j]].hasOwnProperty("id")){
|
||||
filteredVariants[j] = {};
|
||||
filteredVariants[j].id = productsToFilter[i].variants[j].id;
|
||||
if (productsToFilter[i].variants[j].hasOwnProperty("on_hand")) filteredVariants[j].on_hand = productsToFilter[i].variants[j].on_hand;
|
||||
if (productsToFilter[i].variants[j].hasOwnProperty("price")) filteredVariants[j].price = productsToFilter[i].variants[j].price;
|
||||
filteredVariants[j].id = productsToFilter[productKeys[i]].variants[variantKeys[j]].id;
|
||||
if (productsToFilter[productKeys[i]].variants[variantKeys[j]].hasOwnProperty("on_hand")) filteredVariants[j].on_hand = productsToFilter[productKeys[i]].variants[variantKeys[j]].on_hand;
|
||||
if (productsToFilter[productKeys[i]].variants[variantKeys[j]].hasOwnProperty("price")) filteredVariants[j].price = productsToFilter[productKeys[i]].variants[variantKeys[j]].price;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hasUpdatableProperty = false;
|
||||
filteredProduct.id = productsToFilter[i].id;
|
||||
if (productsToFilter[i].hasOwnProperty("name")) { filteredProduct.name = productsToFilter[i].name; hasUpdatableProperty = true; }
|
||||
if (productsToFilter[i].hasOwnProperty("supplier_id")) { filteredProduct.supplier_id = productsToFilter[i].supplier_id; hasUpdatableProperty = true; }
|
||||
//if (productsToFilter[i].hasOwnProperty("master")) filteredProduct.master_attributes = productsToFilter[i].master
|
||||
if (productsToFilter[i].hasOwnProperty("price")) { filteredProduct.price = productsToFilter[i].price; hasUpdatableProperty = true; }
|
||||
if (productsToFilter[i].hasOwnProperty("on_hand") && filteredVariants.length == 0) { filteredProduct.on_hand = productsToFilter[i].on_hand; hasUpdatableProperty = true; } //only update if no variants present
|
||||
if (productsToFilter[i].hasOwnProperty("available_on")) { filteredProduct.available_on = productsToFilter[i].available_on; hasUpdatableProperty = true; }
|
||||
filteredProduct.id = productsToFilter[productKeys[i]].id;
|
||||
if (productsToFilter[productKeys[i]].hasOwnProperty("name")) { filteredProduct.name = productsToFilter[productKeys[i]].name; hasUpdatableProperty = true; }
|
||||
if (productsToFilter[productKeys[i]].hasOwnProperty("supplier_id")) { filteredProduct.supplier_id = productsToFilter[productKeys[i]].supplier_id; hasUpdatableProperty = true; }
|
||||
//if (productsToFilter[productKeys[i]].hasOwnProperty("master")) filteredProduct.master_attributes = productsToFilter[productKeys[i]].master
|
||||
if (productsToFilter[productKeys[i]].hasOwnProperty("price")) { filteredProduct.price = productsToFilter[productKeys[i]].price; hasUpdatableProperty = true; }
|
||||
if (productsToFilter[productKeys[i]].hasOwnProperty("on_hand") && filteredVariants.length == 0) { filteredProduct.on_hand = productsToFilter[productKeys[i]].on_hand; hasUpdatableProperty = true; } //only update if no variants present
|
||||
if (productsToFilter[productKeys[i]].hasOwnProperty("available_on")) { filteredProduct.available_on = productsToFilter[productKeys[i]].available_on; hasUpdatableProperty = true; }
|
||||
if (filteredVariants.length > 0) { filteredProduct.variants_attributes = filteredVariants; hasUpdatableProperty = true; } // Note that the name of the property changes to enable mass assignment of variants attributes with rails
|
||||
|
||||
if (hasUpdatableProperty) filteredProducts.push(filteredProduct);
|
||||
@@ -255,6 +303,22 @@ function filterSubmitProducts(productsToFilter){
|
||||
return filteredProducts;
|
||||
}
|
||||
|
||||
function addDirtyProperty(dirtyObjects, objectID, propertyName, propertyValue){
|
||||
if (dirtyObjects.hasOwnProperty(objectID)){
|
||||
dirtyObjects[objectID][propertyName] = propertyValue;
|
||||
}
|
||||
else {
|
||||
dirtyObjects[objectID] = {};
|
||||
dirtyObjects[objectID]["id"] = objectID;
|
||||
dirtyObjects[objectID][propertyName] = propertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
function removeCleanProperty(dirtyObjects, objectID, propertyName){
|
||||
if (dirtyObjects.hasOwnProperty(objectID) && dirtyObjects[objectID].hasOwnProperty(propertyName)) delete dirtyObjects[objectID][propertyName];
|
||||
if (dirtyObjects.hasOwnProperty(objectID) && Object.keys(dirtyObjects[objectID]).length <= 1) delete dirtyObjects[objectID];
|
||||
}
|
||||
|
||||
function isEmpty(object){
|
||||
for (var i in object){
|
||||
if (object.hasOwnProperty(i)){
|
||||
|
||||
@@ -23,24 +23,24 @@
|
||||
%tbody{ 'ng-repeat' => 'product in products | filter:query' }
|
||||
%tr
|
||||
%td
|
||||
%input{ 'ng-model' => "product.name", :name => 'product_name', :type => 'text' }
|
||||
%input{ 'ng-model' => "product.name", :name => 'product_name', 'ng-track-product' => 'name', :type => 'text' }
|
||||
%td
|
||||
%select.select2{ :name => 'supplier_id', 'ng-model' => 'product.supplier_id', 'ng-options' => 's.id as s.name for s in suppliers' }
|
||||
%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' }
|
||||
%td
|
||||
%input{ 'ng-model' => 'product.price', 'ng-decimal' => :true, :name => 'price', :type => 'text' }
|
||||
%input{ 'ng-model' => 'product.price', 'ng-decimal' => :true, :name => 'price', 'ng-track-product' => 'price', :type => 'text' }
|
||||
%td
|
||||
%span{ 'ng-bind' => 'product.on_hand', :name => 'on_hand', 'ng-show' => 'product.variants.length > 0' }
|
||||
%input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ng-show' => 'product.variants.length == 0', :type => 'number' }
|
||||
%input.field{ 'ng-model' => 'product.on_hand', :name => 'on_hand', 'ng-track-product' => 'on_hand', 'ng-show' => 'product.variants.length == 0', :type => 'number' }
|
||||
%td
|
||||
%input{ 'ng-model' => 'product.available_on', :name => 'available_on', :type => 'text' }
|
||||
%input{ 'ng-model' => 'product.available_on', :name => 'available_on', 'ng-track-product' => 'available_on', :type => 'text' }
|
||||
%tr{ 'ng-repeat' => 'variant in product.variants' }
|
||||
%td
|
||||
{{ variant.options_text }}
|
||||
%td
|
||||
%td
|
||||
%input{ 'ng-model' => 'variant.price', 'ng-decimal' => :true, :name => 'variant_price', :type => 'text' }
|
||||
%input{ 'ng-model' => 'variant.price', 'ng-decimal' => :true, :name => 'variant_price', 'ng-track-variant' => 'price', :type => 'text' }
|
||||
%td
|
||||
%input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', :type => 'number' }
|
||||
%input.field{ 'ng-model' => 'variant.on_hand', 'ng-change' => 'updateOnHand(product)', :name => 'variant_on_hand', 'ng-track-variant' => 'on_hand', :type => 'number' }
|
||||
%td
|
||||
%input{:type => 'button', :value => 'Update', 'ng-click' => 'prepareProductsForSubmit()'}
|
||||
%span{ id: "update-status-message", 'ng-style' => 'updateStatusMessage.style' }
|
||||
|
||||
@@ -142,10 +142,11 @@ describe("Auxillary functions", function(){
|
||||
});
|
||||
|
||||
describe("filtering products", function(){
|
||||
it("only accepts and returns an array", function(){
|
||||
it("accepts an object or an array and only returns an array", function(){
|
||||
expect( filterSubmitProducts( [] ) ).toEqual([]);
|
||||
expect( filterSubmitProducts( {} ) ).toEqual([]);
|
||||
expect( filterSubmitProducts( { thingone: { id: 1, name: "lala" } } ) ).toEqual([]);
|
||||
expect( filterSubmitProducts( { 1: { id: 1, name: "lala" } } ) ).toEqual( [ { id: 1, name: "lala" } ] );
|
||||
expect( filterSubmitProducts( [ { id: 1, name: "lala" } ] ) ).toEqual( [ { id: 1, name: "lala" } ] );
|
||||
expect( filterSubmitProducts( 1 ) ).toEqual([]);
|
||||
expect( filterSubmitProducts( "2" ) ).toEqual([]);
|
||||
expect( filterSubmitProducts( null ) ).toEqual([]);
|
||||
@@ -283,6 +284,46 @@ describe("Auxillary functions", function(){
|
||||
});
|
||||
});
|
||||
|
||||
describe("Maintaining a live record of dirty products and properties", function(){
|
||||
describe("adding product properties to the dirtyProducts object", function(){ // Applies to both products and variants
|
||||
it("adds the product and the property to the list if property is dirty", function(){
|
||||
var dirtyProducts = { };
|
||||
addDirtyProperty(dirtyProducts, 1, "name", "Product 1");
|
||||
|
||||
expect(dirtyProducts).toEqual( { 1: { id: 1, name: "Product 1" } } );
|
||||
});
|
||||
|
||||
it("adds the relevant property to a product that is already in the list but which does not yet possess it if the property is dirty", function(){
|
||||
var dirtyProducts = { 1: { id: 1, notaname: "something" } };
|
||||
addDirtyProperty(dirtyProducts, 1, "name", "Product 3");
|
||||
|
||||
expect(dirtyProducts).toEqual( { 1: { id: 1, notaname: "something", name: "Product 3" } } );
|
||||
});
|
||||
|
||||
it("changes the relevant property of a product that is already in the list if the property is dirty", function(){
|
||||
var dirtyProducts = { 1: { id: 1, name: "Product 1" } };
|
||||
addDirtyProperty(dirtyProducts, 1, "name", "Product 2");
|
||||
|
||||
expect(dirtyProducts).toEqual( { 1: { id: 1, name: "Product 2" } } );
|
||||
});
|
||||
});
|
||||
|
||||
describe("removing properties of products which are clean", function(){
|
||||
it("removes the relevant property from a product if the property is clean and the product has that property", function(){
|
||||
var dirtyProducts = { 1: { id: 1, someProperty: "something", name: "Product 1" } };
|
||||
removeCleanProperty(dirtyProducts, 1, "name", "Product 1");
|
||||
|
||||
expect(dirtyProducts).toEqual( { 1: { id: 1, someProperty: "something" } } );
|
||||
});
|
||||
|
||||
it("removes the product from dirtyProducts if the property is clean and by removing an existing property on an id is left", function(){
|
||||
var dirtyProducts = { 1: { id: 1, name: "Product 1" } };
|
||||
removeCleanProperty(dirtyProducts, 1, "name", "Product 1");
|
||||
|
||||
expect(dirtyProducts).toEqual( { } );
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("AdminBulkProductsCtrl", function(){
|
||||
describe("loading data upon initialisation", function(){
|
||||
@@ -376,24 +417,18 @@ describe("AdminBulkProductsCtrl", function(){
|
||||
describe("preparing products for submit",function(){
|
||||
beforeEach(function(){
|
||||
ctrl('AdminBulkProductsCtrl', { $scope: scope } );
|
||||
spyOn(window, "getDirtyObjects").andReturn( [ { id: 1, value: 1 }, { id:2, value: 2 } ] );
|
||||
spyOn(window, "filterSubmitProducts").andReturn( [ { id: 1, value: 3 }, { id:2, value: 4 } ] );
|
||||
spyOn(window, "filterSubmitProducts").andReturn( [ { id: 1, value: 3 }, { id: 2, value: 4 } ] );
|
||||
spyOn(scope, "updateProducts");
|
||||
scope.products = [1,2,3,4,5];
|
||||
scope.cleanProducts = [1,2,3,4,5];
|
||||
scope.dirtyProducts = { 1: { id: 1 }, 2: { id: 2 } };
|
||||
scope.prepareProductsForSubmit();
|
||||
});
|
||||
|
||||
it("fetches dirty products required for submitting", function(){
|
||||
expect(getDirtyObjects).toHaveBeenCalledWith([1,2,3,4,5],[1,2,3,4,5]);
|
||||
});
|
||||
|
||||
it("filters returned dirty products", function(){
|
||||
expect(filterSubmitProducts).toHaveBeenCalledWith( [ { id: 1, value: 1 }, { id:2, value: 2 } ] );
|
||||
expect(filterSubmitProducts).toHaveBeenCalledWith( { 1: { id: 1 }, 2: { id: 2 } } );
|
||||
});
|
||||
|
||||
it("sends dirty and filtered objects to submitProducts()", function(){
|
||||
expect(scope.updateProducts).toHaveBeenCalledWith( [ { id: 1, value: 3 }, { id:2, value: 4 } ] );
|
||||
expect(scope.updateProducts).toHaveBeenCalledWith( [ { id: 1, value: 3 }, { id: 2, value: 4 } ] );
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user