(js) Drag'n'drop of cards in AddressBook module

Required to add the option to move multiple cards to another
addressbook. Fixed the possibility to copy cards to a subscribed
calendar.
pull/27/merge
Francis Lachapelle 2016-08-05 16:08:04 -04:00
parent e963981460
commit 907024d2c4
7 changed files with 264 additions and 51 deletions

1
NEWS
View File

@ -3,6 +3,7 @@
New features
- [web] drag'n'drop of messages in the Mail module (#3497, #3586, #3734, #3788)
- [web] drag'n'drop of cards in the AddressBook module
- [eas] added folder merging capabilities
Enhancements

View File

@ -41,20 +41,28 @@
"Move To" = "Move To";
"Copy To" = "Copy To";
"Add to" = "Add to";
/* Subheader of empty addressbook */
"No contact" = "No contact";
/* Subheader of system addressbook */
"Start a search to browse this address book" = "Start a search to browse this address book";
/* Number of contacts in addressbook; string is prefixed by number */
"contacts" = "contacts";
/* No contact matching search criteria */
"No matching contact" = "No matching contact";
/* Number of contacts matching search criteria; string is prefixed by number */
"matching contacts" = "matching contacts";
/* Number of selected contacts in list */
"selected" = "selected";
/* Empty right pane */
"No contact selected" = "No contact selected";
/* Tooltips */
"Create a new address book card" = "Create a new address book card";
"Create a new list" = "Create a new list";
@ -95,9 +103,11 @@
"Work" = "Work";
"Mobile" = "Mobile";
"Pager" = "Pager";
/* categories */
"contacts_category_labels" = "Colleague, Competitor, Customer, Friend, Family, Business Partner, Provider, Press, VIP";
"New category" = "New category";
/* adresses */
"Title" = "Title";
"Service" = "Service";
@ -141,6 +151,7 @@
= "You cannot subscribe to a folder that you own.";
"Unable to subscribe to that folder!"
= "Unable to subscribe to that folder.";
/* acls */
"Access rights to" = "Access rights to";
"For user" = "For user";
@ -159,11 +170,15 @@
"The selected contact has no email address."
= "The selected contact has no email address.";
"Please select a contact." = "Please select a contact.";
/* Error messages for move and copy */
/* Messages for move and copy */
"%{0} card(s) copied" = "%{0} card(s) copied";
"%{0} card(s) moved" = "%{0} card(s) moved";
"SoAccessDeniedException" = "You cannot write to this address book.";
"Forbidden" = "You cannot write to this address book.";
"Invalid Contact" = "The selected contact no longer exists.";
"Unknown Destination Folder" = "The chosen destination address book no longer exists.";
/* Lists */
"List details" = "List details";
"List name" = "List name";
@ -176,6 +191,8 @@
"Export" = "Export";
"Export Address Book..." = "Export Address Book...";
"View Raw Source" = "View Raw Source";
/* Import */
"Import Cards" = "Import Cards";
"Select a vCard or LDIF file." = "Select a vCard or LDIF file.";
"Upload" = "Upload";
@ -185,6 +202,7 @@
"No card was imported." = "No card was imported.";
"A total of %{0} cards were imported in the addressbook." = "A total of %{0} cards were imported in the addressbook.";
"Reload" = "Reload";
/* Properties window */
"Address Book Name" = "Address Book Name";
"Links to this Address Book" = "Links to this Address Book";

View File

@ -19,7 +19,11 @@
ui-view="addressbooks"
ng-controller="navController"><!-- addressbooks list --></main>
<!-- TEMPLATE SCRIPT WRAPPER -->
<sg-draggable-helper>
<md-icon>person</md-icon>
<sg-draggable-helper-counter class="md-default-theme md-warn md-hue-1 md-bg"><!-- count --></sg-draggable-helper-counter>
</sg-draggable-helper>
<script type="text/ng-template" id="UIxContactFoldersView">
<!-- Sidenav -->
@ -47,7 +51,9 @@
ng-click="app.select($event, folder)"
ng-dblclick="app.edit(folder)"
ui-sref="app.addressbook({addressbookId: folder.id})"
ui-sref-active="md-default-theme md-background md-bg md-hue-1">
ui-sref-active="md-default-theme md-background md-bg md-hue-1"
sg-droppable="app.isDroppableFolder(dragFolder, folder)"
sg-drop="app.dragSelectedCards(dragFolder, folder, dragMode)">
<md-icon>contacts</md-icon>
<p class="sg-item-name"
ng-show="app.editMode!=folder.id">
@ -133,7 +139,9 @@
ng-click="app.select($event, folder)"
ng-dblclick="app.edit(folder)"
ui-sref="app.addressbook({addressbookId: folder.id})"
ui-sref-active="md-default-theme md-background md-bg md-hue-1">
ui-sref-active="md-default-theme md-background md-bg md-hue-1"
sg-droppable="app.isDroppableFolder(dragFolder, folder)"
sg-drop="app.dragSelectedCards(dragFolder, folder, dragMode)">
<md-icon>contacts</md-icon>
<p class="sg-item-name"
ng-show="app.editMode!=folder.id">
@ -333,29 +341,11 @@
ng-click="addressbook.confirmDeleteSelectedCards()">
<md-icon>delete</md-icon>
</md-button>
<md-menu>
<md-button class="sg-icon-button" label:aria-label="Copy contacts" ng-click="$mdOpenMenu()">
<md-tooltip md-direction="left"><var:string label:value="Copy To"/></md-tooltip>
<md-icon>content_copy</md-icon>
</md-button>
<md-menu-content width="4">
<md-menu-item>
<md-button class="md-primary" ng-disabled="true"><var:string label:value="Copy To"/></md-button>
</md-menu-item>
<md-menu-divider><!-- divider --></md-menu-divider>
<md-menu-item ng-repeat="folder in app.service.$addressbooks track by folder.id"
ng-hide="addressbook.selectedFolder.id == folder.id">
<md-button ng-click="addressbook.copySelectedCards(folder.id)">
<span ng-class="'sg-child-level-' + folder.level">{{folder.name}}</span>
</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
<md-menu>
<md-button class="sg-icon-button" label:aria-label="More messages options" ng-click="$mdOpenMenu()">
<md-icon>more_vert</md-icon>
</md-button>
<md-menu-content width="2">
<md-menu-content width="3">
<md-menu-item>
<md-button ng-click="addressbook.newMessageWithSelectedCards($event)">
<var:string label:value="Write"/>
@ -366,6 +356,42 @@
<var:string label:value="Export"/>
</md-button>
</md-menu-item>
<md-menu-item>
<md-menu>
<md-button label:aria-label="Copy To" ng-click="$mdOpenMenu($event)">
<var:string label:value="Copy To"/>
</md-button>
<md-menu-content class="md-dense" width="4">
<md-menu-item ng-repeat="folder in app.service.$addressbooks track by folder.id"
ng-hide="addressbook.selectedFolder.id == folder.id">
<md-button ng-click="addressbook.copySelectedCards(folder.id)">{{folder.name}}</md-button>
</md-menu-item>
<md-menu-item ng-repeat="folder in app.service.$subscriptions track by folder.id"
ng-hide="addressbook.selectedFolder.id == folder.id">
<md-button ng-click="addressbook.copySelectedCards(folder.id)"
ng-disabled="!folder.acls.objectCreator">{{folder.name}}</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
</md-menu-item>
<md-menu-item ng-show="addressbook.selectedFolder.acls.objectEraser">
<md-menu>
<md-button label:aria-label="Move To" ng-click="$mdOpenMenu($event)">
<var:string label:value="Move To"/>
</md-button>
<md-menu-content class="md-dense" width="4">
<md-menu-item ng-repeat="folder in app.service.$addressbooks track by folder.id"
ng-hide="addressbook.selectedFolder.id == folder.id">
<md-button ng-click="addressbook.moveSelectedCards(folder.id)">{{folder.name}}</md-button>
</md-menu-item>
<md-menu-item ng-repeat="folder in app.service.$subscriptions track by folder.id"
ng-hide="addressbook.selectedFolder.id == folder.id">
<md-button ng-click="addressbook.moveSelectedCards(folder.id)"
ng-disabled="!folder.acls.objectCreator">{{folder.name}}</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
</md-menu-item>
</md-menu-content>
</md-menu>
</div>
@ -395,7 +421,11 @@
</md-subheader>
<md-virtual-repeat-container class="md-flex" md-top-index="addressbook.selectedFolder.$topIndex">
<md-list class="sg-section-list"
ng-class="{ 'sg-list-selectable': addressbook.mode.multiple }">
ng-class="{ 'sg-list-selectable': addressbook.mode.multiple }"
sg-draggable="addressbook.selectedFolder"
sg-drag-start="addressbook.selectedFolder.hasSelectedCard() ||
addressbook.selectedFolder.$selectedCount()"
sg-drag-count="addressbook.selectedFolder.$selectedCount()">
<md-list-item
class="md-default-theme md-accent md-hue-2"
ng-class="{'md-bg': addressbook.selectedFolder.isSelectedCard(currentCard.id)}"

View File

@ -337,6 +337,16 @@
return loaded;
};
/**
* @function hasSelectedMessage
* @memberof AddressBook.prototype
* @desc Check if a card is selected.
* @returns true if the a card is selected
*/
AddressBook.prototype.hasSelectedCard = function() {
return angular.isDefined(this.selectedCard);
};
/**
* @function isSelectedCard
* @memberof AddressBook.prototype
@ -345,7 +355,29 @@
* @returns true if the specified card is selected
*/
AddressBook.prototype.isSelectedCard = function(cardId) {
return this.selectedCard == cardId;
return this.hasSelectedCard() && this.selectedCard == cardId;
};
/**
* @function $selectedCard
* @memberof AddressBook.prototype
* @desc Return the currently visible card.
* @returns a Card instance or undefined if no card is displayed
*/
AddressBook.prototype.$selectedCard = function() {
var _this = this;
return _.find(this.$cards, function(card) { return card.id == _this.selectedCard; });
};
/**
* @function $selectedCards
* @memberof AddressBook.prototype
* @desc Return the cards selected by the user.
* @returns Card instances
*/
AddressBook.prototype.$selectedCards = function() {
return _.filter(this.$cards, function(card) { return card.selected; });
};
/**
@ -553,6 +585,33 @@
return d.promise;
};
/**
* @function $_deleteCards
* @memberof AddressBook.prototype
* @desc Delete multiple cards from AddressBook object.
* @param {string[]} ids - the cards ids
*/
AddressBook.prototype.$_deleteCards = function(ids) {
var _this = this;
// Remove cards from $cards and idsMap
_.forEachRight(this.$cards, function(card, index) {
var selectedIndex = _.findIndex(ids, function(id) {
return card.id == id;
});
if (selectedIndex > -1) {
ids.splice(selectedIndex, 1);
delete _this.idsMap[card.id];
if (_this.isSelectedCard(card.id))
delete _this.selectedCard;
_this.$cards.splice(index, 1);
}
else {
_this.idsMap[card.id] -= ids.length;
}
});
};
/**
* @function $deleteCards
* @memberof AddressBook.prototype
@ -561,25 +620,10 @@
*/
AddressBook.prototype.$deleteCards = function(cards) {
var _this = this,
ids = _.map(cards, function(card) { return card.id; });
ids = _.map(cards, 'id');
return AddressBook.$$resource.post(this.id, 'batchDelete', {uids: ids}).then(function() {
// Remove cards from $cards and idsMap
_.forEachRight(_this.$cards, function(card, index) {
var selectedIndex = _.findIndex(ids, function(id) {
return card.id == id;
});
if (selectedIndex > -1) {
ids.splice(selectedIndex, 1);
delete _this.idsMap[card.id];
if (_this.isSelectedCard(card.id))
delete _this.selectedCard;
_this.$cards.splice(index, 1);
}
else {
_this.idsMap[card.id] -= ids.length;
}
});
_this.$_deleteCards(ids);
});
};
@ -590,10 +634,28 @@
* @return a promise of the HTTP operation
*/
AddressBook.prototype.$copyCards = function(cards, folder) {
var uids = _.map(cards, function(card) { return card.id; });
var uids = _.map(cards, 'id');
return AddressBook.$$resource.post(this.id, 'copy', {uids: uids, folder: folder});
};
/**
* @function $moveCards
* @memberof AddressBook.prototype
* @desc Move multiple cards from the current addressbook to a target one
* @param {object[]} cards - instances of Card object
* @param {string} folder - the destination folder id
* @return a promise of the HTTP operation
*/
AddressBook.prototype.$moveCards = function(cards, folder) {
var _this = this, uids;
uids = _.map(cards, 'id');
return AddressBook.$$resource.post(this.id, 'move', {uids: uids, folder: folder})
.then(function() {
return _this.$_deleteCards(uids);
});
};
/**
* @function $save
* @memberof AddressBook.prototype

View File

@ -6,8 +6,8 @@
/**
* @ngInject
*/
AddressBookController.$inject = ['$scope', '$q', '$window', '$state', '$timeout', '$mdDialog', 'Account', 'Card', 'AddressBook', 'Dialog', 'sgSettings', 'stateAddressbooks', 'stateAddressbook'];
function AddressBookController($scope, $q, $window, $state, $timeout, $mdDialog, Account, Card, AddressBook, Dialog, Settings, stateAddressbooks, stateAddressbook) {
AddressBookController.$inject = ['$scope', '$q', '$window', '$state', '$timeout', '$mdDialog', '$mdToast', 'Account', 'Card', 'AddressBook', 'Dialog', 'sgSettings', 'stateAddressbooks', 'stateAddressbook'];
function AddressBookController($scope, $q, $window, $state, $timeout, $mdDialog, $mdToast, Account, Card, AddressBook, Dialog, Settings, stateAddressbooks, stateAddressbook) {
var vm = this;
AddressBook.selectedFolder = stateAddressbook;
@ -20,6 +20,7 @@
vm.unselectCards = unselectCards;
vm.confirmDeleteSelectedCards = confirmDeleteSelectedCards;
vm.copySelectedCards = copySelectedCards;
vm.moveSelectedCards = moveSelectedCards;
vm.selectAll = selectAll;
vm.sort = sort;
vm.sortedBy = sortedBy;
@ -66,11 +67,58 @@
});
}
function copySelectedCards(folder) {
var selectedCards = _.filter(vm.selectedFolder.$cards, function(card) { return card.selected; });
vm.selectedFolder.$copyCards(selectedCards, folder).then(function() {
// TODO: refresh target addressbook?
/**
* @see AddressBooksController.dragSelectedCards
*/
function _selectedCardsOperation(operation, dstId) {
var srcFolder, allCards, cards, ids, clearCardView, promise, success;
srcFolder = vm.selectedFolder;
clearCardView = false;
allCards = srcFolder.$selectedCards();
cards = _.filter(allCards, function(card) {
return card.$isCard();
});
if (cards.length != allCards.length)
$mdToast.show(
$mdToast.simple()
.content(l("Lists can't be moved or copied."))
.position('top right')
.hideDelay(2000));
if (cards.length) {
if (operation == 'copy') {
promise = srcFolder.$copyCards(cards, dstId);
success = l('%{0} card(s) copied', cards.length);
}
else {
promise = srcFolder.$moveCards(cards, dstId);
success = l('%{0} card(s) moved', cards.length);
// Check if currently displayed card will be moved
ids = _.map(cards, 'id');
clearCardView = (srcFolder.selectedCard && ids.indexOf(srcFolder.selectedCard) >= 0);
}
// Show success toast when action succeeds
promise.then(function() {
if (clearCardView)
$state.go('app.addressbook');
$mdToast.show(
$mdToast.simple()
.content(success)
.position('top right')
.hideDelay(2000));
});
}
}
function copySelectedCards(folder) {
_selectedCardsOperation('copy', folder);
}
function moveSelectedCards(folder) {
_selectedCardsOperation('move', folder);
}
function selectAll() {

View File

@ -23,6 +23,8 @@
vm.showProperties = showProperties;
vm.share = share;
vm.subscribeToFolder = subscribeToFolder;
vm.isDroppableFolder = isDroppableFolder;
vm.dragSelectedCards = dragSelectedCards;
function select($event, folder) {
if ($state.params.addressbookId != folder.id &&
@ -214,7 +216,7 @@
addressbook: addressbook
}
});
/**
* @ngInject
*/
@ -301,6 +303,58 @@
.hideDelay(3000));
});
}
function isDroppableFolder(srcFolder, dstFolder) {
return (dstFolder.id != srcFolder.id) && (dstFolder.isOwned || dstFolder.acls.objectCreator);
}
/**
* @see AddressBookController._selectedCardsOperation
*/
function dragSelectedCards(srcFolder, dstFolder, mode) {
var dstId, cards, ids, clearCardView, promise, success;
dstId = dstFolder.id;
clearCardView = false;
cards = srcFolder.$selectedCards();
if (cards.length === 0)
cards = [srcFolder.$selectedCard()];
if (_.find(cards, function(card) {
return card.$isList();
})) {
$mdToast.show(
$mdToast.simple()
.content(l("Lists can't be moved or copied."))
.position('top right')
.hideDelay(2000));
return;
}
if (mode == 'copy') {
promise = srcFolder.$copyCards(cards, dstId);
success = l('%{0} card(s) copied', cards.length);
}
else {
promise = srcFolder.$moveCards(cards, dstId);
success = l('%{0} card(s) moved', cards.length);
// Check if currently displayed card will be moved
ids = _.map(cards, 'id');
clearCardView = (srcFolder.selectedCard && ids.indexOf(srcFolder.selectedCard) >= 0);
}
// Show success toast when action succeeds
promise.then(function() {
if (clearCardView)
$state.go('app.addressbook');
$mdToast.show(
$mdToast.simple()
.content(success)
.position('top right')
.hideDelay(2000));
});
}
}
angular

View File

@ -110,7 +110,7 @@
return addressbook.id == $stateParams.addressbookId;
});
if (addressbook) {
addressbook.selectedCard = false;
delete addressbook.selectedCard;
addressbook.$reload();
return addressbook;
}