Merge pull request #166 from whiteout-io/dev/WO-629

Dev/wo 629
This commit is contained in:
Tankred Hase 2014-11-10 13:38:31 +01:00
commit b3f1f4b3fe
12 changed files with 389 additions and 145 deletions

View File

@ -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: {

View File

@ -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);
app.controller('DialogCtrl', DialogCtrl);
app.controller('ActionBarCtrl', ActionBarCtrl);

View File

@ -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;

View File

@ -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
};
//

View File

@ -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;

View File

@ -1,18 +1,20 @@
<div class="action-bar">
<div class="action-bar" ng-controller="ActionBarCtrl">
<div class="action-bar__primary">
<button class="btn btn--light">Delete</button>
<button class="btn btn--light">Spam</button>
<button class="btn btn--light" wo-touch="state.read.open ? deleteMessage(state.mailList.selected) : deleteCheckedMessages()">Delete</button>
<button class="btn btn--light" disabled>Spam</button>
<button class="btn btn--light-dropdown" wo-dropdown="#dropdown-folder">
<svg><use xlink:href="#icon-folder" /><title>Folder</title></svg>
<svg class="btn__dropdown" role="presentation"><use xlink:href="#icon-dropdown" /></svg>
</button>
</div>
</div><!--/action-bar__primary-->
<div class="action-bar__secondary">
<button class="btn btn--light-dropdown" wo-dropdown="#dropdown-more">
More
<svg class="btn__dropdown" role="presentation"><use xlink:href="#icon-dropdown" /></svg>
</button>
</div>
</div><!--/action-bar__secondary-->
<div class="action-bar__search">
<div class="search">
<svg><use xlink:href="#icon-search" /><title>Search</title></svg>
@ -20,15 +22,20 @@
ng-change="displaySearchResults(searchText)"
placeholder="Search" focus-me="state.mailList.searching">
</div>
</div>
</div><!--/action-bar__search-->
<!-- dropdowns -->
<ul id="dropdown-folder" class="dropdown">
<li><button><svg><use xlink:href="#icon-folder" /></svg> Lorem</button></li>
<li><button><svg><use xlink:href="#icon-folder" /></svg> Ipsum</button></li>
</ul>
<li ng-repeat="folder in account.folders">
<button wo-touch="state.read.open ? moveMessage(state.mailList.selected, folder) : moveCheckedMessages(folder)">
<svg><use xlink:href="#icon-folder" /></svg>
{{folder.wellknown ? folder.type : folder.name}}
</button>
</li>
</ul><!--/dropdown-->
<ul id="dropdown-more" class="dropdown">
<li><button>Mark as read</button></li>
<li><button>Mark as unread</button></li>
</ul>
<li><button wo-touch="state.read.open ? markMessage(state.mailList.selected, false) : markCheckedMessages(false)">Mark as read</button></li>
<li><button wo-touch="state.read.open ? markMessage(state.mailList.selected, true) : markCheckedMessages(true)">Mark as unread</button></li>
</ul><!--/dropdown-->
</div>

View File

@ -30,9 +30,9 @@
ng-repeat="email in displayMessages">
<ul class="mail-list-entry__flags">
<li class="mail-list-entry__flags-unread"></li>
<li class="mail-list-entry__flags-checked">
<li class="mail-list-entry__flags-checked" wo-touch="$event.stopPropagation()">
<label class="checkbox">
<input type="checkbox">
<input type="checkbox" ng-model="email.checked">
<span><svg role="presentation"><use xlink:href="#icon-check" /></svg></span>
</label>
</li>

View File

@ -7,7 +7,7 @@
<ul class="nav__folders">
<li ng-repeat="folder in account.folders" ng-if="folder.wellknown" ng-hide="folder.type === 'Outbox' && folder.count < 1"
class="nav__folder" ng-class="{'nav__folder--open': state.nav.currentFolder === folder}">
<a href="javascript:;" wo-touch="openFolder(folder)">
<a href="#" wo-touch="$event.preventDefault(); openFolder(folder)">
<svg role="presentation">
<use ng-if="folder.type === 'Inbox'" xlink:href="#icon-inbox" />
<use ng-if="folder.type === 'Sent'" xlink:href="#icon-sent" />
@ -26,9 +26,9 @@
</ul><!--/nav__folders-->
<ul class="nav__folders">
<li ng-repeat="folder in account.folders | orderBy:folder.name" ng-if="!folder.wellknown"
class="nav__folder nav__folder__other" ng-class="{'nav__folder--open': state.nav.currentFolder === folder}">
<a href="javascript:;" wo-touch="openFolder(folder)">
<li ng-repeat="folder in account.folders" ng-if="!folder.wellknown"
class="nav__folder" ng-class="{'nav__folder--open': state.nav.currentFolder === folder}">
<a href="#" wo-touch="$event.preventDefault(); openFolder(folder)">
<svg role="presentation"><use xlink:href="#icon-folder" /></svg>
{{folder.name}}
<span ng-show="folder.count > 0" class="nav__counter">{{folder.count}}</span>
@ -38,32 +38,32 @@
<ul class="nav__secondary">
<li>
<a href="javascript:;" wo-touch="state.account.toggle(true)">
<a href="#" wo-touch="$event.preventDefault(); state.account.toggle(true)">
<svg role="presentation"><use xlink:href="#icon-account" /></svg> Account
</a>
</li>
<li>
<a href="javascript:;" wo-touch="state.contacts.toggle(true)">
<a href="#" wo-touch="$event.preventDefault(); state.contacts.toggle(true)">
<svg role="presentation"><use xlink:href="#icon-contact" /></svg> Contacts
</a>
</li>
<li>
<a href="javascript:;" wo-touch="state.privateKeyUpload.toggle(true)">
<a href="#" wo-touch="$event.preventDefault(); state.privateKeyUpload.toggle(true)">
<svg role="presentation"><use xlink:href="#icon-key" /></svg> Key sync (experimental)
</a>
</li>
<li>
<a href="javascript:;" wo-touch="state.writer.reportBug()">
<a href="#" wo-touch="$event.preventDefault(); state.writer.reportBug()">
<svg role="presentation"><use xlink:href="#icon-bug" /></svg> Report a bug
</a>
</li>
<li>
<a href="javascript:;" wo-touch="state.about.toggle(true)">
<a href="#" wo-touch="$event.preventDefault(); state.about.toggle(true)">
<svg role="presentation"><use xlink:href="#icon-about" /></svg> About
</a>
</li>
<li>
<a href="javascript:;" wo-touch="logout()">
<a href="#" wo-touch="$event.preventDefault(); logout()">
<svg role="presentation"><use xlink:href="#icon-account" /></svg> Logout
</a>
</li>

