From 958f0b4958d879a496ea6f571a1d2700c678927b Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Wed, 20 Aug 2014 15:49:44 -0400 Subject: [PATCH] Contacts: Add possibility to add/delete objects --- .../ContactsUI/UIxContactFoldersView.wox | 58 +++++--- UI/WebServerResources/js/Common/resource.js | 38 ++++- .../js/Contacts/addressbook-model.js | 68 ++++----- .../js/Contacts/card-model.js | 138 +++++++++++------- UI/WebServerResources/js/ContactsUI.js | 94 +++++++++--- UI/WebServerResources/scss/ContactsUI.scss | 35 ++--- 6 files changed, 277 insertions(+), 154 deletions(-) diff --git a/UI/Templates/ContactsUI/UIxContactFoldersView.wox b/UI/Templates/ContactsUI/UIxContactFoldersView.wox index 39a5757a4..4a76231d5 100644 --- a/UI/Templates/ContactsUI/UIxContactFoldersView.wox +++ b/UI/Templates/ContactsUI/UIxContactFoldersView.wox @@ -92,26 +92,41 @@
+ +
    -
  • -
  • -
  • -
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
+ + +
    +
  • +
+ + +
-
-
    -
  • -
+
  • - {{folder.name}} @@ -129,18 +144,18 @@ data-ng-cloak="ng-cloak" data-sg-focus-on="addressBookName_{{$index}}"/>
    - +
