diff --git a/Gruntfile.js b/Gruntfile.js index 7db0f06..09268ee 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -185,6 +185,7 @@ module.exports = function(grunt) { 'test/unit/navigation-ctrl-test.js', 'test/unit/mail-list-ctrl-test.js', 'test/unit/write-ctrl-test.js', + 'test/unit/action-bar-ctrl-test.js', 'test/unit/outbox-bo-test.js', 'test/unit/invitation-dao-test.js', 'test/unit/update-handler-test.js', @@ -301,6 +302,7 @@ module.exports = function(grunt) { }, options: { mangle: false, + compress: false, sourceMap: true, sourceMapIn: 'test/unit/index.browserified.js.map', sourceMapIncludeSources: true, @@ -319,6 +321,7 @@ module.exports = function(grunt) { }, options: { mangle: false, + compress: false, sourceMap: true, sourceMapIn: 'test/integration/index.browserified.js.map', sourceMapIncludeSources: true, @@ -417,7 +420,7 @@ module.exports = function(grunt) { tasks: ['dist-css', 'manifest'] }, js: { - files: ['src/js/**/*.js'], + files: ['src/js/**/*.js', 'test/unit/*.js', 'test/integration/*.js'], tasks: ['dist-js', 'manifest'] }, icons: { diff --git a/src/js/app.js b/src/js/app.js index 36412fb..46fdd1f 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -31,6 +31,7 @@ var DialogCtrl = require('./controller/dialog'), ReadCtrl = require('./controller/read'), WriteCtrl = require('./controller/write'), NavigationCtrl = require('./controller/navigation'), + ActionBarCtrl = require('./controller/action-bar'), errorUtil = require('./util/error'), backButtonUtil = require('./util/backbutton-handler'); require('./directives/common'); @@ -113,4 +114,5 @@ app.controller('SetPassphraseCtrl', SetPassphraseCtrl); app.controller('PrivateKeyUploadCtrl', PrivateKeyUploadCtrl); app.controller('ContactsCtrl', ContactsCtrl); app.controller('AboutCtrl', AboutCtrl); -app.controller('DialogCtrl', DialogCtrl); \ No newline at end of file +app.controller('DialogCtrl', DialogCtrl); +app.controller('ActionBarCtrl', ActionBarCtrl); \ No newline at end of file diff --git a/src/js/controller/action-bar.js b/src/js/controller/action-bar.js new file mode 100644 index 0000000..d1d6aa3 --- /dev/null +++ b/src/js/controller/action-bar.js @@ -0,0 +1,168 @@ +'use strict'; + +var appController = require('../app-controller'), + emailDao; + +// +// Controller +// + +var ActionBarCtrl = function($scope) { + + emailDao = appController._emailDao; + + /** + * Move a single message from the currently selected folder to another folder + * @param {Object} message The message that is to be moved + * @param {Object} destination The folder object where the message should be moved to + */ + $scope.moveMessage = function(message, destination) { + if (!message) { + return; + } + + // close read state + $scope.state.read.open = false; + + $scope.state.mailList.updateStatus('Moving message...'); + + emailDao.moveMessage({ + folder: currentFolder(), + destination: destination, + message: message + }, function(err) { + if (err) { + // show errors where appropriate + if (err.code === 42) { + $scope.select(message); + $scope.state.mailList.updateStatus('Unable to move message in offline mode!'); + return; + } + $scope.state.mailList.updateStatus('Error during move!'); + $scope.onError(err); + return; + } + $scope.state.mailList.updateStatus('Message moved.'); + $scope.$apply(); + }); + }; + + /** + * Move all checked messages from the currently selected folder to another folder + * @param {Object} destination The folder object where the message should be moved to + */ + $scope.moveCheckedMessages = function(destination) { + getCheckMessages().forEach(function(message) { + $scope.moveMessage(message, destination); + }); + }; + + /** + * Delete a message. This moves the message from the current folder to the trash folder, + * or if the current folder is the trash folder, the message will be purged. + * @param {Object} message The message that is to be deleted + */ + $scope.deleteMessage = function(message) { + if (!message) { + return; + } + + // close read state + $scope.state.read.open = false; + + $scope.state.mailList.updateStatus('Deleting message...'); + + emailDao.deleteMessage({ + folder: currentFolder(), + message: message + }, function(err) { + if (err) { + // show errors where appropriate + if (err.code === 42) { + $scope.select(message); + $scope.state.mailList.updateStatus('Unable to delete message in offline mode!'); + return; + } + $scope.state.mailList.updateStatus('Error during delete!'); + $scope.onError(err); + return; + } + $scope.state.mailList.updateStatus('Message deleted.'); + $scope.$apply(); + }); + }; + + /** + * Delete all of the checked messages. This moves the messages from the current folder to the trash folder, + * or if the current folder is the trash folder, the messages will be purged. + */ + $scope.deleteCheckedMessages = function() { + getCheckMessages().forEach($scope.deleteMessage); + }; + + /** + * Mark a single message as either read or unread + * @param {Object} message The message to be marked + * @param {boolean} unread If the message should be marked as read or unread + */ + $scope.markMessage = function(message, unread) { + if (!message) { + return; + } + + $scope.state.mailList.updateStatus('Updating unread flag...'); + + // close read state + $scope.state.read.open = false; + + var originalState = message.unread; + message.unread = unread; + emailDao.setFlags({ + folder: currentFolder(), + message: message + }, function(err) { + if (err && err.code === 42) { + // offline, restore + message.unread = originalState; + $scope.state.mailList.updateStatus('Unable to mark message in offline mode!'); + return; + } + + if (err) { + $scope.state.mailList.updateStatus('Error on sync!'); + $scope.onError(err); + return; + } + + $scope.state.mailList.updateStatus('Online'); + $scope.$apply(); + }); + }; + + /** + * Mark all of the checked messages as either read or unread. + * @param {boolean} unread If the message should be marked as read or unread + */ + $scope.markCheckedMessages = function(unread) { + getCheckMessages().forEach(function(message) { + $scope.markMessage(message, unread); + }); + }; + + // share local scope functions with root state + $scope.state.actionBar = { + markMessage: $scope.markMessage + }; + + function currentFolder() { + return $scope.state.nav.currentFolder; + } + + function getCheckMessages() { + return currentFolder().messages.filter(function(message) { + return message.checked; + }); + } +}; + +module.exports = ActionBarCtrl; \ No newline at end of file diff --git a/src/js/controller/mail-list.js b/src/js/controller/mail-list.js index a2b14d4..968238c 100644 --- a/src/js/controller/mail-list.js +++ b/src/js/controller/mail-list.js @@ -97,74 +97,13 @@ var MailListCtrl = function($scope, $routeParams) { } } - $scope.toggleUnread(email); - } - }; - - /** - * Mark an email as unread or read, respectively - */ - $scope.toggleUnread = function(message) { - updateStatus('Updating unread flag...'); - - message.unread = !message.unread; - emailDao.setFlags({ - folder: currentFolder(), - message: message - }, function(err) { - if (err && err.code === 42) { - // offline, restore - message.unread = !message.unread; - updateStatus('Unable to mark unread flag in offline mode!'); - return; - } - - if (err) { - updateStatus('Error on sync!'); - $scope.onError(err); - return; - } - - updateStatus('Online'); - $scope.$apply(); - }); - }; - - /** - * Delete a message - */ - $scope.remove = function(message) { - if (!message) { - return; - } - - updateStatus('Deleting message...'); - remove(); - - function remove() { - emailDao.deleteMessage({ - folder: currentFolder(), - message: message - }, function(err) { - if (err) { - // show errors where appropriate - if (err.code === 42) { - $scope.select(message); - updateStatus('Unable to delete message in offline mode!'); - return; - } - updateStatus('Error during delete!'); - $scope.onError(err); - } - updateStatus('Message deleted!'); - $scope.$apply(); - }); + $scope.state.actionBar.markMessage(email, false); } }; // share local scope functions with root state $scope.state.mailList = { - remove: $scope.remove + updateStatus: updateStatus }; // diff --git a/src/sass/blocks/layout/_nav.scss b/src/sass/blocks/layout/_nav.scss index b227cfa..102746c 100755 --- a/src/sass/blocks/layout/_nav.scss +++ b/src/sass/blocks/layout/_nav.scss @@ -132,6 +132,10 @@ } &__folders + .nav__folders { border-top-color: $color-border-light; + + .nav__folder { + margin-bottom: 8px; + } } &__folder { font-size: $font-size-base; @@ -151,9 +155,6 @@ top: 0.25em; } } - &__folder__other { - margin-bottom: 8px; - } &__counter { display: inline; position: static; diff --git a/src/tpl/action-bar.html b/src/tpl/action-bar.html index 20692bc..44f9b5a 100644 --- a/src/tpl/action-bar.html +++ b/src/tpl/action-bar.html @@ -1,18 +1,20 @@ -
+
- - + + -
+
+
-
+
+ + +
  • + +
  • + + +
  • +
  • + diff --git a/src/tpl/mail-list.html b/src/tpl/mail-list.html index c73434d..6611fb3 100644 --- a/src/tpl/mail-list.html +++ b/src/tpl/mail-list.html @@ -30,9 +30,9 @@ ng-repeat="email in displayMessages"> - + + + diff --git a/test/unit/action-bar-ctrl-test.js b/test/unit/action-bar-ctrl-test.js new file mode 100644 index 0000000..8cc414c --- /dev/null +++ b/test/unit/action-bar-ctrl-test.js @@ -0,0 +1,149 @@ +'use strict'; + +var mocks = angular.mock, + EmailDAO = require('../../src/js/dao/email-dao'), + appController = require('../../src/js/app-controller'), + ActionBarCtrl = require('../../src/js/controller/action-bar'); + +describe('Action Bar Controller unit test', function() { + var scope, actionBarCtrl, emailDaoMock, origEmailDao; + + beforeEach(function() { + origEmailDao = appController._emailDao; + emailDaoMock = sinon.createStubInstance(EmailDAO); + appController._emailDao = emailDaoMock; + + angular.module('actionbartest', []); + mocks.module('actionbartest'); + mocks.inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + scope.state = { + mailList: { + updateStatus: function() {} + } + }; + + scope.state.nav = { + currentFolder: { + type: 'Inbox', + path: 'INBOX', + messages: [{ + checked: true + }, { + checked: false + }] + } + }; + + scope.state.read = { + open: true + }; + + actionBarCtrl = $controller(ActionBarCtrl, { + $scope: scope + }); + }); + }); + + afterEach(function() { + // restore the module + appController._emailDao = origEmailDao; + }); + + describe('deleteMessage', function() { + it('should not delete without a selected mail', function() { + scope.deleteMessage(); + }); + + it('should delete the selected mail', function() { + emailDaoMock.deleteMessage.yields(); + + scope.deleteMessage({}); + + expect(emailDaoMock.deleteMessage.calledOnce).to.be.true; + expect(scope.state.read.open).to.be.false; + }); + }); + + describe('deleteCheckedMessages', function() { + var deleteMessageStub; + + beforeEach(function() { + deleteMessageStub = sinon.stub(scope, 'deleteMessage'); + }); + afterEach(function() { + deleteMessageStub.restore(); + }); + + it('should delete the selected mail', function() { + scope.deleteCheckedMessages(); + + expect(deleteMessageStub.calledOnce).to.be.true; + }); + }); + + describe('moveMessage', function() { + it('should not move without a selected mail', function() { + scope.moveMessage(); + }); + + it('should move the selected mail', function() { + emailDaoMock.moveMessage.yields(); + + scope.moveMessage({}, {}); + + expect(emailDaoMock.moveMessage.calledOnce).to.be.true; + expect(scope.state.read.open).to.be.false; + }); + }); + + describe('moveCheckedMessages', function() { + var moveMessageStub; + + beforeEach(function() { + moveMessageStub = sinon.stub(scope, 'moveMessage'); + }); + afterEach(function() { + moveMessageStub.restore(); + }); + + it('should delete the selected mail', function() { + scope.moveCheckedMessages(); + + expect(moveMessageStub.calledOnce).to.be.true; + }); + }); + + describe('markMessage', function() { + it('should not move without a selected mail', function() { + scope.markMessage(); + }); + + it('should move the selected mail', function() { + emailDaoMock.setFlags.yields(); + + scope.markMessage({}, true); + + expect(emailDaoMock.setFlags.calledOnce).to.be.true; + expect(scope.state.read.open).to.be.false; + }); + }); + + describe('markCheckedMessages', function() { + var markMessageStub; + + beforeEach(function() { + markMessageStub = sinon.stub(scope, 'markMessage'); + }); + afterEach(function() { + markMessageStub.restore(); + }); + + it('should delete the selected mail', function() { + scope.markCheckedMessages(); + + expect(markMessageStub.calledOnce).to.be.true; + }); + }); + +}); \ No newline at end of file diff --git a/test/unit/index.html b/test/unit/index.html index 7a85f7c..011f4a2 100644 --- a/test/unit/index.html +++ b/test/unit/index.html @@ -29,7 +29,9 @@ if (window.mochaPhantomJS) { mochaPhantomJS.run(); } else { - mocha.run(); + setTimeout(function() { + mocha.run(); + }, 1000) } diff --git a/test/unit/mail-list-ctrl-test.js b/test/unit/mail-list-ctrl-test.js index bb9e14d..ee915ea 100644 --- a/test/unit/mail-list-ctrl-test.js +++ b/test/unit/mail-list-ctrl-test.js @@ -241,7 +241,6 @@ describe('Mail List controller unit test', function() { describe('scope variables', function() { it('should be set correctly', function() { expect(scope.select).to.exist; - expect(scope.remove).to.exist; expect(scope.state.mailList).to.exist; }); }); @@ -361,6 +360,9 @@ describe('Mail List controller unit test', function() { mailList: {}, read: { toggle: function() {} + }, + actionBar: { + markMessage: function() {} } }; @@ -399,7 +401,9 @@ describe('Mail List controller unit test', function() { } }; - keychainMock.refreshKeyForUserId.withArgs({userId: mail.from[0].address}).yields(); + keychainMock.refreshKeyForUserId.withArgs({ + userId: mail.from[0].address + }).yields(); scope.select(mail); @@ -408,42 +412,4 @@ describe('Mail List controller unit test', function() { expect(scope.state.mailList.selected).to.equal(mail); }); }); - - describe('remove', function() { - it('should not delete without a selected mail', function() { - scope.remove(); - }); - - it('should delete the selected mail', function() { - var uid, mail, currentFolder; - - scope._stopWatchTask(); - - scope.account = {}; - uid = 123; - mail = { - uid: uid, - from: [{ - address: 'asd' - }], - subject: '[whiteout] asdasd', - unread: true - }; - currentFolder = { - type: 'Inbox', - path: 'INBOX', - messages: [mail] - }; - scope.account.folders = [currentFolder]; - scope.state.nav = { - currentFolder: currentFolder - }; - emailDaoMock.deleteMessage.yields(); - - scope.remove(mail); - - expect(emailDaoMock.deleteMessage.calledOnce).to.be.true; - expect(scope.state.mailList.selected).to.exist; - }); - }); }); \ No newline at end of file