View File

@ -11,8 +11,9 @@
{{state.nav.currentFolder.wellknown ? state.nav.currentFolder.type : state.nav.currentFolder.name}}
</a>
</div>
</div>
<div class="read__action-toolbar">
</div><!--/read__folder-toolbar-->
<div class="read__action-toolbar" ng-controller="ActionBarCtrl">
<div class="toolbar">
<ul class="toolbar__actions">
<li>
@ -32,7 +33,7 @@
</button>
</li>
<li>
<button wo-touch="state.mailList.remove(state.mailList.selected)" class="btn-icon-light" title="Delete mail">
<button wo-touch="deleteMessage(state.mailList.selected)" class="btn-icon-light" title="Delete mail">
<svg><use xlink:href="#icon-trash" /><title>Delete mail</title></svg>
</button>
</li>
@ -51,13 +52,14 @@
</li>
</ul>
</div>
</div>
</div><!--/read__action-toolbar-->
<header class="read__header">
<div class="read__controls">
<button class="btn-icon-light" wo-touch="state.writer.write(state.mailList.selected)" title="Reply"><svg><use xlink:href="#icon-reply_light" /></svg></button>
<button class="btn-icon-light" wo-touch="state.writer.write(state.mailList.selected, true)" title="Reply All"><svg><use xlink:href="#icon-reply_all_light" /></svg></button>
<button class="btn-icon-light" wo-touch="state.writer.write(state.mailList.selected, null, true)" title="Forward"><svg><use xlink:href="#icon-forward_light" /></svg></button>
</div>
</div><!--/read__controls-->
<h2 class="read__subject" wo-touch="notStripped = !notStripped">
<button ng-hide="notStripped" class="btn-icon-very-light">
@ -114,7 +116,7 @@
{{attachment.filename}}
</li>
</ul>
</header>
</header><!--/read__header-->
<!-- working spinner -->
<div class="read__working"
@ -140,7 +142,7 @@
<iframe sandbox="allow-popups allow-scripts" src="tpl/read-sandbox.html"
frame-load>
</iframe>
</div>
</div><!--/read__body-->
<!-- tooltips -->
<div id="fingerprint-info" class="tooltip">
@ -153,10 +155,15 @@
<li><button wo-touch="state.writer.write(state.mailList.selected)"><svg><use xlink:href="#icon-reply_light" /></svg> Reply</button></li>
<li><button wo-touch="state.writer.write(state.mailList.selected, true)"><svg><use xlink:href="#icon-reply_all_light" /></svg> Reply All</button></li>
<li><button wo-touch="state.writer.write(state.mailList.selected, null, true)"><svg><use xlink:href="#icon-forward_light" /></svg> Forward</button></li>
</ul>
<ul id="read-dropdown-folder" class="dropdown">
<li><button><svg><use xlink:href="#icon-folder" /></svg> Lorem</button></li>
<li><button><svg><use xlink:href="#icon-folder" /></svg> Ipsum</button></li>
</ul>
</ul><!--/dropdown-->
<ul id="read-dropdown-folder" class="dropdown" ng-controller="ActionBarCtrl">
<li ng-repeat="folder in account.folders">
<button wo-touch="moveMessage(state.mailList.selected, folder)">
<svg><use xlink:href="#icon-folder" /></svg>
{{folder.wellknown ? folder.type : folder.name}}
</button>
</li>
</ul><!--/dropdown-->
</div>

View File

@ -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;
});
});
});

View File

@ -29,7 +29,9 @@
if (window.mochaPhantomJS) {
mochaPhantomJS.run();
} else {
mocha.run();
setTimeout(function() {
mocha.run();
}, 1000)
}
</script>
</body>

View File

@ -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;
});
});
});