diff --git a/README.md b/README.md index e6bc92cf..6d138d7b 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,24 @@ Alternately, if you pass a string, it is compiled just like an Angular template, ''; listView.listActions(template); +* `selectable(boolean)` +Enable the selection column (an initial column made of checkboxes) on the list. + +Once the user selects lines, a button appears and displays the number of selected entries. A click on this button reveals the list of "batch actions", i.e. actions that can be performed on a selection of entries. By default, the only batch action available is a batch delete. + +* `batchActions(String|Array)` +Add you own batch action directives. + +The scope contains a `selection` variable which contains the current selection: + + listView.batchActions(['select', '']) + +*Tip*: This `selection` variable is also in the scope of the main view actions. + +```js +listView.actions('create', ''); +``` + ## Fields A field is the representation of a property of an entity. diff --git a/src/javascripts/ng-admin/Crud/CrudModule.js b/src/javascripts/ng-admin/Crud/CrudModule.js index ee970f3f..29ba0ad6 100644 --- a/src/javascripts/ng-admin/Crud/CrudModule.js +++ b/src/javascripts/ng-admin/Crud/CrudModule.js @@ -24,6 +24,7 @@ define(function (require) { CrudModule.controller('ShowController', require('ng-admin/Crud/show/ShowController')); CrudModule.controller('FormController', require('ng-admin/Crud/form/FormController')); CrudModule.controller('DeleteController', require('ng-admin/Crud/delete/DeleteController')); + CrudModule.controller('BatchDeleteController', require('ng-admin/Crud/delete/BatchDeleteController')); CrudModule.service('PromisesResolver', require('ng-admin/Crud/misc/PromisesResolver')); CrudModule.service('RetrieveQueries', require('ng-admin/Crud/repository/RetrieveQueries')); @@ -52,6 +53,8 @@ define(function (require) { CrudModule.directive('maDatagrid', require('ng-admin/Crud/list/maDatagrid')); CrudModule.directive('maDatagridPagination', require('ng-admin/Crud/list/maDatagridPagination')); CrudModule.directive('maDatagridInfinitePagination', require('ng-admin/Crud/list/maDatagridInfinitePagination')); + CrudModule.directive('maDatagridItemSelector', require('ng-admin/Crud/list/maDatagridItemSelector')); + CrudModule.directive('maDatagridMultiSelector', require('ng-admin/Crud/list/maDatagridMultiSelector')); CrudModule.directive('maFilter', require('ng-admin/Crud/filter/maFilter')); CrudModule.directive('maColumn', require('ng-admin/Crud/column/maColumn')); @@ -73,8 +76,10 @@ define(function (require) { CrudModule.directive('maShowButton', require('ng-admin/Crud/button/maShowButton')); CrudModule.directive('maListButton', require('ng-admin/Crud/button/maListButton')); CrudModule.directive('maDeleteButton', require('ng-admin/Crud/button/maDeleteButton')); + CrudModule.directive('maBatchDeleteButton', require('ng-admin/Crud/button/maBatchDeleteButton')); CrudModule.directive('maViewActions', require('ng-admin/Crud/misc/ViewActions')); + CrudModule.directive('maViewBatchActions', require('ng-admin/Crud/misc/ViewBatchActions')); CrudModule.directive('compile', require('ng-admin/Crud/misc/Compile')); CrudModule.config(require('ng-admin/Crud/routing')); diff --git a/src/javascripts/ng-admin/Crud/button/maBatchDeleteButton.js b/src/javascripts/ng-admin/Crud/button/maBatchDeleteButton.js new file mode 100644 index 00000000..4ae5fc39 --- /dev/null +++ b/src/javascripts/ng-admin/Crud/button/maBatchDeleteButton.js @@ -0,0 +1,33 @@ +/*global define*/ + +define(function () { + 'use strict'; + + function maBatchDeleteButtonDirective($state) { + return { + restrict: 'E', + scope: { + 'entity': '&', + 'selection': '&', + }, + link: function ($scope) { + $scope.gotoBatchDelete = function () { + var entity = $scope.entity(); + var ids = $scope.selection().map(function(entry) { + return entry.identifierValue + }); + $state.go('batchDelete', { ids: ids, entity: entity.name() }); + }; + }, + template: +'' + + ' Delete' + +'' + + }; + } + + maBatchDeleteButtonDirective.$inject = ['$state']; + + return maBatchDeleteButtonDirective; +}); diff --git a/src/javascripts/ng-admin/Crud/delete/BatchDeleteController.js b/src/javascripts/ng-admin/Crud/delete/BatchDeleteController.js new file mode 100644 index 00000000..f0854f64 --- /dev/null +++ b/src/javascripts/ng-admin/Crud/delete/BatchDeleteController.js @@ -0,0 +1,63 @@ +/*global define*/ + +define(function () { + 'use strict'; + + var BatchDeleteController = function ($scope, $state, $stateParams, $filter, $location, $window, DeleteQueries, notification, view) { + this.$scope = $scope; + this.$state = $state; + this.$stateParams = $stateParams; + this.$filter = $filter; + this.$location = $location; + this.$window = $window; + this.DeleteQueries = DeleteQueries; + this.notification = notification; + this.view = view; + this.entity = view.getEntity(); + this.entityIds = $stateParams.ids; + this.selection = []; // fixme: query db to get selection + this.title = view.title(); + this.description = view.description(); + this.actions = view.actions(); + this.loadingPage = false; + this.fields = this.$filter('orderElement')(view.fields()); + + $scope.$on('$destroy', this.destroy.bind(this)); + }; + + BatchDeleteController.prototype.batchDelete = function () { + var notification = this.notification, + $state = this.$state, + entityName = this.entity.name(); + + this.DeleteQueries.batchDelete(this.view, this.entityIds).then(function () { + $state.go($state.get('list'), { 'entity': entityName }); + }, function (response) { + // @TODO: share this method when splitting controllers + var body = response.data; + if (typeof body === 'object') { + body = JSON.stringify(body); + } + + notification.log('Oops, an error occured : (code: ' + response.status + ') ' + body, {addnCls: 'humane-flatty-error'}); + }); + }; + + BatchDeleteController.prototype.back = function () { + this.$window.history.back(); + }; + + BatchDeleteController.prototype.destroy = function () { + this.$scope = undefined; + this.$state = undefined; + this.$stateParams = undefined; + this.$filter = undefined; + this.$location = undefined; + this.$window = undefined; + this.DeleteQueries = undefined; + }; + + BatchDeleteController.$inject = ['$scope', '$state', '$stateParams', '$filter', '$location', '$window', 'DeleteQueries', 'notification', 'view']; + + return BatchDeleteController; +}); diff --git a/src/javascripts/ng-admin/Crud/delete/batchDelete.html b/src/javascripts/ng-admin/Crud/delete/batchDelete.html new file mode 100644 index 00000000..5b2b2859 --- /dev/null +++ b/src/javascripts/ng-admin/Crud/delete/batchDelete.html @@ -0,0 +1,31 @@ +
+
+ + + + + +
+
+ +
+
+