- +
@@ -169,20 +184,21 @@
-
+
@@ -203,7 +219,7 @@
- +
diff --git a/UI/WebServerResources/js/Common/resource.js b/UI/WebServerResources/js/Common/resource.js index 8f6964497..5c6288dda 100644 --- a/UI/WebServerResources/js/Common/resource.js +++ b/UI/WebServerResources/js/Common/resource.js @@ -2,18 +2,19 @@ 'use strict'; /* Constructor */ - function Resource($http, $q, path) { + function Resource($http, $q, path, options) { angular.extend(this, { _http: $http, _q: $q, _path: path }); + angular.extend(this, options); } /* The factory we'll use to register with Angular */ Resource.$factory = ['$http', '$q', function($http, $q) { - return function(path) { - return new Resource($http, $q, path); + return function(path, options) { + return new Resource($http, $q, path, options); }; }]; @@ -29,7 +30,8 @@ Resource.prototype.find = function(uid) { var deferred = this._q.defer(); - this._http.get(this.path(uid)) + this._http + .get(this.path(uid)) .success(deferred.resolve) .error(deferred.reject); @@ -50,9 +52,22 @@ return deferred.promise; }; - Resource.prototype.set = function(uid, newValue) { + Resource.prototype.newguid = function(uid) { var deferred = this._q.defer(); - var path = this._path + '/' + uid + '/save'; + var path = this._path + '/' + uid + '/newguid'; + + this._http + .get(path) + .success(deferred.resolve) + .error(deferred.reject); + + return deferred.promise; + }; + + Resource.prototype.set = function(uid, newValue, options) { + var deferred = this._q.defer(); + var action = (options && options.action)? options.action : 'save'; + var path = this._path + '/' + uid + '/' + action; this._http .post(path, newValue) @@ -62,4 +77,15 @@ return deferred.promise; }; + Resource.prototype.remove = function(uid) { + var deferred = this._q.defer(); + var path = this._path + '/' + uid + '/delete'; + + this._http + .get(path) + .success(deferred.resolve) + .error(deferred.reject); + + return deferred.promise; + }; })(); diff --git a/UI/WebServerResources/js/Contacts/addressbook-model.js b/UI/WebServerResources/js/Contacts/addressbook-model.js index ebcccd659..6b3d83824 100644 --- a/UI/WebServerResources/js/Contacts/addressbook-model.js +++ b/UI/WebServerResources/js/Contacts/addressbook-model.js @@ -27,6 +27,7 @@ /* Factory registration in Angular module */ angular.module('SOGo.Contacts').factory('sgAddressBook', AddressBook.$factory); + /* Set or get the list of addressbooks */ AddressBook.$all = function(data) { if (data) { this.$addressbooks = data; @@ -34,10 +35,10 @@ return this.$addressbooks; }; - /* Fetch list of cards */ + /* Fetch list of cards and return an AddressBook instance */ AddressBook.$find = function(addressbook_id) { - var futureAddressBookData = AddressBook.$$resource.find(addressbook_id); // should return attributes of addressbook + - // AddressBook.$selectedId(addressbook_id); + var futureAddressBookData = AddressBook.$$resource.find(addressbook_id); + return new AddressBook(futureAddressBookData); }; @@ -59,32 +60,35 @@ return this.$id().then(function(addressbook_id) { var futureAddressBookData = AddressBook.$$resource.filter(addressbook_id, params); return futureAddressBookData.then(function(data) { - return AddressBook.$timeout(function() { - self.cards = data.cards; - return self.cards; + self.cards = data.cards; + // Instanciate Card objects + angular.forEach(self.cards, function(o, i) { + self.cards[i] = new AddressBook.$Card(o); }); + return self.cards; }); }); }; + AddressBook.prototype.$delete = function() { + return AddressBook.$$resource.remove(this.id); + }; + + AddressBook.prototype.$save = function() { + return AddressBook.$$resource.set(this.id, this.$omit()).then(function (data) { + return data; + }); + }; + AddressBook.prototype.$getCard = function(card_id) { var self = this; return this.$id().then(function(addressbook_id) { - var card = AddressBook.$Card.$find(addressbook_id, card_id); - - AddressBook.$timeout(function() { - self.card = card; - }); - - return card.$futureCardData.then(function() { - angular.forEach(self.cards, function(o, i) { - if (o.c_name == card_id) { - angular.extend(self.card, o); - } - }); - - return self.card; + self.card = AddressBook.$Card.$find(addressbook_id, card_id); + self.card.$id().catch(function() { + // Card not found + self.card = null; }); + return self.card; }); }; @@ -93,7 +97,7 @@ }; AddressBook.prototype.$viewerIsActive = function(editMode) { - return (!angular.isUndefined(this.card) && !editMode); + return (this.card != null && !editMode); }; // Unwrap a promise @@ -104,12 +108,16 @@ this.$futureAddressBookData.then(function(data) { AddressBook.$timeout(function() { angular.extend(self, data); - self.$id().then(function(addressbook_id) { - angular.forEach(AddressBook.$all(), function(o, i) { - if (o.id == addressbook_id) { - angular.extend(self, o); - } - }); + // Also extend AddressBook instance from data of addressbooks list. + // Will inherit attributes such as isEditable and isRemote. + angular.forEach(AddressBook.$all(), function(o, i) { + if (o.id == self.id) { + angular.extend(self, o); + } + }); + // Instanciate Card objects + angular.forEach(self.cards, function(o, i) { + self.cards[i] = new AddressBook.$Card(o); }); }); }); @@ -128,10 +136,4 @@ }); return addressbook; }; - - AddressBook.prototype.$save = function() { - return AddressBook.$$resource.set(this.id, this.$omit()).then(function (data) { - return data; - }); - }; })(); diff --git a/UI/WebServerResources/js/Contacts/card-model.js b/UI/WebServerResources/js/Contacts/card-model.js index 56a7b6659..12f4e5855 100644 --- a/UI/WebServerResources/js/Contacts/card-model.js +++ b/UI/WebServerResources/js/Contacts/card-model.js @@ -2,16 +2,21 @@ 'use strict'; /* Constructor */ - function Card(futureCardData) { - /* DATA IS IMMEDIATELY AVAILABLE */ - // if (!futureCardData.inspect) { - // angular.extend(this, futureCardData); - // return; - // } + // Data is immediately available + if (typeof futureCardData.then !== 'function') { + angular.extend(this, futureCardData); + if (this.pid && !this.id) { + // Prepare for the creation of a new card; + // Get UID from the server. + var newCardData = Card.$$resource.newguid(this.pid); + this.$unwrap(newCardData); + } + return; + } - /* THE PROMISE WILL BE UNWRAPPED FIRST */ + // The promise will be unwrapped first this.$unwrap(futureCardData); } @@ -20,7 +25,7 @@ Card.$url_types = ['work', 'home', 'pref']; Card.$address_types = ['work', 'home']; - /* THE FACTORY WE'LL USE TO REGISTER WITH ANGULAR */ + /* The factory we'll use to register with Angular */ Card.$factory = ['$timeout', 'sgSettings', 'sgResource', function($timeout, Settings, Resource) { angular.extend(Card, { $$resource: new Resource(Settings.baseURL), @@ -30,10 +35,11 @@ return Card; // return constructor }]; - /* ANGULAR MODULE REGISTRATION */ + /* Factory registration in Angular module */ angular.module('SOGo.Contacts') .factory('sgCard', Card.$factory) + // Directive to format a postal address .directive('sgAddress', function() { return { restrict: 'A', @@ -57,12 +63,8 @@ } }); - - /* FETCH A UNIT */ + /* Fetch a card */ Card.$find = function(addressbook_id, card_id) { - - /* FALLS BACK ON AN INSTANCE OF RESOURCE */ - // Fetch data over the wire. var futureCardData = this.$$resource.find([addressbook_id, card_id].join('/')); if (card_id) return new Card(futureCardData); // a single card @@ -70,26 +72,53 @@ return Card.$unwrapCollection(futureCardData); // a collection of cards }; - // Instance methods + /* Unwrap to a collection of Card instances */ + Card.$unwrapCollection = function(futureCardData) { + var collection = {}; + + collection.$futureCardData = futureCardData; + + futureCardData.then(function(cards) { + Card.$timeout(function() { + angular.forEach(cards, function(data, index) { + collection[data.id] = new Card(data); + }); + }); + }); + + return collection; + }; + + /* Instance methods */ Card.prototype.$id = function() { - //var self = this; - return this.$futureAddressBookData.then(function(data) { + return this.$futureCardData.then(function(data) { return data.id; }); }; - // Unwrap a promise - Card.prototype.$unwrap = function(futureCardData) { - var self = this; - - this.$futureCardData = futureCardData; - this.$futureCardData.then(function(data) { - // The success callback. Calling _.extend from $timeout will wrap it into a try/catch call. - Card.$timeout(function() { - angular.extend(self, data); + Card.prototype.$save = function() { + var action = 'saveAsContact'; + if (this.tag == 'VLIST') action = 'saveAsList'; + //var action = 'saveAs' + this.tag.substring(1).capitalize(); + return Card.$$resource.set([this.pid, this.id || '_new_'].join('/'), + this.$omit(), + { 'action': action }) + .then(function (data) { + return data; }); - }); + }; + + Card.prototype.$delete = function(attribute, index) { + if (attribute) { + if (index > -1 && this[attribute].length > index) { + this[attribute].splice(index, 1); + } + } + else { + // No arguments -- delete card + return Card.$$resource.remove([this.pid, this.id].join('/')); + } }; Card.prototype.$fullname = function() { @@ -110,6 +139,9 @@ else if (this.emails && this.emails.length > 0) { fn = _.find(this.emails, function(i) { return i.value != ''; }).value; } + else if (this.c_cn && this.c_cn.length > 0) { + fn = this.c_cn; + } } return fn; @@ -130,22 +162,30 @@ return description.join(', '); }; + Card.prototype.$preferredEmail = function() { + var email = _.find(this.emails, function(o) { + return o.type == 'pref'; + }); + if (email) { + email = email.value; + } + else if (this.emails && this.emails.length) { + email = this.emails[0].value; + } + + return email; + }; + Card.prototype.$birthday = function() { return new Date(this.birthday*1000); }; Card.prototype.$isCard = function() { - return this.tag == 'VCARD'; + return this.tag == 'vcard'; }; Card.prototype.$isList = function() { - return this.tag == 'VLIST'; - }; - - Card.prototype.$delete = function(attribute, index) { - if (index > -1 && this[attribute].length > index) { - this[attribute].splice(index, 1); - } + return this.tag == 'vlist'; }; Card.prototype.$addOrgUnit = function(orgUnit) { @@ -226,24 +266,21 @@ return this.addresses.length - 1; }; - /* never call for now */ - Card.$unwrapCollection = function(futureCardData) { - var collection = {}; + // Unwrap a promise + Card.prototype.$unwrap = function(futureCardData) { + var self = this; - collection.$futureCardData = futureCardData; - - futureCardData.then(function(cards) { + this.$futureCardData = futureCardData; + this.$futureCardData.then(function(data) { + // The success callback. Calling _.extend from $timeout will wrap it into a try/catch call and return + // a promise resolved immediately. Card.$timeout(function() { - angular.forEach(cards, function(data, index) { - collection[data.id] = new Card(data); - }); + angular.extend(self, data); }); }); - - return collection; }; - // $omit returns a sanitized object used to send to the server + // Return a sanitized object used to send to the server Card.prototype.$omit = function() { var card = {}; angular.forEach(this, function(value, key) { @@ -253,11 +290,4 @@ }); return card; }; - - Card.prototype.$save = function() { - return Card.$$resource.set([this.pid, this.id].join('/'), this.$omit()).then(function (data) { - return data; - }); - }; - })(); diff --git a/UI/WebServerResources/js/ContactsUI.js b/UI/WebServerResources/js/ContactsUI.js index 698bb7189..8cfe578e2 100644 --- a/UI/WebServerResources/js/ContactsUI.js +++ b/UI/WebServerResources/js/ContactsUI.js @@ -6,7 +6,7 @@ angular.module('SOGo.Common', []); - angular.module('SOGo.Contacts', ['ngSanitize', 'ui.router', 'mm.foundation', 'mm.foundation.offcanvas', 'SOGo.Common']) + angular.module('SOGo.Contacts', ['ngSanitize', 'ui.router', 'mm.foundation', 'mm.foundation.offcanvas', 'SOGo.Common', 'SOGo.UIDesktop']) .constant('sgSettings', { 'baseURL': ApplicationBaseURL @@ -31,6 +31,15 @@ controller: 'cardCtrl' } } + }) + .state('addressbook.new', { + url: "/:contact_type/new", + views: { + 'card': { + templateUrl: "card.html", + controller: 'cardCtrl' + } + } }); // if none of the above states are matched, use this as the fallback @@ -56,31 +65,36 @@ } }]) - .controller('AddressBookCtrl', ['$state', '$scope', '$rootScope', '$stateParams', '$timeout', '$modal', 'sgFocus', 'sgCard', 'sgAddressBook', function($state, $scope, $rootScope, $stateParams, $timeout, $modal, focus, Card, AddressBook) { + .controller('AddressBookCtrl', ['$state', '$scope', '$rootScope', '$stateParams', '$timeout', '$modal', 'sgFocus', 'sgCard', 'sgAddressBook', 'sgDialog', function($state, $scope, $rootScope, $stateParams, $timeout, $modal, focus, Card, AddressBook, Dialog) { + // $scope objects $scope.search = { 'status': null, 'filter': null, 'last_filter': null }; + if ($stateParams.addressbook_id && ($rootScope.addressbook == undefined || $stateParams.addressbook_id != $rootScope.addressbook.id)) { - // Selected addressbook has changed + // Selected addressbook has changed; fetch list of contacts $rootScope.addressbook = AddressBook.$find($stateParams.addressbook_id); - // Extend resulting model instance with parameters from addressbooks listing + // Adjust search status depending on addressbook type var o = _.find($rootScope.addressbooks, function(o) { return o.id == $stateParams.addressbook_id; }); - $scope.search.status = (o.isRemote)? 'remote-addressbook' : ''; + $scope.search.status = (o && o.isRemote)? 'remote-addressbook' : ''; } // Initialize with data from template $scope.init = function() { $rootScope.addressbooks = AddressBook.$all(contactFolders); }; + // $scope functions $scope.select = function(rowIndex) { $scope.editMode = false; }; $scope.edit = function(i) { - if (angular.isUndefined(i)) { - i = _.indexOf(_.pluck($rootScope.addressbooks, 'id'), $rootScope.addressbook.id); + if (!$rootScope.addressbook.isRemote) { + if (angular.isUndefined(i)) { + i = _.indexOf(_.pluck($rootScope.addressbooks, 'id'), $rootScope.addressbook.id); + } + $scope.editMode = $rootScope.addressbook.id; + focus('addressBookName_' + i); } - $scope.editMode = $rootScope.addressbook.id; - focus('addressBookName_' + i); }; $scope.save = function(i) { $rootScope.addressbook.name = $rootScope.addressbooks[i].name; @@ -92,7 +106,22 @@ console.debug("failed"); }); }; - $scope.sharing = function() { + $scope.confirmDelete = function() { + Dialog.confirm(l('Warning'), l('Are you sure you want to delete the addressbook "%{0}"?', + $rootScope.addressbook.name), function() { + $rootScope.addressbook.$delete() + .then(function() { + $rootScope.addressbooks = _.reject($rootScope.addressbooks, function(o) { + return o.id == $rootScope.addressbook.id; + }); + $rootScope.addressbook = null; + }, function(data, status) { + Dialog.alert(l('Warning'), l('An error occured while deleting the addressbook "%{0}".', + $rootScope.addressbook.name)); + }); + }); + }; + $scope.share = function() { var modal = $modal.open({ templateUrl: 'addressbookSharing.html', //controller: 'addressbookSharingCtrl' @@ -130,26 +159,27 @@ }; }]) - // angular.module('SOGo').controller('addressbookSharingCtrl', ['$scope', '$modalInstance', function($scope, modal) { - // $scope.closeModal = function() { - // console.debug('please close it'); - // modal.close(); - // }; - // }]); - - .controller('cardCtrl', ['$scope', '$rootScope', 'sgAddressBook', 'sgCard', 'sgFocus', '$stateParams', function($scope, $rootScope, AddressBook, Card, focus, $stateParams) { + .controller('cardCtrl', ['$scope', '$rootScope', 'sgAddressBook', 'sgCard', 'sgDialog', 'sgFocus', '$state', '$stateParams', function($scope, $rootScope, AddressBook, Card, Dialog, focus, $state, $stateParams) { if ($stateParams.card_id) { + // Show existing card if ($rootScope.addressbook == null) { // Card is directly access with URL fragment $rootScope.addressbook = AddressBook.$find($stateParams.addressbook_id); } - $rootScope.addressbook.$getCard($stateParams.card_id); + $rootScope.addressbook.$getCard($stateParams.card_id) $scope.editMode = false; } + else if ($stateParams.contact_type) { + // Create new card or list + var tag = 'v' + $stateParams.contact_type; + $scope.addressbook.card = new Card({ 'pid': $stateParams.addressbook_id, 'tag': tag }); + $scope.editMode = true; + } $scope.allEmailTypes = Card.$email_types; $scope.allTelTypes = Card.$tel_types; $scope.allUrlTypes = Card.$url_types; $scope.allAddressTypes = Card.$address_types; + $scope.edit = function() { $rootScope.master_card = angular.copy($rootScope.addressbook.card); $scope.editMode = true; @@ -180,12 +210,20 @@ focus('address_' + i); }; $scope.save = function(cardForm) { - console.debug("save"); if (cardForm.$valid) { $rootScope.addressbook.card.$save() .then(function(data) { console.debug("saved!"); $scope.editMode = false; + var i = _.indexOf(_.pluck($rootScope.addressbook.cards, 'id'), $rootScope.addressbook.card.id); + if (i < 0) { + // Reload contacts list and show addressbook in which the card has been created + $rootScope.addressbook = AddressBook.$find(data.pid); + } + else { + // Update contacts list with new version of the Card object + $rootScope.addressbook.cards[i] = angular.copy($rootScope.addressbook.card); + } }, function(data, status) { console.debug("failed"); }); @@ -198,6 +236,22 @@ $scope.reset = function() { $rootScope.addressbook.card = angular.copy($rootScope.master_card); }; + $scope.confirmDelete = function(card) { + Dialog.confirm(l('Warning'), + l('Are you sure you want to delete the card of "%{0}"?', card.$fullname()), + function() { + card.$delete() + .then(function() { + $rootScope.addressbook.cards = _.reject($rootScope.addressbook.cards, function(o) { + return o.id == card.id; + }); + $rootScope.addressbook.card = null; + }, function(data, status) { + Dialog.alert(l('Warning'), l('An error occured while deleting the card "%{0}".', + card.$fullname())); + }); + }); + }; }]); })(); diff --git a/UI/WebServerResources/scss/ContactsUI.scss b/UI/WebServerResources/scss/ContactsUI.scss index 646f5ac9c..278d171a2 100644 --- a/UI/WebServerResources/scss/ContactsUI.scss +++ b/UI/WebServerResources/scss/ContactsUI.scss @@ -9,7 +9,7 @@ $module-color: #6F5A73; $module-secondary-color: #8EC588; // light green $module-secondary-color: #3D792A; // green $module-secondary-color: #B996BF; -$module-secondary-color: #B59BB9; +$module-secondary-color: #B59BB9; // SOGo green $module-light-color: #F7ECFF; $topbar-bg-color: $module-color; $topbar-link-bg-active-hover: scale-color($module-secondary-color, $lightness: -14%); @@ -66,6 +66,7 @@ $topbar-link-bg-hover: scale-color($module-color, $lightness: -14%); //vertical-align: middle; } .icon { + //vertical-align: middle; // causes glitch when selecting row width: 2em; } form { @@ -77,7 +78,7 @@ $topbar-link-bg-hover: scale-color($module-color, $lightness: -14%); display: block; width: 100%; padding: 0; - padding-left: $off-canvas-link-padding; + //padding-left: $off-canvas-link-padding; } input { border: 0; @@ -117,7 +118,7 @@ $column-gutter: 0; height: 40px; border-radius: 20px; color: $input-disabled-bg; - line-height: 40px; + //line-height: 40px; text-align: center; border: 1px solid $input-disabled-bg; margin-right: 1em; @@ -130,6 +131,15 @@ $column-gutter: 0; #pageContent { @include grid-row($behavior: nest); + #newListDrop { + a { + text-transform: uppercase; + &.tiny { + @include button-size($padding:$button-tny); + } + } + } + #addressbooksList { position: absolute; overflow: auto; @@ -393,14 +403,14 @@ $column-gutter: 0; } input { display: table-cell; - /* transition: background-color 300ms ease; */ + //transition: all 300ms ease; //background-color: $secondary-color; background-color: transparent; border: 0; box-shadow: none; margin-bottom: 0; /* &:focus { */ - /* background-color: #fff; */ + /* border: 1px solid blue; */ /* } */ } } @@ -439,21 +449,6 @@ $column-gutter: 0; } .f-dropdown { - dd { - a { - background-color: $module-secondary-color; - color: #fff; - &:hover { - background-color: $module-color; - } - } - &.active { - a { - background-color: $module-color; - color: #fff; - } - } - } &.icons-dropdown { width: auto; //background-color: $primary-color;