From a14c45680029a846759179c33743626059835520 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Mon, 4 Oct 2021 10:02:33 -0400 Subject: [PATCH] feat(addressbook): warn when similar contacts are found When creating a new contact, we now search for similar contacts in the current addressbook, based on the firstname, lastname, email address, and telephone number. We return a 409 when such a card is found. --- UI/Contacts/English.lproj/Localizable.strings | 1 + UI/Contacts/UIxContactEditor.m | 94 ++++++++++- .../ContactsUI/UIxContactEditorTemplate.wox | 74 +++++--- .../ContactsUI/UIxContactViewTemplate.wox | 4 +- .../js/Contacts/Card.service.js | 12 +- .../js/Contacts/CardController.js | 158 +++++++++--------- .../scss/components/toolbar/toolbar.scss | 5 +- 7 files changed, 228 insertions(+), 120 deletions(-) diff --git a/UI/Contacts/English.lproj/Localizable.strings b/UI/Contacts/English.lproj/Localizable.strings index 20a700c890..8c97bd1067 100644 --- a/UI/Contacts/English.lproj/Localizable.strings +++ b/UI/Contacts/English.lproj/Localizable.strings @@ -350,6 +350,7 @@ "Synchronization" = "Synchronization"; "Synchronize" = "Synchronize"; "Sucessfully subscribed to address book" = "Successfully subscribed to address book"; +"A similar card already exists. Would you like to save it anyway?" = "A similar card already exists. Would you like to save it anyway?"; /* Aria label for scope of search on contacts */ "Search scope" = "Search scope"; diff --git a/UI/Contacts/UIxContactEditor.m b/UI/Contacts/UIxContactEditor.m index 4be69db310..e7741fc1df 100644 --- a/UI/Contacts/UIxContactEditor.m +++ b/UI/Contacts/UIxContactEditor.m @@ -463,28 +463,106 @@ - (void) setAttributes: (NSDictionary *) attributes * @apiParam {Object[]} urls URLs * @apiParam {String} urls.type Type (e.g., personal or work) * @apiParam {String} urls.value URL + * @apiParam {Boolean} ignoreDuplicate Don't check for similar cards */ - (id ) saveAction { + BOOL forceSave; + NSArray *similarRecords; + NSDictionary *params; SOGoContentObject *co; WORequest *request; - NSDictionary *params, *data; + id data; + unsigned int status; + status = 200; + similarRecords = [NSArray array]; co = [self clientObject]; card = [co vCard]; request = [context request]; params = [[request contentAsString] objectFromJSONString]; + forceSave = [[params objectForKey: @"ignoreDuplicate"] boolValue]; [self setAttributes: params]; - [co save]; - // Return card UID and addressbook ID in a JSON payload - data = [NSDictionary dictionaryWithObjectsAndKeys: - [[co container] nameInContainer], @"pid", - [co nameInContainer], @"id", - nil]; + if (forceSave == NO) + { + // Check if a similar card already exists in the same addressbook + EOKeyValueQualifier *qualifier; + NSEnumerator *allKeys; + NSMutableArray *qualifiers; + NSMutableDictionary *checks; + NSString *key; + id o; + + qualifiers = [NSMutableArray array]; + checks = [NSMutableDictionary dictionary]; + + o = [card n]; + if (o) + { + NSString *lastName = [o flattenedValueAtIndex: 0 forKey: @""]; + NSString *firstName = [o flattenedValueAtIndex: 1 forKey: @""]; + if ([lastName length] > 0) + [checks setObject: lastName forKey: @"c_sn"]; + if ([firstName length] > 0) + [checks setObject: firstName forKey: @"c_givenname"]; + } + o = [card fn]; + if ([o length]) + [checks setObject: o forKey: @"c_cn"]; + o = [card preferredEMail]; + if ([o length]) + [checks setObject: o forKey: @"c_mail"]; + o = [card preferredTel]; + if ([o length]) + [checks setObject: o forKey: @"c_telephonenumber"]; + + allKeys = [checks keyEnumerator]; + while ((key = [allKeys nextObject])) + { + qualifier = [[EOKeyValueQualifier alloc] + initWithKey: key + operatorSelector: EOQualifierOperatorCaseInsensitiveLike + value: [checks objectForKey: key]]; + [qualifier autorelease]; + [qualifiers addObject: qualifier]; + } + + if ([qualifiers count]) + { + if (![self isNew]) + { + // Exclude current contact + qualifier = [[EOKeyValueQualifier alloc] + initWithKey: @"c_name" + operatorSelector: EOQualifierOperatorNotEqual + value: [co nameInContainer]]; + [qualifier autorelease]; + [qualifiers addObject: qualifier]; + } + qualifier = [[EOAndQualifier alloc] initWithQualifierArray: qualifiers]; + similarRecords = [(SOGoContactGCSFolder *) [co container] lookupContactsWithQualifier: qualifier]; + } + } + + if ([similarRecords count]) + { + status = 409; + data = [similarRecords objectAtIndex: 0]; + } + else + { + [co save]; + + // Return card UID and addressbook ID in a JSON payload + data = [NSDictionary dictionaryWithObjectsAndKeys: + [[co container] nameInContainer], @"pid", + [co nameInContainer], @"id", + nil]; + } - return [self responseWithStatus: 200 andJSONRepresentation: data]; + return [self responseWithStatus: status andJSONRepresentation: data]; } @end /* UIxContactEditor */ diff --git a/UI/Templates/ContactsUI/UIxContactEditorTemplate.wox b/UI/Templates/ContactsUI/UIxContactEditorTemplate.wox index e81ea4312f..d170d9514c 100644 --- a/UI/Templates/ContactsUI/UIxContactEditorTemplate.wox +++ b/UI/Templates/ContactsUI/UIxContactEditorTemplate.wox @@ -8,37 +8,38 @@
+ +
+ + contacts +
+ + close + + + undo + + + save + +
+
-
- - close - - - undo - - - save -
-
-
-
{{editor.card.$description()}}
-
-
@@ -472,6 +473,25 @@
+ +
+
+ person {{ editor.duplicatedCard.$shortFormat() }} +
+
+ + + + + + + + diff --git a/UI/Templates/ContactsUI/UIxContactViewTemplate.wox b/UI/Templates/ContactsUI/UIxContactViewTemplate.wox index d55de05380..8b0e7eea76 100644 --- a/UI/Templates/ContactsUI/UIxContactViewTemplate.wox +++ b/UI/Templates/ContactsUI/UIxContactViewTemplate.wox @@ -9,9 +9,9 @@
- + diff --git a/UI/WebServerResources/js/Contacts/Card.service.js b/UI/WebServerResources/js/Contacts/Card.service.js index 71de58e24a..7543fd734a 100644 --- a/UI/WebServerResources/js/Contacts/Card.service.js +++ b/UI/WebServerResources/js/Contacts/Card.service.js @@ -268,9 +268,10 @@ * @memberof Card.prototype * @desc Save the card to the server. */ - Card.prototype.$save = function() { + Card.prototype.$save = function(options) { var _this = this, - action = 'saveAsContact'; + action = 'saveAsContact', + data; if (this.c_component == 'vlist') { action = 'saveAsList'; @@ -279,11 +280,16 @@ }); } + data = this.$omit(); + if (options && options.ignoreDuplicate) { + angular.extend(data, options); + } + return Card.$$resource.save([ Card.encodeUri(this.pid), Card.encodeUri(this.id) || '_new_' ].join('/'), - this.$omit(), + data, { action: action }) .then(function(data) { // Format birthdate diff --git a/UI/WebServerResources/js/Contacts/CardController.js b/UI/WebServerResources/js/Contacts/CardController.js index 9b161da801..de6417941f 100644 --- a/UI/WebServerResources/js/Contacts/CardController.js +++ b/UI/WebServerResources/js/Contacts/CardController.js @@ -20,25 +20,6 @@ vm.allAddressTypes = Card.$ADDRESS_TYPES; vm.categories = {}; vm.userFilterResults = []; - vm.transformCategory = transformCategory; - vm.removeAttribute = removeAttribute; - vm.addOrg = addOrg; - vm.addBirthday = addBirthday; - vm.addScreenName = addScreenName; - vm.addEmail = addEmail; - vm.addPhone = addPhone; - vm.addUrl = addUrl; - vm.addAddress = addAddress; - vm.canAddCustomField = canAddCustomField; - vm.addCustomField = addCustomField; - vm.deleteCustomField = deleteCustomField; - vm.userFilter = userFilter; - vm.save = save; - vm.close = close; - vm.reset = reset; - vm.cancel = cancel; - vm.confirmDelete = confirmDelete; - vm.toggleRawSource = toggleRawSource; vm.showRawSource = false; @@ -60,7 +41,7 @@ description: l('Delete'), callback: function($event) { if (vm.currentFolder.acls.objectEraser && vm.currentFolder.$selectedCount() === 0) - confirmDelete(); + vm.confirmDelete(); $event.preventDefault(); } })); @@ -81,67 +62,80 @@ }); } - function transformCategory(input) { + this.transformCategory = function (input) { if (angular.isString(input)) return { value: input }; else return input; - } - function removeAttribute(form, attribute, index) { - vm.card.$delete(attribute, index); + }; + + this.removeAttribute = function (form, attribute, index) { + this.card.$delete(attribute, index); form.$setDirty(); - } - function addOrg() { - var i = vm.card.$addOrg({ value: '' }); + }; + + this.addOrg = function () { + var i = this.card.$addOrg({ value: '' }); focus('org_' + i); - } - function addBirthday() { - vm.card.birthday = new Date(); - } - function addScreenName() { - vm.card.$addScreenName(''); - } - function addEmail() { - var i = vm.card.$addEmail(''); + }; + + this.addBirthday = function () { + this.card.birthday = new Date(); + }; + + this.addScreenName = function () { + this.card.$addScreenName(''); + }; + + this.addEmail = function () { + var i = this.card.$addEmail(''); focus('email_' + i); - } - function addPhone() { - var i = vm.card.$addPhone(''); + }; + + this.addPhone = function () { + var i = this.card.$addPhone(''); focus('phone_' + i); - } - function addUrl() { - var i = vm.card.$addUrl('', 'https://www.fsf.org/'); + }; + + this.addUrl = function () { + var i = this.card.$addUrl('', 'https://www.fsf.org/'); focus('url_' + i); - } - function canAddCustomField() { - return _.keys(stateCard.customFields).length < 4; - } - function addCustomField() { - if (!angular.isDefined(vm.card.customFields)) - vm.card.customFields = {}; + }; + + this.canAddCustomField = function () { + return _.keys(this.customFields).length < 4; + }; + + this.addCustomField = function () { + if (!angular.isDefined(this.card.customFields)) + this.card.customFields = {}; // Find the first 'available' custom field - var availableKeys = _.pullAll(['1', '2', '3', '4'], _.keys(stateCard.customFields)); - vm.card.customFields[availableKeys[0]] = ""; - } - function deleteCustomField(key) { - delete vm.card.customFields[key]; - } - function addAddress() { - var i = vm.card.$addAddress('', '', '', '', '', '', '', ''); + var availableKeys = _.pullAll(['1', '2', '3', '4'], _.keys(this.customFields)); + this.card.customFields[availableKeys[0]] = ""; + }; + + this.deleteCustomField = function (key) { + delete this.card.customFields[key]; + }; + + this.addAddress = function () { + var i = this.card.$addAddress('', '', '', '', '', '', '', ''); focus('address_' + i); - } - function userFilter($query, excludedCards) { + }; + + this.userFilter = function ($query, excludedCards) { if ($query.length < sgSettings.minimumSearchLength()) return []; return AddressBook.selectedFolder.$filter($query, {dry: true, excludeLists: true}, excludedCards).then(function(cards) { return cards; }); - } - function save(form) { + }; + + this.save = function (form, options) { if (form.$valid) { - vm.card.$save() + this.card.$save(options) .then(function(data) { var i = _.indexOf(_.map(AddressBook.selectedFolder.$cards, 'id'), vm.card.id); if (i < 0) { @@ -153,20 +147,31 @@ AddressBook.selectedFolder.$cards[i] = angular.copy(vm.card); } $state.go('app.addressbook.card.view', { cardId: vm.card.id }); + }, function(response) { + vm.duplicatedCard = new Card(response.data); }); } - } - function close() { + }; + + this.close = function () { $state.go('app.addressbook').then(function() { vm.card = null; delete AddressBook.selectedFolder.selectedCard; }); - } - function reset(form) { + }; + + this.edit = function (form) { + this.duplicatedCard = false; + form.$setPristine(); + form.$setDirty(); + }; + + this.reset = function (form) { vm.card.$reset(); form.$setPristine(); - } - function cancel() { + }; + + this.cancel = function () { vm.card.$reset(); if (vm.card.isNew) { // Cancelling the creation of a card @@ -178,8 +183,9 @@ // Cancelling the edition of an existing card $state.go('app.addressbook.card.view', { cardId: vm.card.id }); } - } - function confirmDelete() { + }; + + this.confirmDelete = function () { var card = stateCard; Dialog.confirm(l('Warning'), @@ -195,19 +201,19 @@ card.$fullname())); }); }); - } + }; - function toggleRawSource($event) { - if (!vm.showRawSource && !vm.rawSource) { - Card.$$resource.post(vm.currentFolder.id + '/' + vm.card.id, "raw").then(function(data) { + this.toggleRawSource = function ($event) { + if (!this.showRawSource && !this.rawSource) { + Card.$$resource.post(this.currentFolder.id + '/' + this.card.id, "raw").then(function(data) { vm.rawSource = data; vm.showRawSource = true; }); } else { - vm.showRawSource = !vm.showRawSource; + this.showRawSource = !this.showRawSource; } - } + }; } angular diff --git a/UI/WebServerResources/scss/components/toolbar/toolbar.scss b/UI/WebServerResources/scss/components/toolbar/toolbar.scss index 75b29997d0..a2f64832dc 100644 --- a/UI/WebServerResources/scss/components/toolbar/toolbar.scss +++ b/UI/WebServerResources/scss/components/toolbar/toolbar.scss @@ -14,15 +14,12 @@ $toolbar-padding: $mg; // must be declare in containers with sg-padded md-toolbar { z-index: $z-index-toolbar; - // dirty fix to override angular-material botchy typography - font-size: 1em !important; // No transition when switching toolbars transition-duration: 0s; } .md-toolbar-tools { - // dirty fix to override angular-material botchy typography - font-size: 1em !important; + font-size: $subhead-font-size-base; } md-toolbar,