Are you sure ?

+ + +
+
+ +
+
+ + +
+
diff --git a/src/javascripts/ng-admin/Crud/list/Datagrid.html b/src/javascripts/ng-admin/Crud/list/Datagrid.html index ada1b018..38334b6c 100644 --- a/src/javascripts/ng-admin/Crud/list/Datagrid.html +++ b/src/javascripts/ng-admin/Crud/list/Datagrid.html @@ -1,6 +1,9 @@ + + diff --git a/src/javascripts/ng-admin/Crud/list/DatagridController.js b/src/javascripts/ng-admin/Crud/list/DatagridController.js index 94e4c28a..94cdb943 100644 --- a/src/javascripts/ng-admin/Crud/list/DatagridController.js +++ b/src/javascripts/ng-admin/Crud/list/DatagridController.js @@ -18,6 +18,9 @@ define(function () { this.$anchorScroll = $anchorScroll; this.filters = {}; + $scope.toggleSelect = this.toggleSelect.bind(this); + $scope.toggleSelectAll = this.toggleSelectAll.bind(this); + this.$scope.gotoDetail = this.gotoDetail.bind(this); var searchParams = this.$location.search(); @@ -94,6 +97,29 @@ define(function () { return this.$scope.name + '.' + field.name(); }; + DatagridController.prototype.toggleSelect = function (entry) { + var selection = this.$scope.selection.slice(); + + var index = selection.indexOf(entry); + + if (index === -1) { + this.$scope.selection = selection.concat(entry); + return; + } + selection.splice(index, 1); + this.$scope.selection = selection; + }; + + DatagridController.prototype.toggleSelectAll = function () { + + if (this.$scope.selection.length < this.$scope.entries.length) { + this.$scope.selection = this.$scope.entries; + return; + } + + this.$scope.selection = []; + }; + DatagridController.$inject = ['$scope', '$location', '$anchorScroll']; return DatagridController; diff --git a/src/javascripts/ng-admin/Crud/list/ListController.js b/src/javascripts/ng-admin/Crud/list/ListController.js index 5fdad2b7..29f1db76 100644 --- a/src/javascripts/ng-admin/Crud/list/ListController.js +++ b/src/javascripts/ng-admin/Crud/list/ListController.js @@ -16,6 +16,7 @@ define(function () { this.title = view.title(); this.description = view.description(); this.actions = view.actions(); + this.batchActions = view.batchActions(); this.loadingPage = false; this.filters = this.$filter('orderElement')(view.filters()); this.hasFilters = Object.keys(this.filters).length > 0; @@ -27,6 +28,7 @@ define(function () { this.infinitePagination = this.view.infinitePagination(); this.nextPageCallback = this.nextPage.bind(this); this.setPageCallback = this.setPage.bind(this); + this.selection = this.batchActions.length ? [] : null; $scope.$on('$destroy', this.destroy.bind(this)); }; diff --git a/src/javascripts/ng-admin/Crud/list/list.html b/src/javascripts/ng-admin/Crud/list/list.html index cc3ba465..10412f25 100644 --- a/src/javascripts/ng-admin/Crud/list/list.html +++ b/src/javascripts/ng-admin/Crud/list/list.html @@ -1,6 +1,7 @@
- + + @@ -19,6 +20,7 @@

diff --git a/src/javascripts/ng-admin/Crud/list/maDatagrid.js b/src/javascripts/ng-admin/Crud/list/maDatagrid.js index ee86fe0f..9dd2cdf7 100644 --- a/src/javascripts/ng-admin/Crud/list/maDatagrid.js +++ b/src/javascripts/ng-admin/Crud/list/maDatagrid.js @@ -13,6 +13,7 @@ define(function (require) { scope: { name: '@', entries: '=', + selection: '=', fields: '&', listActions: '&', entity: '&' diff --git a/src/javascripts/ng-admin/Crud/list/maDatagridItemSelector.js b/src/javascripts/ng-admin/Crud/list/maDatagridItemSelector.js new file mode 100644 index 00000000..08328361 --- /dev/null +++ b/src/javascripts/ng-admin/Crud/list/maDatagridItemSelector.js @@ -0,0 +1,26 @@ +/*global define*/ + +define(function () { + 'use strict'; + + function DatagridItemSelectorDirective() { + return { + restrict: 'E', + scope: { + entry: '=', + selection: '=', + toggleSelect: '&' + }, + template: '', + link: function (scope) { + scope.toggle = function (entry) { + scope.toggleSelect({entry: entry}); + }; + } + }; + } + + DatagridItemSelectorDirective.$inject = []; + + return DatagridItemSelectorDirective; +}); diff --git a/src/javascripts/ng-admin/Crud/list/maDatagridMultiSelector.js b/src/javascripts/ng-admin/Crud/list/maDatagridMultiSelector.js new file mode 100644 index 00000000..88a9960a --- /dev/null +++ b/src/javascripts/ng-admin/Crud/list/maDatagridMultiSelector.js @@ -0,0 +1,29 @@ +/*global define*/ + +define(function () { + 'use strict'; + + function DatagridMultiSelectorDirective() { + return { + restrict: 'E', + scope: { + entries: '=', + selection: '=', + toggleSelectAll: '&' + }, + template: '', + link: function (scope, element) { + scope.$watch('selection', function (selection) { + element.children()[0].indeterminate = selection.length > 0 && selection.length != scope.entries.length; + }); + scope.$watch('entries', function (entries) { + element.children()[0].indeterminate = scope.selection.length > 0 && scope.selection.length != entries.length; + }); + } + }; + } + + DatagridMultiSelectorDirective.$inject = []; + + return DatagridMultiSelectorDirective; +}); diff --git a/src/javascripts/ng-admin/Crud/misc/ViewActions.js b/src/javascripts/ng-admin/Crud/misc/ViewActions.js index eda0b772..98c1193e 100644 --- a/src/javascripts/ng-admin/Crud/misc/ViewActions.js +++ b/src/javascripts/ng-admin/Crud/misc/ViewActions.js @@ -14,7 +14,9 @@ define(function (require) { scope: { 'override': '&', 'entry': '=', - 'entity': '=' + 'entity': '=', + 'selection': '=', + batchButtons: '&' }, template: viewActionsTemplate, link: function($scope, element, attrs, controller, transcludeFn) { diff --git a/src/javascripts/ng-admin/Crud/misc/ViewBatchActions.js b/src/javascripts/ng-admin/Crud/misc/ViewBatchActions.js new file mode 100644 index 00000000..2ae12443 --- /dev/null +++ b/src/javascripts/ng-admin/Crud/misc/ViewBatchActions.js @@ -0,0 +1,43 @@ +/*global define*/ + +define(function (require) { + 'use strict'; + + var viewActionsTemplate = require('text!./view-batch-actions.html'); + + function ViewBatchActionsDirective($injector) { + var $compile = $injector.get('$compile'); + + return { + restrict: 'E', + transclude: true, + scope: { + 'entity': '=', + 'selection': '=', + 'buttons': '&' + }, + template: viewActionsTemplate, + link: function(scope) { + + scope.isopen = false; + + scope.toggleDropdown = function($event) { + $event.preventDefault(); + $event.stopPropagation(); + scope.isopen = !scope.isopen; + }; + + scope.buttons = scope.buttons(); + if (typeof scope.buttons === 'string') { + scope.customTemplate = scope.buttons; + scope.buttons = null; + } + + } + }; + } + + ViewBatchActionsDirective.$inject = ['$injector']; + + return ViewBatchActionsDirective; +}); diff --git a/src/javascripts/ng-admin/Crud/misc/view-batch-actions.html b/src/javascripts/ng-admin/Crud/misc/view-batch-actions.html new file mode 100644 index 00000000..98255462 --- /dev/null +++ b/src/javascripts/ng-admin/Crud/misc/view-batch-actions.html @@ -0,0 +1,15 @@ + + + + diff --git a/src/javascripts/ng-admin/Crud/repository/DeleteQueries.js b/src/javascripts/ng-admin/Crud/repository/DeleteQueries.js index ca3f34b3..8b0446b3 100644 --- a/src/javascripts/ng-admin/Crud/repository/DeleteQueries.js +++ b/src/javascripts/ng-admin/Crud/repository/DeleteQueries.js @@ -29,6 +29,24 @@ define(function (require) { .customDELETE(); }; + /** + * Delete a batch of entity + * Delete the data to the API + * + * @param {String} view the formView related to the entity + * @param {*} entityIds the entities ids + * + * @returns {promise} + */ + DeleteQueries.prototype.batchDelete = function (view, entityIds) { + var self = this; + var promises = entityIds.map(function (id) { + return self.deleteOne(view, id); + }); + + return this.$q.all(promises); + }; + DeleteQueries.$inject = ['$q', 'Restangular', 'NgAdminConfiguration', 'PromisesResolver']; return DeleteQueries; diff --git a/src/javascripts/ng-admin/Crud/routing.js b/src/javascripts/ng-admin/Crud/routing.js index 0f5a3816..a1fb5769 100644 --- a/src/javascripts/ng-admin/Crud/routing.js +++ b/src/javascripts/ng-admin/Crud/routing.js @@ -7,7 +7,8 @@ define(function (require) { showTemplate = require('text!./show/show.html'), createTemplate = require('text!./form/create.html'), editTemplate = require('text!./form/edit.html'), - deleteTemplate = require('text!./delete/delete.html'); + deleteTemplate = require('text!./delete/delete.html'), + batchDeleteTemplate = require('text!./delete/batchDelete.html'); function templateProvider(viewName, defaultView) { return ['$stateParams', 'NgAdminConfiguration', function ($stateParams, Configuration) { @@ -169,6 +170,25 @@ define(function (require) { }] } }); + + $stateProvider + .state('batchDelete', { + parent: 'main', + url: '/batch-delete/:entity/{ids:json}', + controller: 'BatchDeleteController', + controllerAs: 'batchDeleteController', + templateProvider: templateProvider('BatchDeleteView', batchDeleteTemplate), + params: { + entity: {}, + ids: [], + }, + resolve: { + view: viewProvider('BatchDeleteView'), + params: ['$stateParams', function ($stateParams) { + return $stateParams; + }] + } + }); } routing.$inject = ['$stateProvider']; diff --git a/src/javascripts/ng-admin/es6/lib/Entity/Entity.js b/src/javascripts/ng-admin/es6/lib/Entity/Entity.js index 49664667..391a30d9 100644 --- a/src/javascripts/ng-admin/es6/lib/Entity/Entity.js +++ b/src/javascripts/ng-admin/es6/lib/Entity/Entity.js @@ -7,6 +7,7 @@ import CreateView from '../View/CreateView'; import EditView from '../View/EditView'; import DeleteView from '../View/DeleteView'; import ShowView from '../View/ShowView'; +import BatchDeleteView from '../View/BatchDeleteView'; class Entity { constructor(name) { @@ -90,6 +91,13 @@ class Entity { return this._views["DeleteView"]; } + /** + * @deprecated Use .views["BatchDeleteView"] instead + */ + batchDeleteView() { + return this._views["BatchDeleteView"]; + } + /** * @deprecated Use .views["ShowView"] instead */ @@ -111,6 +119,7 @@ class Entity { "CreateView": new CreateView().setEntity(this), "EditView": new EditView().setEntity(this), "DeleteView": new DeleteView().setEntity(this), + "BatchDeleteView": new BatchDeleteView().setEntity(this), "ShowView": new ShowView().setEntity(this) }; } @@ -127,6 +136,7 @@ class Entity { this._views["CreateView"].disable(); this._views["EditView"].disable(); this._views["DeleteView"].disable(); + this._views["BatchDeleteView"].disable(); return this; } diff --git a/src/javascripts/ng-admin/es6/lib/View/BatchDeleteView.js b/src/javascripts/ng-admin/es6/lib/View/BatchDeleteView.js new file mode 100644 index 00000000..d9842554 --- /dev/null +++ b/src/javascripts/ng-admin/es6/lib/View/BatchDeleteView.js @@ -0,0 +1,11 @@ +import View from './View'; + +class BatchDeleteView extends View { + constructor(name) { + super(name); + + this._type = 'BatchDeleteView'; + } +} + +export default BatchDeleteView; diff --git a/src/javascripts/ng-admin/es6/lib/View/ListView.js b/src/javascripts/ng-admin/es6/lib/View/ListView.js index 1046c5d6..ef8a1242 100644 --- a/src/javascripts/ng-admin/es6/lib/View/ListView.js +++ b/src/javascripts/ng-admin/es6/lib/View/ListView.js @@ -8,6 +8,7 @@ class ListView extends View { this._perPage = 30; this._infinitePagination = false; this._listActions = []; + this._batchActions = ['delete']; this._filters = []; this._sortField = 'id'; @@ -15,14 +16,14 @@ class ListView extends View { } perPage() { - if (!arguments.length) return this._perPage; + if (!arguments.length) { return this._perPage; } this._perPage = arguments[0]; return this; } /** @deprecated Use perPage instead */ limit() { - if (!arguments.length) return this.perPage(); + if (!arguments.length) { return this.perPage(); } return this.perPage(arguments[0]); } @@ -63,6 +64,16 @@ class ListView extends View { return this; } + batchActions(actions) { + if (!arguments.length) { + return this._batchActions; + } + + this._batchActions = actions; + + return this; + } + filters(filters) { if (!arguments.length) { return this._filters; diff --git a/src/javascripts/ng-admin/es6/tests/lib/Entity/EntityTest.js b/src/javascripts/ng-admin/es6/tests/lib/Entity/EntityTest.js index d1fcc008..ddbd56a9 100644 --- a/src/javascripts/ng-admin/es6/tests/lib/Entity/EntityTest.js +++ b/src/javascripts/ng-admin/es6/tests/lib/Entity/EntityTest.js @@ -13,6 +13,7 @@ describe('Entity', function() { 'CreateView', 'EditView', 'DeleteView', + 'BatchDeleteView', 'ShowView' ], Object.keys(entity.views)); }); diff --git a/src/javascripts/test/e2e/filterViewSpec.js b/src/javascripts/test/e2e/filterViewSpec.js index a6f610c7..74ca050f 100644 --- a/src/javascripts/test/e2e/filterViewSpec.js +++ b/src/javascripts/test/e2e/filterViewSpec.js @@ -19,7 +19,7 @@ describe('Global filter', function () { // Filter globally for 'rabbit' $$('.filters .filter:nth-child(1) input').sendKeys('rabbit'); $$('.filters button[type="submit"]').click(); - $$('.grid tr td:nth-child(3)').then(function (tdElements) { + $$('.grid tr td:nth-child(4)').then(function (tdElements) { expect(tdElements.length).toBe(1); expect(tdElements[0].getText()).toBe('White Rabbit: it was indeed: she was out of the gr...'); }); @@ -55,7 +55,7 @@ describe('Global filter', function () { // Filter on post_id '3' $$('.filters .filter select option[value="3"]').click(); $$('.filters button[type="submit"]').click(); - $$('.grid tr td:nth-child(3)').then(function (tdElements) { + $$('.grid tr td:nth-child(4)').then(function (tdElements) { expect(tdElements.length).toBe(2); expect(tdElements[0].getText()).toBe('I\'d been the whiting,\' said the Hatter, it woke up...'); expect(tdElements[1].getText()).toBe('I\'m not Ada,\' she said, \'and see whether it\'s mark...'); diff --git a/src/javascripts/test/unit/Crud/list/DatagridControllerSpec.js b/src/javascripts/test/unit/Crud/list/DatagridControllerSpec.js new file mode 100644 index 00000000..93859154 --- /dev/null +++ b/src/javascripts/test/unit/Crud/list/DatagridControllerSpec.js @@ -0,0 +1,67 @@ +/*global define,describe,it,expect,beforeEach*/ + +define(function (require) { + 'use strict'; + + describe('controller: ma-datagrid', function () { + var DataGridController = require('ng-admin/Crud/list/DatagridController'), + Entity = require('ng-admin/es6/lib/Entity/Entity'), + Entry = require('ng-admin/es6/lib/Entry'); + var dataGridController, entries; + + beforeEach(function () { + entries = [ + new Entry('my_entity', {value: 1}, 1), + new Entry('my_entity', {value: 2}, 2), + new Entry('my_entity', {value: 3}, 3), + ]; + + dataGridController = new DataGridController({ + entity: function () { + return new Entity('my_entity'); + }, + entries: entries, + selection: [] + }, { + search: function () { + return {}; + } + }); + }); + + describe('toggleSelect', function () { + + it('should add entry in selection if it was not in it', function () { + dataGridController.toggleSelect(entries[0]); + expect(dataGridController.$scope.selection).toEqual([entries[0]]); + }); + + it('should remove entry from selection if it was in it', function () { + dataGridController.$scope.selection = entries; + dataGridController.toggleSelect(entries[0]); + expect(dataGridController.$scope.selection).toEqual([entries[1], entries[2]]); + }); + + }); + + describe('toggleSelectAll', function () { + it('should empty selection if it was full', function () { + dataGridController.$scope.selection = entries; + dataGridController.toggleSelectAll(); + expect(dataGridController.$scope.selection).toEqual([]); + }); + + it('should add all entries if selection was empty', function () { + dataGridController.$scope.selection = [entries]; + dataGridController.toggleSelectAll(); + expect(dataGridController.$scope.selection).toEqual(entries); + }); + + it('should select all entries if selection was incomplete', function () { + dataGridController.$scope.selection = [entries[0]]; + dataGridController.toggleSelectAll(); + expect(dataGridController.$scope.selection).toEqual(entries); + }); + }); + }); +}); diff --git a/src/javascripts/test/unit/Crud/list/maDatagridItemSelectorSpec.js b/src/javascripts/test/unit/Crud/list/maDatagridItemSelectorSpec.js new file mode 100644 index 00000000..1ab2edf8 --- /dev/null +++ b/src/javascripts/test/unit/Crud/list/maDatagridItemSelectorSpec.js @@ -0,0 +1,64 @@ +/*global define,angular,inject,describe,it,expect,beforeEach,module*/ + +define(function (require) { + 'use strict'; + + describe('directive: ma-datagrid-item-selector', function () { + var directive = require('ng-admin/Crud/list/maDatagridItemSelector'), + Entry = require('ng-admin/es6/lib/Entry'), + $compile, + scope, + directiveUsage = '' + ; + + angular.module('testapp_DatagridItemSelector', []) + .directive('maDatagridItemSelector', directive); + require('angular-mocks'); + + beforeEach(module('testapp_DatagridItemSelector')); + + beforeEach(inject(function (_$compile_, _$rootScope_) { + $compile = _$compile_; + scope = _$rootScope_; + scope.entry = new Entry('entity', {some: 'values'}, 'entity_1'); + scope.selection = [new Entry('entity', {some: 'values'}, 'entity_2'), new Entry('entity', {some: 'values'}, 'entity_3')]; + })); + + it('should be unchecked if entry is not in selection', function () { + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].checked).toBe(false); + }); + + it('should be checked if entry is in selection', function () { + scope.selection.push(scope.entry); + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].checked).toBe(true); + }); + + it('should become checked if entry is removed from selection', function () { + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].checked).toBe(false); + scope.selection = scope.selection.concat(scope.entry); + scope.$digest(); + expect(element.children()[0].checked).toBe(true); + }); + + it('should become unchecked if entry is added to selection', function () { + scope.selection = scope.selection.concat(scope.entry); + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].checked).toBe(true); + scope.selection = []; + scope.$digest(); + expect(element.children()[0].checked).toBe(false); + }); + + }); +}); diff --git a/src/javascripts/test/unit/Crud/list/maDatagridMultiSelectorSpec.js b/src/javascripts/test/unit/Crud/list/maDatagridMultiSelectorSpec.js new file mode 100644 index 00000000..3103b3da --- /dev/null +++ b/src/javascripts/test/unit/Crud/list/maDatagridMultiSelectorSpec.js @@ -0,0 +1,133 @@ +/*global define,angular,inject,describe,it,expect,beforeEach,module*/ + +define(function (require) { + 'use strict'; + + describe('directive: ma-datagrid-multi-selector', function () { + var directive = require('ng-admin/Crud/list/maDatagridMultiSelector'), + Entry = require('ng-admin/es6/lib/Entry'), + $compile, + scope, + directiveUsage = '' + ; + + angular.module('testapp_DatagridMultiSelector', []) + .directive('maDatagridMultiSelector', directive); + require('angular-mocks'); + + beforeEach(module('testapp_DatagridMultiSelector')); + + beforeEach(inject(function (_$compile_, _$rootScope_) { + $compile = _$compile_; + scope = _$rootScope_; + scope.entries = [new Entry('entity', {some: 'values'}, 'entity_1'), new Entry('entity', {some: 'values'}, 'entity_2'), new Entry('entity', {some: 'values'}, 'entity_3')]; + scope.selection = [new Entry('entity', {some: 'values'}, 'entity_2'), new Entry('entity', {some: 'values'}, 'entity_3')]; + })); + + it('checkbox should be indeterminate if entries does not correspond to selection', function () { + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(true); + expect(element.children()[0].checked).toBe(false); + }); + + it('checkbox should be true if entries correspond to selection', function () { + scope.selection = scope.entries; + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(false); + expect(element.children()[0].checked).toBe(true); + }); + + it('checkbox should be true if entries correspond to selection even if order differ', function () { + scope.selection = [scope.entries[2], scope.entries[1], scope.entries[0]]; + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(false); + expect(element.children()[0].checked).toBe(true); + }); + + it('checkbox should be false if selection is empty', function () { + scope.selection = []; + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(false); + expect(element.children()[0].checked).toBe(false); + }); + + it('checkbox should become indeterminate once selection stop corresponding to entries', function () { + scope.selection = scope.entries; + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(false); + expect(element.children()[0].checked).toBe(true); + + scope.selection = [scope.selection.pop()]; + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(true); + expect(element.children()[0].checked).toBe(false); + }); + + it('checkbox should become indeterminate once entries stop corresponding to selection', function () { + scope.selection = scope.entries; + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(false); + expect(element.children()[0].checked).toBe(true); + + scope.entries = scope.entries.concat(new Entry('entity', {some: 'value'}, 'entity_4')); + + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(true); + expect(element.children()[0].checked).toBe(false); + }); + + + it('checkbox should become false once selection become empty', function () { + scope.selection = scope.entries; + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(false); + expect(element.children()[0].checked).toBe(true); + + scope.selection = []; + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(false); + expect(element.children()[0].checked).toBe(false); + }); + + it('checkbox should become true once entries correspond to selection', function () { + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(true); + expect(element.children()[0].checked).toBe(false); + + scope.selection = scope.entries; + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(false); + expect(element.children()[0].checked).toBe(true); + }); + + it('checkbox should be false if selection is empty', function () { + scope.selection = []; + var element = $compile(directiveUsage)(scope); + scope.$digest(); + + expect(element.children()[0].indeterminate).toBe(false); + expect(element.children()[0].checked).toBe(false); + }); + + }); +}); diff --git a/src/javascripts/test/unit/Crud/repository/DeleteQueriesSpec.js b/src/javascripts/test/unit/Crud/repository/DeleteQueriesSpec.js index 0cc8b209..45672598 100644 --- a/src/javascripts/test/unit/Crud/repository/DeleteQueriesSpec.js +++ b/src/javascripts/test/unit/Crud/repository/DeleteQueriesSpec.js @@ -48,5 +48,22 @@ define(function (require) { .then(done, done.fail); }); }); + + describe("batchDelete", function () { + it('should DELETE entities when calling batchEntities', function () { + var deleteQueries = new DeleteQueries({all: function (promises) { + return promises; + }}, Restangular, config); + spyOn(Restangular, 'oneUrl').and.callThrough(); + spyOn(Restangular, 'customDELETE').and.callThrough(); + + var promises = deleteQueries.batchDelete(view, [1, 2]); + expect(promises.length).toBe(2); + expect(Restangular.oneUrl).toHaveBeenCalledWith('cat', 'http://localhost/cat/1'); + expect(Restangular.oneUrl).toHaveBeenCalledWith('cat', 'http://localhost/cat/2'); + expect(Restangular.customDELETE).toHaveBeenCalledWith(); + expect(Restangular.customDELETE).toHaveBeenCalledWith(); + }); + }); }); }); diff --git a/src/sass/ng-admin.scss b/src/sass/ng-admin.scss index 5648609b..01265903 100644 --- a/src/sass/ng-admin.scss +++ b/src/sass/ng-admin.scss @@ -227,4 +227,3 @@ div.bottom-loader { border-radius: 4px; } } -

+ + @@ -16,6 +19,9 @@
+ +