Skip to content

Commit

Permalink
feat(addressbook): warn when similar contacts are found
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cgx committed Oct 4, 2021
1 parent d0c91d7 commit a14c456
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 120 deletions.
1 change: 1 addition & 0 deletions UI/Contacts/English.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
94 changes: 86 additions & 8 deletions UI/Contacts/UIxContactEditor.m
Original file line number Diff line number Diff line change
Expand Up @@ -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 <WOActionResults>) saveAction
{
BOOL forceSave;
NSArray *similarRecords;
NSDictionary *params;
SOGoContentObject <SOGoContactObject> *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 */
Expand Down
74 changes: 47 additions & 27 deletions UI/Templates/ContactsUI/UIxContactEditorTemplate.wox
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,38 @@

<div layout="column" class="layout-fill sg-reversible">
<md-card style="overflow: hidden">
<md-toolbar>
<div class="md-toolbar-tools">
<md-button ng-click="toggleCenter()"
class="sg-icon-button md-primary md-hue-1 hide show-gt-xs"
aria-hidden="true">
<md-icon>{{ centerIsClose ? 'fullscreen_exit' : 'fullscreen' }}</md-icon>
</md-button>
<md-icon class="material-icons sg-icon-toolbar-bg">contacts</md-icon>
<div class="sg-md-title md-flex" ng-bind-html="editor.card.$fullname({html: true})"><!-- computed fullname --></div>
<md-button class="sg-icon-button"
label:aria-label="Cancel"
ng-click="editor.cancel()">
<md-icon>close</md-icon>
</md-button>
<md-button class="sg-icon-button"
label:aria-label="Reset"
ng-click="editor.reset(contactForm)">
<md-icon>undo</md-icon>
</md-button>
<md-button class="sg-icon-button"
label:aria-label="Save"
type="submit"
ng-disabled="contactForm.$pristine || contactForm.$invalid || contactForm.$submitted || editor.duplicatedCard"
ng-click="editor.save(contactForm)">
<md-icon>save</md-icon>
</md-button>
</div>
</md-toolbar>
<md-card-actions flex-none="flex-none" layout="row" layout-align="end center">
<md-button ng-click="toggleCenter()"
class="md-icon-button md-primary md-hue-1 hide show-gt-xs"
aria-hidden="true">
<md-icon>{{ centerIsClose ? 'fullscreen_exit' : 'fullscreen' }}</md-icon>
</md-button>
<div class="md-flex"><!-- spacer --></div>
<md-button class="md-icon-button"
label:aria-label="Cancel"
ng-click="editor.cancel()">
<md-icon>close</md-icon>
</md-button>
<md-button class="md-icon-button"
label:aria-label="Reset"
ng-click="editor.reset(contactForm)">
<md-icon>undo</md-icon>
</md-button>
<md-button class="md-icon-button"
label:aria-label="Save"
type="submit"
ng-disabled="contactForm.$pristine || contactForm.$invalid || contactForm.$submitted"
ng-click="editor.save(contactForm)">
<md-icon>save</md-icon>
</md-button>
</md-card-actions>
<md-card-content>
<hgroup class="header">
<div class="sg-md-display-2--thin" ng-bind-html="editor.card.$fullname()"><!-- fullname --></div>
<div class="sg-md-display-2-subheader--thin">{{editor.card.$description()}}</div>
</hgroup>

<form class="md-inline-form" name="contactForm"
ng-submit="editor.save(contactForm)">

Expand Down Expand Up @@ -472,6 +473,25 @@
</div>
</form>
</md-card-content>
<md-card-actions class="md-default-theme md-bg md-warn md-padding sg-dialog-message ng-hide"
ng-show="editor.duplicatedCard">
<div class="sg-padded--bottom" ng-bind-html="::'A similar card already exists. Would you like to save it anyway?' | loc | txt2html"><!-- warning --></div>
<div class="md-flex">
<md-icon>person</md-icon> {{ editor.duplicatedCard.$shortFormat() }}
</div>
</md-card-actions>
<md-card-actions class="ng-hide" layout="row" layout-align="end center" ng-show="editor.duplicatedCard">
<md-button type="button"
ng-click="editor.edit(contactForm)">
<var:string label:value="Edit"/>
</md-button>
<md-button class="md-warn" type="button"
label:aria-label="Save"
ng-disabled="contactForm.$invalid || contactForm.$submitted"
ng-click="editor.save(contactForm, { ignoreDuplicate: true })">
<var:string label:value="Save"/>
</md-button>
</md-card-actions>
</md-card>
</div>
</container>
4 changes: 2 additions & 2 deletions UI/Templates/ContactsUI/UIxContactViewTemplate.wox
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
<div class="sg-reversible" ng-class="{ 'sg-flip': editor.showRawSource }">
<div class="sg-face" layout="column" layout-fill="layout-fill">
<md-card>
<md-card-actions flex-none="flex-none" layout="row" layout-align="end center">
<md-card-actions class="md-toolbar-tools" flex-none="flex-none" layout="row" layout-align="end center">
<md-button ng-click="toggleCenter()"
class="md-icon-button md-primary md-hue-1 hide show-gt-xs"
class="sg-icon-button md-primary md-hue-1 hide show-gt-xs"
aria-hidden="true">
<md-icon>{{ centerIsClose ? 'fullscreen_exit' : 'fullscreen' }}</md-icon>
</md-button>
Expand Down
12 changes: 9 additions & 3 deletions UI/WebServerResources/js/Contacts/Card.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down
Loading

0 comments on commit a14c456

Please sign in to comment.