mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-03-26 08:32:44 +00:00
(feature) Live loading of (GCS) addressbooks
Using md-on-demand of md-virtual-repeat, we now progressively load the cards metadata of a personal addressbook.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (C) 2006-2015 Inverse inc.
|
||||
Copyright (C) 2006-2016 Inverse inc.
|
||||
|
||||
This file is part of SOGo.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with OGo; see the file COPYING. If not, write to the
|
||||
License along with SOGo; see the file COPYING. If not, write to the
|
||||
Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
|
||||
02111-1307, USA.
|
||||
*/
|
||||
@@ -22,6 +22,8 @@
|
||||
#ifndef __Contacts_SOGoContactGCSFolder_H__
|
||||
#define __Contacts_SOGoContactGCSFolder_H__
|
||||
|
||||
#import <EOControl/EOQualifier.h>
|
||||
|
||||
#import <SOGo/SOGoGCSFolder.h>
|
||||
|
||||
#import "SOGoFolder+CardDAV.h"
|
||||
@@ -34,7 +36,13 @@
|
||||
NSString *baseCardDAVURL, *basePublicCardDAVURL;
|
||||
}
|
||||
- (void) fixupContactRecord: (NSMutableDictionary *) contactRecord;
|
||||
- (EOQualifier *) qualifierForFilter: (NSString *) filter
|
||||
onCriteria: (NSString *) criteria;
|
||||
- (NSDictionary *) lookupContactWithName: (NSString *) aName;
|
||||
- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier;
|
||||
- (NSArray *) lookupContactsFields: (NSArray *) fields
|
||||
withQualifier: (EOQualifier *) qualifier
|
||||
andOrderings: (NSArray *) orderings;
|
||||
- (NSString *) cardDavURL;
|
||||
- (NSString *) publicCardDavURL;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (C) 2006-2013-2015 Inverse inc.
|
||||
Copyright (C) 2006-2016 Inverse inc.
|
||||
|
||||
This file is part of SOGo.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with OGo; see the file COPYING. If not, write to the
|
||||
License along with SOGo; see the file COPYING. If not, write to the
|
||||
Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
|
||||
02111-1307, USA.
|
||||
*/
|
||||
@@ -26,6 +26,7 @@
|
||||
#import <NGExtensions/NSObject+Logs.h>
|
||||
#import <NGExtensions/NSString+misc.h>
|
||||
#import <SaxObjC/XMLNamespaces.h>
|
||||
#import <EOControl/EOFetchSpecification.h>
|
||||
#import <EOControl/EOQualifier.h>
|
||||
#import <EOControl/EOSortOrdering.h>
|
||||
|
||||
@@ -181,8 +182,8 @@ static NSArray *folderListingFields = nil;
|
||||
return obj;
|
||||
}
|
||||
|
||||
- (EOQualifier *) _qualifierForFilter: (NSString *) filter
|
||||
onCriteria: (NSString *) criteria
|
||||
- (EOQualifier *) qualifierForFilter: (NSString *) filter
|
||||
onCriteria: (NSString *) criteria
|
||||
{
|
||||
NSString *qs;
|
||||
EOQualifier *qualifier;
|
||||
@@ -239,7 +240,7 @@ static NSArray *folderListingFields = nil;
|
||||
if ([data length])
|
||||
[contactRecord setObject: data forKey: @"id"];
|
||||
|
||||
// c_cn => fn
|
||||
// c_cn
|
||||
data = [contactRecord objectForKey: @"c_cn"];
|
||||
if (![data length])
|
||||
{
|
||||
@@ -255,7 +256,7 @@ static NSArray *folderListingFields = nil;
|
||||
}
|
||||
}
|
||||
|
||||
// c_screenname => X-AIM
|
||||
// c_screenname
|
||||
if (![contactRecord objectForKey: @"c_screenname"])
|
||||
[contactRecord setObject: @"" forKey: @"c_screenname"];
|
||||
|
||||
@@ -268,9 +269,12 @@ static NSArray *folderListingFields = nil;
|
||||
[contactRecord setObject: [NSArray arrayWithObject: email] forKey: @"emails"];
|
||||
}
|
||||
else
|
||||
[contactRecord setObject: @"" forKey: @"c_mail"];
|
||||
{
|
||||
[contactRecord setObject: @"" forKey: @"c_mail"];
|
||||
[contactRecord setObject: [NSArray array] forKey: @"emails"];
|
||||
}
|
||||
|
||||
// c_telephonenumber => phones
|
||||
// c_telephonenumber => phones[]
|
||||
data = [contactRecord objectForKey: @"c_telephonenumber"];
|
||||
if ([data length])
|
||||
{
|
||||
@@ -279,7 +283,10 @@ static NSArray *folderListingFields = nil;
|
||||
[contactRecord setObject: [NSArray arrayWithObject: phonenumber] forKey: @"phones"];
|
||||
}
|
||||
else
|
||||
[contactRecord setObject: @"" forKey: @"c_telephonenumber"];
|
||||
{
|
||||
[contactRecord setObject: @"" forKey: @"c_telephonenumber"];
|
||||
[contactRecord setObject: [NSArray array] forKey: @"phones"];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *) _flattenedRecords: (NSArray *) records
|
||||
@@ -347,9 +354,9 @@ static NSArray *folderListingFields = nil;
|
||||
EOQualifier *qualifier;
|
||||
EOSortOrdering *ordering;
|
||||
|
||||
qualifier = [self _qualifierForFilter: filter onCriteria: criteria];
|
||||
qualifier = [self qualifierForFilter: filter onCriteria: criteria];
|
||||
dbRecords = [[self ocsFolder] fetchFields: folderListingFields
|
||||
matchingQualifier: qualifier];
|
||||
matchingQualifier: qualifier];
|
||||
|
||||
if ([dbRecords count] > 0)
|
||||
{
|
||||
@@ -370,6 +377,37 @@ static NSArray *folderListingFields = nil;
|
||||
return records;
|
||||
}
|
||||
|
||||
- (NSArray *) lookupContactsWithQualifier: (EOQualifier *) qualifier
|
||||
{
|
||||
return [self lookupContactsFields: folderListingFields
|
||||
withQualifier: qualifier
|
||||
andOrderings: nil];
|
||||
}
|
||||
|
||||
- (NSArray *) lookupContactsFields: (NSArray *) fields
|
||||
withQualifier: (EOQualifier *) qualifier
|
||||
andOrderings: (NSArray *) orderings
|
||||
{
|
||||
NSArray *dbRecords, *records;
|
||||
EOFetchSpecification *spec;
|
||||
|
||||
spec = [EOFetchSpecification fetchSpecificationWithEntityName: [[self ocsFolder] folderName]
|
||||
qualifier: qualifier
|
||||
sortOrderings: orderings];
|
||||
|
||||
dbRecords = [[self ocsFolder] fetchFields: fields
|
||||
fetchSpecification: spec
|
||||
ignoreDeleted: YES];
|
||||
|
||||
if ([dbRecords count] > 0 && fields == folderListingFields)
|
||||
records = [self _flattenedRecords: dbRecords];
|
||||
else
|
||||
records = dbRecords;
|
||||
|
||||
[self debugWithFormat:@"fetched %i records.", [records count]];
|
||||
return records;
|
||||
}
|
||||
|
||||
- (NSDictionary *) davSQLFieldsTable
|
||||
{
|
||||
static NSMutableDictionary *davSQLFieldsTable = nil;
|
||||
|
||||
@@ -31,9 +31,14 @@
|
||||
@interface UIxContactsListActions : WODirectAction
|
||||
{
|
||||
NSDictionary *currentContact;
|
||||
|
||||
NSArray *contactInfos;
|
||||
NSArray *sortedIDs;
|
||||
}
|
||||
|
||||
- (NSString *) cardDavURL;
|
||||
- (NSString *) publicCardDavURL;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* __UIxContactsListActions_H__ */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (C) 2006-2015 Inverse inc.
|
||||
Copyright (C) 2006-2016 Inverse inc.
|
||||
|
||||
This file is part of SOGo.
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Foundation/NSEnumerator.h>
|
||||
|
||||
#import <SoObjects/SOGo/NSArray+Utilities.h>
|
||||
#import <SoObjects/SOGo/NSDictionary+Utilities.h>
|
||||
#import <SoObjects/SOGo/NSString+Utilities.h>
|
||||
@@ -32,6 +34,9 @@
|
||||
#import <NGExtensions/NSString+misc.h>
|
||||
#import <NGExtensions/NSNull+misc.h>
|
||||
|
||||
#import <EOControl/EOQualifier.h>
|
||||
#import <EOControl/EOSortOrdering.h>
|
||||
|
||||
#import <Common/WODirectAction+SOGo.h>
|
||||
|
||||
|
||||
@@ -43,12 +48,18 @@
|
||||
|
||||
#import "UIxContactsListActions.h"
|
||||
|
||||
// The maximum number of headers to prefetch when querying the IDs list
|
||||
#define headersPrefetchMaxSize 100
|
||||
|
||||
@implementation UIxContactsListActions
|
||||
|
||||
- (id) init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
contactInfos = nil;
|
||||
{
|
||||
contactInfos = nil;
|
||||
sortedIDs = nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
@@ -56,6 +67,7 @@
|
||||
- (void) dealloc
|
||||
{
|
||||
[contactInfos release];
|
||||
[sortedIDs release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@@ -99,7 +111,7 @@
|
||||
[us setObject: contactSettings forKey: @"Contact"];
|
||||
}
|
||||
[contactSettings setObject: [NSArray arrayWithObjects: [sort lowercaseString], [NSString stringWithFormat: @"%d", [ascending intValue]], nil]
|
||||
forKey: @"SortingState"];
|
||||
forKey: @"SortingState"];
|
||||
[us synchronize];
|
||||
}
|
||||
}
|
||||
@@ -108,11 +120,12 @@
|
||||
{
|
||||
id <SOGoContactFolder> folder;
|
||||
NSString *ascending, *searchText, *valueText;
|
||||
NSArray *results;
|
||||
NSMutableArray *filteredContacts;
|
||||
NSArray *results, *fields;
|
||||
NSMutableArray *filteredContacts, *headers;
|
||||
NSDictionary *contact;
|
||||
BOOL excludeLists;
|
||||
NSComparisonResult ordering;
|
||||
NSUInteger max, count;
|
||||
WORequest *rq;
|
||||
unsigned int i;
|
||||
|
||||
@@ -158,6 +171,152 @@
|
||||
{
|
||||
contactInfos = results;
|
||||
}
|
||||
|
||||
// Convert array of dictionaries to array of arrays (lighter on the wire)
|
||||
max = [contactInfos count];
|
||||
headers = [NSMutableArray arrayWithCapacity: max];
|
||||
if (max > 0)
|
||||
{
|
||||
count = 0;
|
||||
fields = [[contactInfos objectAtIndex: 0] allKeys];
|
||||
[headers addObject: fields];
|
||||
while (count < max)
|
||||
{
|
||||
[headers addObject: [[contactInfos objectAtIndex: count] objectsForKeys: fields
|
||||
notFoundMarker: [NSNull null]]];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
contactInfos = headers;
|
||||
[contactInfos retain];
|
||||
}
|
||||
|
||||
return contactInfos;
|
||||
}
|
||||
|
||||
- (NSArray *) sortedIDs
|
||||
{
|
||||
id <SOGoContactFolder> folder;
|
||||
NSString *ascending, *searchText, *valueText;
|
||||
NSArray *fields, *records;
|
||||
NSDictionary *record;
|
||||
NSEnumerator *recordsList;
|
||||
NSMutableArray *ids;
|
||||
BOOL excludeLists;
|
||||
EOKeyValueQualifier *kvQualifier;
|
||||
EOSortOrdering *ordering;
|
||||
EOQualifier *qualifier;
|
||||
WORequest *rq;
|
||||
SEL compare;
|
||||
|
||||
folder = [self clientObject];
|
||||
|
||||
if (!sortedIDs && [folder isKindOfClass: [SOGoContactGCSFolder class]])
|
||||
{
|
||||
fields = [NSArray arrayWithObjects: @"c_name", nil];
|
||||
rq = [context request];
|
||||
qualifier = nil;
|
||||
|
||||
// ORDER BY clause
|
||||
ascending = [rq formValueForKey: @"asc"];
|
||||
if (![ascending length] || [ascending boolValue])
|
||||
compare = EOCompareAscending;
|
||||
else
|
||||
compare = EOCompareDescending;
|
||||
ordering = [EOSortOrdering sortOrderingWithKey: [self sortKey]
|
||||
selector: compare];
|
||||
|
||||
// WHERE clause
|
||||
searchText = [rq formValueForKey: @"search"];
|
||||
if ([searchText length] > 0)
|
||||
{
|
||||
valueText = [rq formValueForKey: @"value"];
|
||||
qualifier = [(SOGoContactGCSFolder *) folder qualifierForFilter: valueText
|
||||
onCriteria: searchText];
|
||||
}
|
||||
excludeLists = [[rq formValueForKey: @"excludeLists"] boolValue];
|
||||
if (excludeLists)
|
||||
{
|
||||
kvQualifier = [[EOKeyValueQualifier alloc]
|
||||
initWithKey: @"c_component"
|
||||
operatorSelector: EOQualifierOperatorNotEqual
|
||||
value: @"vlist"];
|
||||
[kvQualifier autorelease];
|
||||
if (qualifier)
|
||||
qualifier = [[EOAndQualifier alloc] initWithQualifiers: kvQualifier, qualifier, nil];
|
||||
else
|
||||
qualifier = kvQualifier;
|
||||
}
|
||||
|
||||
// Perform lookup
|
||||
records = [(SOGoContactGCSFolder *) folder lookupContactsFields: fields
|
||||
withQualifier: qualifier
|
||||
andOrderings: [NSArray arrayWithObject: ordering]];
|
||||
|
||||
// Convert records to an array of strings (c_name)
|
||||
ids = [NSMutableArray arrayWithCapacity: [records count]];
|
||||
recordsList = [records objectEnumerator];
|
||||
while ((record = [recordsList nextObject]))
|
||||
[ids addObject: [record objectForKey: @"c_name"]];
|
||||
|
||||
[sortedIDs release];
|
||||
sortedIDs = ids;
|
||||
[sortedIDs retain];
|
||||
}
|
||||
|
||||
return sortedIDs;
|
||||
}
|
||||
|
||||
- (NSArray *) getHeadersForIDs: (NSArray *) ids
|
||||
{
|
||||
id <SOGoContactFolder> folder;
|
||||
NSArray *results, *fields;
|
||||
NSEnumerator *idsList;
|
||||
NSMutableArray *qualifiers, *headers;
|
||||
NSString *contactID;
|
||||
EOKeyValueQualifier *kvQualifier;
|
||||
EOQualifier *orQualifier;
|
||||
NSUInteger max, count;
|
||||
|
||||
folder = [self clientObject];
|
||||
|
||||
if (!contactInfos && [ids count] && [folder isKindOfClass: [SOGoContactGCSFolder class]])
|
||||
{
|
||||
qualifiers = [NSMutableArray arrayWithCapacity: [ids count]];
|
||||
idsList = [ids objectEnumerator];
|
||||
|
||||
while ((contactID = [idsList nextObject]))
|
||||
{
|
||||
kvQualifier = [[EOKeyValueQualifier alloc]
|
||||
initWithKey: @"c_name"
|
||||
operatorSelector: EOQualifierOperatorEqual
|
||||
value: contactID];
|
||||
[kvQualifier autorelease];
|
||||
[qualifiers addObject: kvQualifier];
|
||||
}
|
||||
|
||||
orQualifier = [[EOOrQualifier alloc] initWithQualifierArray: qualifiers];
|
||||
[orQualifier autorelease];
|
||||
|
||||
results = [(SOGoContactGCSFolder *) folder lookupContactsWithQualifier: orQualifier];
|
||||
max = [results count];
|
||||
headers = [NSMutableArray arrayWithCapacity: max];
|
||||
if (max > 0)
|
||||
{
|
||||
count = 0;
|
||||
fields = [[results objectAtIndex: 0] allKeys];
|
||||
[headers addObject: fields];
|
||||
while (count < max)
|
||||
{
|
||||
[headers addObject: [[results objectAtIndex: count] objectsForKeys: fields
|
||||
notFoundMarker: [NSNull null]]];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
[contactInfos release];
|
||||
contactInfos = headers;
|
||||
[contactInfos retain];
|
||||
}
|
||||
|
||||
@@ -172,6 +331,7 @@
|
||||
* @apiExample {curl} Example usage:
|
||||
* curl -i http://localhost/SOGo/so/sogo1/Contacts/personal/view?search=name_or_address\&value=Bob
|
||||
*
|
||||
* @apiParam {Boolean} [partial] Descending sort when false. Defaults to true (ascending).
|
||||
* @apiParam {Boolean} [asc] Descending sort when false. Defaults to true (ascending).
|
||||
* @apiParam {String} [sort] Sort field. Either c_cn, c_mail, c_screenname, c_o, or c_telephonenumber.
|
||||
* @apiParam {String} [search] Field criteria. Either name_or_address, category, or organization.
|
||||
@@ -201,24 +361,87 @@
|
||||
- (id <WOActionResults>) contactsListAction
|
||||
{
|
||||
id <WOActionResults> result;
|
||||
NSDictionary *data;
|
||||
NSArray *contactsList;
|
||||
id folder;
|
||||
NSMutableDictionary *data;
|
||||
NSArray *ids, *partialIds, *headers;
|
||||
NSRange range;
|
||||
NSString *partial;
|
||||
|
||||
contactsList = [self contactInfos];
|
||||
folder = [self clientObject];
|
||||
data = [NSMutableDictionary dictionaryWithObjectsAndKeys:
|
||||
[folder nameInContainer], @"id",
|
||||
[self cardDavURL], @"cardDavURL",
|
||||
[self publicCardDavURL], @"publicCardDavURL",
|
||||
nil];
|
||||
partial = [[context request] formValueForKey: @"partial"];
|
||||
|
||||
data = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[[self clientObject] nameInContainer], @"id",
|
||||
[self cardDavURL], @"cardDavURL",
|
||||
[self publicCardDavURL], @"publicCardDavURL",
|
||||
contactsList, @"cards",
|
||||
nil];
|
||||
if ([partial intValue] && [folder isKindOfClass: [SOGoContactGCSFolder class]])
|
||||
{
|
||||
// Only save sort state when performing a partial listing
|
||||
[self saveSortValue];
|
||||
|
||||
// Fetch all sorted IDs
|
||||
ids = [self sortedIDs];
|
||||
|
||||
// Fetch the first X entries
|
||||
if ([ids count] > headersPrefetchMaxSize)
|
||||
{
|
||||
range = NSMakeRange(0, headersPrefetchMaxSize);
|
||||
partialIds = [ids subarrayWithRange: range];
|
||||
}
|
||||
else
|
||||
{
|
||||
partialIds = ids;
|
||||
}
|
||||
headers = [self getHeadersForIDs: partialIds];
|
||||
|
||||
if (ids)
|
||||
[data setObject: ids forKey: @"ids"];
|
||||
if (headers)
|
||||
[data setObject: headers forKey: @"headers"];
|
||||
}
|
||||
else
|
||||
{
|
||||
headers = [self contactInfos];
|
||||
if (headers)
|
||||
[data setObject: headers forKey: @"headers"];
|
||||
}
|
||||
|
||||
result = [self responseWithStatus: 200
|
||||
andString: [data jsonRepresentation]];
|
||||
andJSONRepresentation: data];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (id <WOActionResults>) getHeadersAction
|
||||
{
|
||||
NSArray *ids, *headers;
|
||||
NSDictionary *data;
|
||||
WORequest *request;
|
||||
WOResponse *response;
|
||||
|
||||
request = [context request];
|
||||
data = [[request contentAsString] objectFromJSONString];
|
||||
if (![[data objectForKey: @"ids"] isKindOfClass: [NSArray class]] ||
|
||||
[[data objectForKey: @"ids"] count] == 0)
|
||||
{
|
||||
data = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"No ID specified", @"message", nil];
|
||||
response = [self responseWithStatus: 404 /* Not Found */
|
||||
andJSONRepresentation: data];
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
ids = [data objectForKey: @"ids"];
|
||||
headers = [self getHeadersForIDs: ids];
|
||||
|
||||
response = [self responseWithStatus: 200
|
||||
andJSONRepresentation: headers];
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
- (id <WOActionResults>) contactSearchAction
|
||||
{
|
||||
id <WOActionResults> result;
|
||||
|
||||
@@ -78,6 +78,11 @@
|
||||
actionClass = "UIxContactsListActions";
|
||||
actionName = "contactsList";
|
||||
};
|
||||
headers = {
|
||||
protectedBy = "View";
|
||||
actionClass = "UIxContactsListActions";
|
||||
actionName = "getHeaders";
|
||||
};
|
||||
// contacts = {
|
||||
// protectedBy = "View";
|
||||
// actionClass = "UIxContactsListActions";
|
||||
|
||||
@@ -356,9 +356,9 @@
|
||||
|
||||
<md-content id="contactsList" layout="column" class="md-flex">
|
||||
<md-subheader ng-show="addressbook.service.$query.value">
|
||||
<span ng-switch="addressbook.selectedFolder.cards.length">
|
||||
<span ng-switch="addressbook.selectedFolder.$cards.length">
|
||||
<span ng-switch-when="0"><var:string label:value="No matching contact"/></span>
|
||||
<span ng-switch-default="true">{{addressbook.selectedFolder.cards.length}} <var:string label:value="matching contacts"/></span>
|
||||
<span ng-switch-default="true">{{addressbook.selectedFolder.$cards.length}} <var:string label:value="matching contacts"/></span>
|
||||
</span>
|
||||
</md-subheader>
|
||||
<md-subheader ng-hide="addressbook.service.$query.value">
|
||||
@@ -367,19 +367,20 @@
|
||||
<var:string label:value="Start a search to browse this address book"/>
|
||||
</span>
|
||||
<span ng-switch-default="true">
|
||||
<span ng-switch="addressbook.selectedFolder.cards.length">
|
||||
<span ng-switch="addressbook.selectedFolder.$cards.length">
|
||||
<span ng-switch-when="0"><var:string label:value="No contact"/></span>
|
||||
<span ng-switch-default="true">{{addressbook.selectedFolder.cards.length}} <var:string label:value="contacts"/></span>
|
||||
<span ng-switch-default="true">{{addressbook.selectedFolder.$cards.length}} <var:string label:value="contacts"/></span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</md-subheader>
|
||||
<md-virtual-repeat-container class="md-flex">
|
||||
<md-virtual-repeat-container class="md-flex" md-top-index="addressbook.selectedFolder.$topIndex">
|
||||
<md-list class="sg-section-list">
|
||||
<md-list-item
|
||||
class="md-default-theme md-accent md-hue-2"
|
||||
ng-class="{'md-bg': addressbook.selectedFolder.isSelectedCard(currentCard.id)}"
|
||||
md-virtual-repeat="currentCard in addressbook.selectedFolder.cards" md-item-size="56"
|
||||
md-virtual-repeat="currentCard in addressbook.selectedFolder"
|
||||
md-on-demand="md-on-demand" md-item-size="56"
|
||||
aria-label="{{currentCard.$fullname()}}"
|
||||
ng-click="addressbook.selectCard(currentCard)">
|
||||
<div class="md-secondary"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
@@ -33,11 +33,12 @@
|
||||
* @desc The factory we'll use to register with Angular
|
||||
* @returns the AddressBook constructor
|
||||
*/
|
||||
AddressBook.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'Resource', 'Card', 'Acl', 'Preferences', function($q, $timeout, $log, Settings, Resource, Card, Acl, Preferences) {
|
||||
AddressBook.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'sgAddressBook_PRELOAD', 'Resource', 'Card', 'Acl', 'Preferences', function($q, $timeout, $log, Settings, AddressBook_PRELOAD, Resource, Card, Acl, Preferences) {
|
||||
angular.extend(AddressBook, {
|
||||
$q: $q,
|
||||
$timeout: $timeout,
|
||||
$log: $log,
|
||||
PRELOAD: AddressBook_PRELOAD,
|
||||
$$resource: new Resource(Settings.activeUser('folderURL') + 'Contacts', Settings.activeUser()),
|
||||
$Card: Card,
|
||||
$$Acl: Acl,
|
||||
@@ -68,6 +69,10 @@
|
||||
angular.module('SOGo.ContactsUI', ['SOGo.Common', 'SOGo.PreferencesUI']);
|
||||
}
|
||||
angular.module('SOGo.ContactsUI')
|
||||
.constant('sgAddressBook_PRELOAD', {
|
||||
LOOKAHEAD: 50,
|
||||
SIZE: 100
|
||||
})
|
||||
.factory('AddressBook', AddressBook.$factory);
|
||||
|
||||
/**
|
||||
@@ -212,12 +217,20 @@
|
||||
* @param {object} data - attributes of addressbook
|
||||
*/
|
||||
AddressBook.prototype.init = function(data, options) {
|
||||
if (!this.$cards) {
|
||||
this.$isLoading = true;
|
||||
this.$cards = [];
|
||||
this.cards = [];
|
||||
var _this = this;
|
||||
if (!this.$$cards) {
|
||||
// Array of cards for "dry" searches (see $filter)
|
||||
this.$$cards = [];
|
||||
}
|
||||
angular.extend(this, data);
|
||||
this.$isLoading = true;
|
||||
this.idsMap = {};
|
||||
this.$cards = []; // TODO Keep the "selected" state of cards
|
||||
// Extend instance with all attributes of data except headers
|
||||
angular.forEach(data, function(value, key) {
|
||||
if (key != 'headers' && key != 'cards') {
|
||||
_this[key] = value;
|
||||
}
|
||||
});
|
||||
// Add 'isOwned' and 'isSubscription' attributes based on active user (TODO: add it server-side?)
|
||||
this.isOwned = AddressBook.activeUser.isSuperUser || this.owner == AddressBook.activeUser.login;
|
||||
this.isSubscription = !this.isRemote && this.owner != AddressBook.activeUser.login;
|
||||
@@ -242,6 +255,100 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @function getLength
|
||||
* @memberof AddressBook.prototype
|
||||
* @desc Used by md-virtual-repeat / md-on-demand
|
||||
* @returns the number of cards in the addressbook
|
||||
*/
|
||||
AddressBook.prototype.getLength = function() {
|
||||
return this.$cards.length;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function getItemAtIndex
|
||||
* @memberof AddressBook.prototype
|
||||
* @desc Used by md-virtual-repeat / md-on-demand
|
||||
* @returns the card at the specified index
|
||||
*/
|
||||
AddressBook.prototype.getItemAtIndex = function(index) {
|
||||
var card;
|
||||
|
||||
if (!this.$isLoading && index >= 0 && index < this.$cards.length) {
|
||||
card = this.$cards[index];
|
||||
this.$lastVisibleIndex = Math.max(0, index - 3); // Magic number is NUM_EXTRA from virtual-repeater.js
|
||||
|
||||
if (this.$loadCard(card))
|
||||
return card;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function $loadCard
|
||||
* @memberof AddressBook.prototype
|
||||
* @desc Check if the card is loaded and in any case, fetch more cards headers from the server.
|
||||
* @returns true if the card metadata are already fetched
|
||||
*/
|
||||
AddressBook.prototype.$loadCard = function(card) {
|
||||
var cardId = card.id,
|
||||
startIndex = this.idsMap[cardId],
|
||||
endIndex,
|
||||
index,
|
||||
max = this.$cards.length,
|
||||
loaded = false,
|
||||
ids,
|
||||
futureHeadersData;
|
||||
|
||||
if (angular.isUndefined(this.ids) && card.id) {
|
||||
loaded = true;
|
||||
}
|
||||
else if (angular.isDefined(startIndex) && startIndex < this.$cards.length) {
|
||||
// Index is valid
|
||||
if (card.$loaded != AddressBook.$Card.STATUS.NOT_LOADED) {
|
||||
// Card headers are loaded or data is coming
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
// Preload more headers if possible
|
||||
endIndex = Math.min(startIndex + AddressBook.PRELOAD.LOOKAHEAD, max - 1);
|
||||
if (this.$cards[endIndex].$loaded != AddressBook.$Card.STATUS.NOT_LOADED) {
|
||||
index = Math.max(startIndex - AddressBook.PRELOAD.LOOKAHEAD, 0);
|
||||
if (this.$cards[index].$loaded != AddressBook.$Card.STATUS.LOADED) {
|
||||
// Previous cards not loaded; preload more headers further up
|
||||
endIndex = startIndex;
|
||||
startIndex = Math.max(startIndex - AddressBook.PRELOAD.SIZE, 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
// Next cards not load; preload more headers further down
|
||||
endIndex = Math.min(startIndex + AddressBook.PRELOAD.SIZE, max - 1);
|
||||
|
||||
if (this.$cards[startIndex].$loaded == AddressBook.$Card.STATUS.NOT_LOADED ||
|
||||
this.$cards[endIndex].$loaded == AddressBook.$Card.STATUS.NOT_LOADED) {
|
||||
|
||||
for (ids = []; startIndex < endIndex && startIndex < max; startIndex++) {
|
||||
if (this.$cards[startIndex].$loaded != AddressBook.$Card.STATUS.NOT_LOADED) {
|
||||
// Card at this index is already loaded; increase the end index
|
||||
endIndex++;
|
||||
}
|
||||
else {
|
||||
// Card at this index will be loaded
|
||||
ids.push(this.$cards[startIndex].id);
|
||||
this.$cards[startIndex].$loaded = AddressBook.$Card.STATUS.LOADING;
|
||||
}
|
||||
}
|
||||
|
||||
AddressBook.$log.debug('Loading Ids ' + ids.join(' ') + ' (' + ids.length + ' cards)');
|
||||
if (ids.length > 0) {
|
||||
futureHeadersData = AddressBook.$$resource.post(this.id, 'headers', {ids: ids});
|
||||
this.$unwrapHeaders(futureHeadersData);
|
||||
}
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
};
|
||||
|
||||
/**
|
||||
* @function isSelectedCard
|
||||
* @memberof AddressBook.prototype
|
||||
@@ -263,8 +370,8 @@
|
||||
var count;
|
||||
|
||||
count = 0;
|
||||
if (this.cards) {
|
||||
count = (_.filter(this.cards, function(card) { return card.selected; })).length;
|
||||
if (this.$cards) {
|
||||
count = (_.filter(this.$cards, function(card) { return card.selected; })).length;
|
||||
}
|
||||
return count;
|
||||
};
|
||||
@@ -312,25 +419,27 @@
|
||||
* @returns a collection of Cards instances
|
||||
*/
|
||||
AddressBook.prototype.$filter = function(search, options, excludedCards) {
|
||||
var _this = this, query;
|
||||
var _this = this, query,
|
||||
dry = options && options.dry;
|
||||
|
||||
if (!options || !options.dry) {
|
||||
this.$isLoading = true;
|
||||
query = AddressBook.$query;
|
||||
}
|
||||
else if (options.dry) {
|
||||
if (dry) {
|
||||
// Don't keep a copy of the query in dry mode
|
||||
query = angular.copy(AddressBook.$query);
|
||||
}
|
||||
else {
|
||||
this.$isLoading = true;
|
||||
query = AddressBook.$query;
|
||||
if (!this.isRemote) query.partial = 1;
|
||||
}
|
||||
|
||||
return AddressBook.$Preferences.ready().then(function() {
|
||||
if (options) {
|
||||
angular.extend(query, options);
|
||||
if (options.dry) {
|
||||
if (dry) {
|
||||
if (!search) {
|
||||
// No query specified
|
||||
_this.$cards = [];
|
||||
return AddressBook.$q.when(_this.$cards);
|
||||
_this.$$cards = [];
|
||||
return AddressBook.$q.when(_this.$$cards);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,54 +448,73 @@
|
||||
query.value = search;
|
||||
|
||||
return _this.$id().then(function(addressbookId) {
|
||||
return AddressBook.$$resource.fetch(addressbookId, 'view', query);
|
||||
}).then(function(response) {
|
||||
var results, cards, card, index,
|
||||
compareIds = function(data) {
|
||||
return _this.id == data.id;
|
||||
};
|
||||
if (options && options.dry) {
|
||||
// Don't keep a copy of the resulting cards.
|
||||
// This is usefull when doing autocompletion.
|
||||
cards = _this.$cards;
|
||||
}
|
||||
else {
|
||||
cards = _this.cards;
|
||||
}
|
||||
if (excludedCards) {
|
||||
// Remove excluded cards from results
|
||||
results = _.filter(response.cards, function(card) {
|
||||
return _.isUndefined(_.find(excludedCards, compareIds, card));
|
||||
var futureData = AddressBook.$$resource.fetch(addressbookId, 'view', query);
|
||||
|
||||
if (dry) {
|
||||
futureData.then(function(response) {
|
||||
var results, headers, card, index, fields, idFieldIndex,
|
||||
cards = _this.$$cards,
|
||||
compareIds = function(card) {
|
||||
return this == card.id;
|
||||
};
|
||||
|
||||
// First entry of 'headers' are keys
|
||||
fields = _.invoke(response.headers[0], 'toLowerCase');
|
||||
idFieldIndex = fields.indexOf('id');
|
||||
response.headers.splice(0, 1);
|
||||
|
||||
if (excludedCards)
|
||||
// Remove excluded cards from results
|
||||
results = _.filter(response.ids, function(id) {
|
||||
return _.isUndefined(_.find(excludedCards, compareIds, id));
|
||||
});
|
||||
else
|
||||
results = response.ids;
|
||||
|
||||
// Remove cards that no longer match the search query
|
||||
for (index = cards.length - 1; index >= 0; index--) {
|
||||
card = cards[index];
|
||||
if (_.isUndefined(_.find(results, compareIds, card.id))) {
|
||||
cards.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new cards matching the search query
|
||||
_.each(results, function(cardId, index) {
|
||||
if (_.isUndefined(_.find(cards, compareIds, cardId))) {
|
||||
var data = { id: cardId };
|
||||
var card = new AddressBook.$Card(data, search);
|
||||
cards.splice(index, 0, card);
|
||||
}
|
||||
});
|
||||
|
||||
// Respect the order of the results
|
||||
_.each(results, function(cardId, index) {
|
||||
var oldIndex, removedCards;
|
||||
if (cards[index].id != cardId) {
|
||||
oldIndex = _.findIndex(cards, compareIds, cardId);
|
||||
removedCards = cards.splice(oldIndex, 1);
|
||||
cards.splice(index, 0, removedCards[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Extend Card objects with received headers
|
||||
_.each(response.headers, function(data) {
|
||||
var card, index = _.findIndex(cards, compareIds, data[idFieldIndex]);
|
||||
if (index > -1) {
|
||||
card = _.object(fields, data);
|
||||
cards[index].init(card, search);
|
||||
}
|
||||
});
|
||||
|
||||
_this.$isLoading = false;
|
||||
return cards;
|
||||
});
|
||||
}
|
||||
else {
|
||||
results = response.cards;
|
||||
// Unwrap promise and instantiate or extend Cards objets
|
||||
_this.$unwrap(futureData);
|
||||
}
|
||||
// Remove cards that no longer match the search query
|
||||
for (index = cards.length - 1; index >= 0; index--) {
|
||||
card = cards[index];
|
||||
if (_.isUndefined(_.find(results, compareIds, card))) {
|
||||
cards.splice(index, 1);
|
||||
}
|
||||
}
|
||||
// Add new cards matching the search query
|
||||
_.each(results, function(data, index) {
|
||||
if (_.isUndefined(_.find(cards, compareIds, data))) {
|
||||
var card = new AddressBook.$Card(data, search);
|
||||
cards.splice(index, 0, card);
|
||||
}
|
||||
});
|
||||
// Respect the order of the results
|
||||
_.each(results, function(data, index) {
|
||||
var oldIndex, removedCards;
|
||||
if (cards[index].id != data.id) {
|
||||
oldIndex = _.findIndex(cards, compareIds, data);
|
||||
removedCards = cards.splice(oldIndex, 1);
|
||||
cards.splice(index, 0, removedCards[0]);
|
||||
}
|
||||
});
|
||||
_this.$isLoading = false;
|
||||
return cards;
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -447,7 +575,7 @@
|
||||
var _this = this;
|
||||
|
||||
return AddressBook.$$resource.post(this.id, 'batchDelete', {uids: uids}).then(function() {
|
||||
_this.cards = _.difference(_this.cards, cards);
|
||||
_this.$cards = _.difference(_this.$cards, cards);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -485,7 +613,7 @@
|
||||
|
||||
return this.$id().then(function(addressbookId) {
|
||||
var fullCard,
|
||||
cachedCard = _.find(_this.cards, function(data) {
|
||||
cachedCard = _.find(_this.$cards, function(data) {
|
||||
return cardId == data.id;
|
||||
});
|
||||
|
||||
@@ -512,22 +640,66 @@
|
||||
AddressBook.prototype.$unwrap = function(futureAddressBookData) {
|
||||
var _this = this;
|
||||
|
||||
this.$isLoading = true;
|
||||
|
||||
// Expose and resolve the promise
|
||||
this.$futureAddressBookData = futureAddressBookData.then(function(data) {
|
||||
this.$futureAddressBookData = futureAddressBookData.then(function(response) {
|
||||
return AddressBook.$timeout(function() {
|
||||
var headers;
|
||||
|
||||
if (!response.ids || _this.$topIndex > response.ids.length - 1)
|
||||
_this.$topIndex = 0;
|
||||
|
||||
// Extend AddressBook instance from data of addressbooks list.
|
||||
// Will inherit attributes such as isEditable and isRemote.
|
||||
angular.forEach(AddressBook.$findAll(), function(o, i) {
|
||||
if (o.id == data.id) {
|
||||
if (o.id == response.id) {
|
||||
angular.extend(_this, o);
|
||||
}
|
||||
});
|
||||
|
||||
// Extend AddressBook instance with received data
|
||||
_this.init(data);
|
||||
// Instanciate Card objects
|
||||
angular.forEach(_this.cards, function(o, i) {
|
||||
_this.cards[i] = new AddressBook.$Card(o);
|
||||
});
|
||||
_this.init(response);
|
||||
|
||||
if (_this.ids) {
|
||||
AddressBook.$log.debug('unwrapping ' + _this.ids.length + ' cards');
|
||||
|
||||
// Instanciate Card objects
|
||||
_.reduce(_this.ids, function(cards, card, i) {
|
||||
var data = { id: card };
|
||||
|
||||
// Build map of ID <=> index
|
||||
_this.idsMap[data.id] = i;
|
||||
|
||||
cards.push(new AddressBook.$Card(data));
|
||||
|
||||
return cards;
|
||||
}, _this.$cards);
|
||||
}
|
||||
|
||||
if (response.headers) {
|
||||
// First entry of 'headers' are keys
|
||||
headers = _.invoke(response.headers[0], 'toLowerCase');
|
||||
response.headers.splice(0, 1);
|
||||
|
||||
if (_this.ids) {
|
||||
// Extend Card objects with received headers
|
||||
_.each(response.headers, function(data) {
|
||||
var o = _.object(headers, data),
|
||||
i = _this.idsMap[o.id];
|
||||
_this.$cards[i].init(o);
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Instanciate Card objects
|
||||
_this.$cards = [];
|
||||
angular.forEach(response.headers, function(data) {
|
||||
var o = _.object(headers, data);
|
||||
_this.$cards.push(new AddressBook.$Card(o));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Instanciate Acl object
|
||||
_this.$acl = new AddressBook.$$Acl('Contacts/' + _this.id);
|
||||
|
||||
@@ -535,6 +707,8 @@
|
||||
|
||||
_this.$isLoading = false;
|
||||
|
||||
AddressBook.$log.debug('addressbook ' + _this.id + ' ready');
|
||||
|
||||
return _this;
|
||||
});
|
||||
}, function(data) {
|
||||
@@ -547,6 +721,34 @@
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @function $unwrapHeaders
|
||||
* @memberof AddressBook.prototype
|
||||
* @desc Unwrap a promise and extend matching Card objects with received data.
|
||||
* @param {promise} futureHeadersData - a promise of the metadata of some cards
|
||||
*/
|
||||
AddressBook.prototype.$unwrapHeaders = function(futureHeadersData) {
|
||||
var _this = this;
|
||||
|
||||
futureHeadersData.then(function(data) {
|
||||
AddressBook.$timeout(function() {
|
||||
var headers, j;
|
||||
if (data.length > 0) {
|
||||
// First entry of 'headers' are keys
|
||||
headers = _.invoke(data[0], 'toLowerCase');
|
||||
data.splice(0, 1);
|
||||
_.each(data, function(cardHeaders) {
|
||||
cardHeaders = _.object(headers, cardHeaders);
|
||||
j = _this.idsMap[cardHeaders.id];
|
||||
if (angular.isDefined(j)) {
|
||||
_this.$cards[j].init(cardHeaders);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @function $omit
|
||||
* @memberof AddressBook.prototype
|
||||
@@ -557,7 +759,7 @@
|
||||
var addressbook = {};
|
||||
angular.forEach(this, function(value, key) {
|
||||
if (key != 'constructor' &&
|
||||
key != 'cards' &&
|
||||
key != 'ids' &&
|
||||
key[0] != '$') {
|
||||
addressbook[key] = value;
|
||||
}
|
||||
|
||||
@@ -38,8 +38,9 @@
|
||||
* @desc The factory we'll use to register with Angular.
|
||||
* @returns the Card constructor
|
||||
*/
|
||||
Card.$factory = ['$timeout', 'sgSettings', 'Resource', 'Preferences', 'Gravatar', function($timeout, Settings, Resource, Preferences, Gravatar) {
|
||||
Card.$factory = ['$timeout', 'sgSettings', 'sgCard_STATUS', 'Resource', 'Preferences', 'Gravatar', function($timeout, Settings, Card_STATUS, Resource, Preferences, Gravatar) {
|
||||
angular.extend(Card, {
|
||||
STATUS: Card_STATUS,
|
||||
$$resource: new Resource(Settings.activeUser('folderURL') + 'Contacts', Settings.activeUser()),
|
||||
$timeout: $timeout,
|
||||
$gravatar: Gravatar,
|
||||
@@ -68,6 +69,11 @@
|
||||
angular.module('SOGo.ContactsUI', ['SOGo.Common', 'SOGo.PreferencesUI']);
|
||||
}
|
||||
angular.module('SOGo.ContactsUI')
|
||||
.constant('sgCard_STATUS', {
|
||||
NOT_LOADED: 0,
|
||||
LOADING: 1,
|
||||
LOADED: 2
|
||||
})
|
||||
.factory('Card', Card.$factory);
|
||||
|
||||
/**
|
||||
@@ -138,7 +144,9 @@
|
||||
this.$$email = this.$preferredEmail(partial);
|
||||
if (!this.$$image)
|
||||
this.$$image = this.image || Card.$gravatar(this.$preferredEmail(partial), 32, Card.$alternateAvatar, {no_404: true});
|
||||
this.selected = false;
|
||||
if (this.isgroup)
|
||||
this.c_component = 'vlist';
|
||||
this.$loaded = angular.isDefined(this.c_name)? Card.STATUS.LOADED : Card.STATUS.NOT_LOADED;
|
||||
|
||||
// An empty attribute to trick md-autocomplete when adding attendees from the appointment editor
|
||||
this.empty = ' ';
|
||||
@@ -449,6 +457,9 @@
|
||||
Card.prototype.$unwrap = function(futureCardData) {
|
||||
var _this = this;
|
||||
|
||||
// Card is not loaded yet
|
||||
this.$loaded = Card.STATUS.LOADING;
|
||||
|
||||
// Expose the promise
|
||||
this.$futureCardData = futureCardData.then(function(data) {
|
||||
_this.init(data);
|
||||
@@ -462,8 +473,11 @@
|
||||
_this.birthday = new Date(_this.birthday * 1000);
|
||||
_this.$birthday = Card.$Preferences.$mdDateLocaleProvider.formatDate(_this.birthday);
|
||||
}
|
||||
// Mark card as loaded
|
||||
_this.$loaded = Card.STATUS.LOADED;
|
||||
// Make a copy of the data for an eventual reset
|
||||
_this.$shadowData = _this.$omit(true);
|
||||
|
||||
return _this;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
vm.addPhone = addPhone;
|
||||
vm.addUrl = addUrl;
|
||||
vm.addAddress = addAddress;
|
||||
vm.addMember = addMember;
|
||||
vm.userFilter = userFilter;
|
||||
vm.save = save;
|
||||
vm.close = close;
|
||||
@@ -71,13 +70,9 @@
|
||||
var i = vm.card.$addAddress('', '', '', '', '', '', '', '');
|
||||
focus('address_' + i);
|
||||
}
|
||||
function addMember() {
|
||||
var i = vm.card.$addMember('');
|
||||
focus('ref_' + i);
|
||||
}
|
||||
function userFilter($query, excludedCards) {
|
||||
AddressBook.selectedFolder.$filter($query, {dry: true, excludeLists: true}, excludedCards);
|
||||
return AddressBook.selectedFolder.$cards;
|
||||
return AddressBook.selectedFolder.$$cards;
|
||||
}
|
||||
function save(form) {
|
||||
if (form.$valid) {
|
||||
@@ -120,7 +115,7 @@
|
||||
}
|
||||
function confirmDelete(card) {
|
||||
Dialog.confirm(l('Warning'),
|
||||
l('Are you sure you want to delete the card of %{0}?', card.$fullname()),
|
||||
l('Are you sure you want to delete the card of %{0}?', '<b>' + card.$fullname() + '</b>'),
|
||||
{ ok: l('Delete') })
|
||||
.then(function() {
|
||||
// User confirmed the deletion
|
||||
|
||||
Reference in New Issue
Block a user