diff --git a/ChangeLog b/ChangeLog index 2c2b2c9a7..d09a5bc0a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,49 @@ 2010-05-05 Wolfgang Sourdeau + * UI/WebServerResources/UIxAttendeesEditor.js (onContactKeydown): + the code for the "enter" and "tab" has been merged, since it was + similar. We also resolve individual contacts from list entries. + (resolveListAttendees): new function that fetches the indivual + entries from contacts lists. + (resolveListAttendeesCallback): callback for the above, that + create rows and input fields corresponding to the returned + entries, and trigger a contact search on each of them. + (performSearch): take an "input" field as argument since + "attendeesEditor.currentField" is no longer used. + (performSearchCallbacks): roles and partstats are now stored in + individual attributes rather than in the element class. We also + now behave differently depending on whether the event owner is + returned or another type of user, in order to match Lightning's + behaviour. + (newAttendee): no longer a callback, and now takes an optional + argument that indicates with attendee row precedes the one being + created. Also the new row is returned. + (checkAttendee): no longer a callback. Now takes an "input" + argument. + (onInputBlur): new callback that invokes "checkAttendee" and + handle the cleanup of the entries menu. + (displayFreeBusyForNode): use DOM methods rather than "innerHTML" + to modify the cell contents. + (updateFreeBusyDataCallback): freebusy requests are no longer + sequential. + (prepareAttendees): we now modify the DOM attributes rather than + the HTML attributes. + + * UI/Contacts/UIxListView.m (-propertiesAction): we now invoke + -[self responseWithStatus:andJSONRepresentation:] to build the + resulting WOResponse. + + * UI/Contacts/UIxContactFoldersView.m (-allContactSearchAction): + the type of returned component can now be deduced from the value + of "c_component" rather than from the c_name extension... Also, we + avoid autoreleasing variables where it's not needed. + (-contactSearchAction): removed obsolete method. + + * SoObjects/Contacts/SOGoContactGCSFolder.m (+initialize): new + method initializing "folderListingFields" as a static NSArray + rather than as macro. Added "c_component" to the list of fields to + retrieve. + * UI/Scheduler/UIxComponentEditor.m (-ownerLogin): new accessor the differenciate between the type of users in the list of attendees. diff --git a/SOPE/sope-patchset-r1664.diff b/SOPE/sope-patchset-r1664.diff index 51a84fa99..1954e86b9 100644 --- a/SOPE/sope-patchset-r1664.diff +++ b/SOPE/sope-patchset-r1664.diff @@ -7812,16 +7812,17 @@ Index: sope-appserver/NGObjWeb/DAVPropMap.plist =================================================================== --- sope-appserver/NGObjWeb/DAVPropMap.plist (revision 1664) +++ sope-appserver/NGObjWeb/DAVPropMap.plist (working copy) -@@ -157,6 +157,8 @@ +@@ -157,6 +157,9 @@ "{urn:ietf:params:xml:ns:caldav}supported-calendar-data" = davSupportedCalendarDataTypes; "{urn:ietf:params:xml:ns:caldav}calendar-description" = davDescription; + "{urn:ietf:params:xml:ns:caldav}calendar-timezone" = davCalendarTimeZone; + "{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL" = davScheduleDefaultCalendarURL; ++ "{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp" = davScheduleCalendarTransparency; /* CardDAV */ "{urn:ietf:params:xml:ns:carddav}addressbook-home-set" = davAddressbookHomeSet; -@@ -168,6 +170,8 @@ +@@ -168,6 +171,8 @@ "{http://calendarserver.org/ns/}dropbox-home-URL" = davDropboxHomeURL; "{http://calendarserver.org/ns/}notifications-URL" = davNotificationsURL; "{http://calendarserver.org/ns/}getctag" = davCollectionTag; diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.m b/SoObjects/Appointments/SOGoAppointmentFolder.m index f78d49d70..b3947e9ee 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.m +++ b/SoObjects/Appointments/SOGoAppointmentFolder.m @@ -2285,6 +2285,11 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir return @""; } +- (NSString *) davScheduleCalendarTransparency +{ + return @"coucou"; +} + /* vevent UID handling */ - (NSString *) resourceNameForEventUID: (NSString *) uid diff --git a/SoObjects/Contacts/SOGoContactGCSFolder.m b/SoObjects/Contacts/SOGoContactGCSFolder.m index 7c75371e9..1fa3f8f9e 100644 --- a/SoObjects/Contacts/SOGoContactGCSFolder.m +++ b/SoObjects/Contacts/SOGoContactGCSFolder.m @@ -46,13 +46,20 @@ #import "SOGoContactGCSFolder.h" -#define folderListingFields [NSArray arrayWithObjects: @"c_name", @"c_cn", \ - @"c_givenname", @"c_sn", @"c_screenname", \ - @"c_o", @"c_mail", @"c_telephonenumber", \ - nil] +static NSArray *folderListingFields = nil; @implementation SOGoContactGCSFolder ++ (void) initialize +{ + if (!folderListingFields) + folderListingFields = [[NSArray alloc] initWithObjects: @"c_name", + @"c_cn", @"c_givenname", @"c_sn", + @"c_screenname", @"c_o", + @"c_mail", @"c_telephonenumber", + @"c_component", nil]; +} + - (Class) objectClassForContent: (NSString *) content { CardGroup *cardEntry; diff --git a/Tests/Integration/test-caldav-scheduling.py b/Tests/Integration/test-caldav-scheduling.py index 99b3573a8..7150bbc25 100755 --- a/Tests/Integration/test-caldav-scheduling.py +++ b/Tests/Integration/test-caldav-scheduling.py @@ -37,6 +37,45 @@ def fetchUserInfo(login): return (displayName, email_nodes[0].childNodes[0].nodeValue) +class CalDAVPropertiesTest(unittest.TestCase): + def setUp(self): + self.client = webdavlib.WebDAVClient(hostname, port, + username, password) + self.user_calendar = "/SOGo/dav/%s/Calendar/personal/" % username + + def testDavScheduleCalendarTransparency(self): + """{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp""" + propfind = webdavlib.WebDAVPROPFIND(self.user_calendar, + ["{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp"], + 0) + self.client.execute(propfind) + propfind.xpath_namespace = { "D": "DAV:", + "C": "urn:ietf:params:xml:ns:caldav" } + propstats = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/C:schedule-calendar-transp') + self.assertTrue(len(propstats) > 0, + "schedule-calendar-transp not present in response") + node = propstats[0] + status = propfind.xpath_evaluate('D:status', + node.parentNode.parentNode)[0] \ + .childNodes[0].nodeValue[9:12] + self.assertEquals(status, "200", + "schedule-calendar-transp marked as 'Not Found' in response") + values = node.childNodes + nvalues = len(values) + self.assertEquals(nvalues, 1, + "expected 1 value (%d received)" % nvalues) + value = values[0] + print node.namespaceURI + self.assertEquals(value.__class__.__name__, "Element", + "schedule-calendar-transp must be an instance of" \ + " %s, not %s" % ("Element", value.__class__.__name__)) + self.assertEquals(value.namespaceURI, "urn:ietf:params:xml:ns:caldav", + "schedule-calendar-transp must have a value in namespace '%s', not '%s'", "urn:ietf:params:xml:ns:caldav", value.namespaceURI) + self.assertTrue(value.tagName == "opaque" + or value.tagName == "transparent", + "schedule-calendar-transp must be 'opaque' or" \ + " 'transparent', not '%s'" % value.tagName) + class CalDAVITIPDelegationTest(unittest.TestCase): def setUp(self): self.client = webdavlib.WebDAVClient(hostname, port, @@ -159,7 +198,7 @@ class CalDAVITIPDelegationTest(unittest.TestCase): % (email, compared_attendees[email], attendees[email])) - def testInvitationDelegation(self): + def xtestInvitationDelegation(self): """ invitation delegation """ # the invitation must not exist diff --git a/Tests/Integration/test-webdav.py b/Tests/Integration/test-webdav.py index 2eefcbe32..538ae8970 100755 --- a/Tests/Integration/test-webdav.py +++ b/Tests/Integration/test-webdav.py @@ -19,8 +19,7 @@ class WebDAVTest(unittest.TestCase): propfind = webdavlib.WebDAVPROPFIND(resource, ["{DAV:}principal-collection-set"], 0) - propfind.xpath_namespace = { "D": "DAV:" } - self.client.execute(propfind) + propfind.xpath_namespace = { "D": "DAV:" } self.client.execute(propfind) self.assertEquals(propfind.response["status"], 207) nodes = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/D:principal-collection-set/D:href', None) diff --git a/UI/Contacts/UIxContactFoldersView.m b/UI/Contacts/UIxContactFoldersView.m index fb17c312a..525a7044a 100644 --- a/UI/Contacts/UIxContactFoldersView.m +++ b/UI/Contacts/UIxContactFoldersView.m @@ -78,8 +78,11 @@ us = [activeUser userSettings]; moduleSettings = [us objectForKey: module]; if (!moduleSettings) - moduleSettings = [NSMutableDictionary dictionary]; - [us setObject: moduleSettings forKey: module]; + { + moduleSettings = [NSMutableDictionary new]; + [us setObject: moduleSettings forKey: module]; + [moduleSettings release]; + } contextIsSetup = YES; } } @@ -149,44 +152,6 @@ return email; } -- (NSArray *) _responseForResults: (NSArray *) results -{ - NSEnumerator *contacts; - NSString *email, *info; - NSDictionary *contact; - NSMutableArray *formattedContacts; - NSMutableDictionary *formattedContact; - - formattedContacts = [NSMutableArray arrayWithCapacity: [results count]]; - if ([results count] > 0) - { - contacts = [results objectEnumerator]; - contact = [contacts nextObject]; - while (contact) - { - email = [contact objectForKey: @"c_email"]; - if ([email length]) - { - formattedContact = [NSMutableDictionary dictionary]; - [formattedContact setObject: [contact objectForKey: @"c_uid"] - forKey: @"uid"]; - [formattedContact setObject: [contact objectForKey: @"cn"] - forKey: @"name"]; - [formattedContact setObject: email - forKey: @"email"]; - info = [contact objectForKey: @"c_info"]; - if (info != nil) - [formattedContact setObject: info - forKey: @"contactInfo"]; - [formattedContacts addObject: formattedContact]; - } - contact = [contacts nextObject]; - } - } - - return formattedContacts; -} - - (id ) allContactSearchAction { id result; @@ -196,7 +161,7 @@ NSArray *folders, *contacts, *descriptors, *sortedContacts; NSMutableArray *sortedFolders; NSMutableDictionary *contact, *uniqueContacts; - unsigned int i, j; + unsigned int i, j, max; NSSortDescriptor *commonNameDescriptor; BOOL excludeGroups, excludeLists; @@ -217,9 +182,10 @@ else [localException raise]; NS_ENDHANDLER; - sortedFolders = [NSMutableArray arrayWithCapacity: [folders count]]; + max = [folders count]; + sortedFolders = [NSMutableArray arrayWithCapacity: max]; uniqueContacts = [NSMutableDictionary dictionary]; - for (i = 0; i < [folders count]; i++) + for (i = 0; i < max; i++) { folder = [folders objectAtIndex: i]; /* We first search in LDAP folders (in case of duplicated entries in GCS folders) */ @@ -228,7 +194,7 @@ else [sortedFolders addObject: folder]; } - for (i = 0; i < [sortedFolders count]; i++) + for (i = 0; i < max; i++) { folder = [sortedFolders objectAtIndex: i]; //NSLog(@" Address book: %@ (%@)", [folder displayName], [folder class]); @@ -245,8 +211,8 @@ && [uniqueContacts objectForKey: mail] == nil && !(excludeGroups && [contact objectForKey: @"isGroup"])) [uniqueContacts setObject: contact forKey: mail]; - else if (!excludeLists - && [[contact objectForKey: @"c_name"] hasSuffix: @".vlf"]) + else if (!excludeLists && [[contact objectForKey: @"c_component"] + isEqualToString: @"vlist"]) { [contact setObject: [folder nameInContainer] forKey: @"container"]; @@ -254,14 +220,16 @@ forKey: [contact objectForKey: @"c_name"]]; } } - } + } if ([uniqueContacts count] > 0) { // Sort the contacts by display name - commonNameDescriptor = [[[NSSortDescriptor alloc] initWithKey: @"c_cn" - ascending:YES] autorelease]; + commonNameDescriptor = [[NSSortDescriptor alloc] initWithKey: @"c_cn" + ascending:YES]; descriptors = [NSArray arrayWithObjects: commonNameDescriptor, nil]; - sortedContacts = [[uniqueContacts allValues] sortedArrayUsingDescriptors: descriptors]; + [commonNameDescriptor release]; + sortedContacts = [[uniqueContacts allValues] + sortedArrayUsingDescriptors: descriptors]; } else sortedContacts = [NSArray array]; @@ -269,7 +237,7 @@ sortedContacts, @"contacts", nil]; result = [self responseWithStatus: 200]; - [(WOResponse*)result appendContentString: [data jsonRepresentation]]; + [(WOResponse*) result appendContentString: [data jsonRepresentation]]; } else result = [NSException exceptionWithHTTPStatus: 400 @@ -278,36 +246,6 @@ return result; } -- (id ) contactSearchAction -{ - NSDictionary *data; - NSArray *contacts; - NSString *searchText, *domain; - id result; - SOGoUserManager *um; - - searchText = [self queryParameterForKey: @"search"]; - if ([searchText length] > 0) - { - um = [SOGoUserManager sharedUserManager]; - domain = [[context activeUser] domain]; - contacts - = [self _responseForResults: [um fetchContactsMatching: searchText - inDomain: domain]]; - data = [NSDictionary dictionaryWithObjectsAndKeys: - searchText, @"searchText", - contacts, @"contacts", - nil]; - result = [self responseWithStatus: 200]; - [(WOResponse*)result appendContentString: [data jsonRepresentation]]; - } - else - result = [NSException exceptionWithHTTPStatus: 400 - reason: @"missing 'search' parameter"]; - - return result; -} - - (NSArray *) _subFoldersFromFolder: (SOGoParentFolder *) parentFolder { NSMutableArray *folders; @@ -375,8 +313,7 @@ - (NSString *) currentContactFolderId { - return [NSString stringWithFormat: @"/%@", - [currentFolder nameInContainer]]; + return [NSString stringWithFormat: @"/%@", [currentFolder nameInContainer]]; } - (NSString *) currentContactFolderName @@ -391,7 +328,8 @@ - (NSString *) currentContactFolderClass { - return ([currentFolder isKindOfClass: [SOGoContactSourceFolder class]]? @"remote" : @"local"); + return ([currentFolder isKindOfClass: [SOGoContactSourceFolder class]] + ? @"remote" : @"local"); } - (NSString *) verticalDragHandleStyle @@ -401,7 +339,8 @@ [self _setupContext]; vertical = [moduleSettings objectForKey: @"DragHandleVertical"]; - return ((vertical && [vertical intValue] > 0) ? (id)[vertical stringByAppendingFormat: @"px"] : nil); + return ((vertical && [vertical intValue] > 0) + ? (id)[vertical stringByAppendingFormat: @"px"] : nil); } - (NSString *) horizontalDragHandleStyle @@ -411,7 +350,8 @@ [self _setupContext]; horizontal = [moduleSettings objectForKey: @"DragHandleHorizontal"]; - return ((horizontal && [horizontal intValue] > 0) ? (id)[horizontal stringByAppendingFormat: @"px"] : nil); + return ((horizontal && [horizontal intValue] > 0) + ? (id)[horizontal stringByAppendingFormat: @"px"] : nil); } - (NSString *) contactsListContentStyle @@ -421,7 +361,8 @@ [self _setupContext]; height = [moduleSettings objectForKey: @"DragHandleVertical"]; - return ((height && [height intValue] > 0) ? [NSString stringWithFormat: @"%ipx", ([height intValue] - 27)] : nil); + return ((height && [height intValue] > 0) + ? [NSString stringWithFormat: @"%ipx", ([height intValue] - 27)] : nil); } - (WOResponse *) saveDragHandleStateAction diff --git a/UI/Contacts/UIxListView.m b/UI/Contacts/UIxListView.m index 36cacca6a..c4499dfd9 100644 --- a/UI/Contacts/UIxListView.m +++ b/UI/Contacts/UIxListView.m @@ -143,29 +143,23 @@ - (WOResponse *) propertiesAction { + NSArray *references; NSMutableArray *data; NGVCardReference *card; - WOResponse *rc; - int i, count; + int count, max; - data = [NSMutableArray array]; - co = [self clientObject]; - list = [co vList]; - - count = [[list cardReferences] count]; - for (i = 0; i < count; i++) + list = [[self clientObject] vList]; + references = [list cardReferences]; + max = [references count]; + data = [NSMutableArray arrayWithCapacity: max]; + for (count = 0; count < max; count++) { - card = [[list cardReferences] objectAtIndex: i]; - [data addObject: [NSArray arrayWithObjects: [card reference], [card fn], - [card email], nil]]; + card = [references objectAtIndex: count]; + [data addObject: [NSArray arrayWithObjects: [card reference], + [card fn], [card email], nil]]; } - rc = [context response]; - [rc setHeader: @"text/plain; charset=utf-8" - forKey: @"content-type"]; - [rc appendContentString: [data jsonRepresentation]]; - - return rc; + return [self responseWithStatus: 200 andJSONRepresentation: data]; } @end diff --git a/UI/Contacts/product.plist b/UI/Contacts/product.plist index c6a05e72c..945c9a136 100644 --- a/UI/Contacts/product.plist +++ b/UI/Contacts/product.plist @@ -23,11 +23,6 @@ pageName = "UIxContactFoldersView"; actionName = "mailerContacts"; }; - contactSearch = { - protectedBy = ""; - pageName = "UIxContactFoldersView"; - actionName = "contactSearch"; - }; allContactSearch = { protectedBy = ""; pageName = "UIxContactFoldersView"; diff --git a/UI/WebServerResources/UIxAttendeesEditor.js b/UI/WebServerResources/UIxAttendeesEditor.js index 441a90e65..1730755c1 100644 --- a/UI/WebServerResources/UIxAttendeesEditor.js +++ b/UI/WebServerResources/UIxAttendeesEditor.js @@ -4,7 +4,6 @@ var OwnerLogin = ""; var resultsDiv; var address; -var awaitingFreeBusyRequests = new Array(); var additionalDays = 2; var isAllDay = parent$("isAllDay").checked + 0; @@ -13,8 +12,6 @@ var displayEndHour = 23; var attendeesEditor = { delay: 500, - delayedSearch: false, - currentField: null, selectedIndex: -1 }; @@ -32,67 +29,135 @@ function handleAllDay () { /* address completion */ +function resolveListAttendees(input, append) { + var urlstr = (UserFolderURL + + "Contacts/" + + escape(input.container) + "/" + + escape(input.cname) + "/properties"); + triggerAjaxRequest(urlstr, resolveListAttendeesCallback, + { "input": input, "append": append }); +} + +function resolveListAttendeesCallback(http) { + if (http.readyState == 4 && http.status == 200) { + var input = http.callbackData["input"]; + var append = http.callbackData["append"]; + var contacts = http.responseText.evalJSON(true); + for (var i = 0; i < contacts.length; i++) { + var contact = contacts[i]; + var fullName = contact[1]; + if (fullName && fullName.length > 0) { + fullName += " <" + contact[2] + ">"; + } + else { + fullName = contact[2]; + } + input.uid = null; + input.cname = null; + input.container = null; + input.isList = false; + input.value = contact[2]; + input.confirmedValue = null; + input.hasfreebusy = false; + input.modified = true; + // input.focussed = true; + // input.activate(); + input.checkAfterLookup = true; + performSearch(input); + if (i < (contacts.length - 1)) { + var nextRow = newAttendee(input.parentNode.parentNode); + input = nextRow.down("input"); + } else if (append) { + var row = input.parentNode.parentNode; + var tBody = row.parentNode; + if (row.rowIndex == (tBody.rows.length - 3)) { + if (input.selectText) { + input.selectText(0, 0); + } else if (input.createTextRange) { + input.createTextRange().moveStart(); + } + newAttendee(); + } else { + var nextRow = tBody.rows[row.rowIndex + 1]; + var input = nextRow.down("input"); + input.selectText(0, input.value.length); + input.focussed = true; + } + } else { + if (input.selectText) { + input.selectText(0, 0); + } else if (input.createTextRange) { + input.createTextRange().moveStart(); + } + input.blur(); + } + } + } +} + function onContactKeydown(event) { if (event.ctrlKey || event.metaKey) { this.focussed = true; return; } - if (event.keyCode == 9) { // Tab + if (event.keyCode == 9 || event.keyCode == 13) { // Tab preventDefault(event); if (this.confirmedValue) this.value = this.confirmedValue; this.hasfreebusy = false; var row = $(this).up("tr").next(); - this.blur(); // triggers checkAttendee function call - var input = row.down("input"); - if (input) { - input.focussed = true; - input.activate(); + if (this.isList) { + resolveListAttendees(this, true); + event.stop(); + } else { + checkAttendee(this); + // this.blur(); // triggers checkAttendee function call + var input = row.down("input"); + if (input) { + input.focussed = true; + input.activate(); + } + else + newAttendee(); } - else - newAttendee(null); } else if (event.keyCode == 0 || event.keyCode == 8 // Backspace || event.keyCode == 32 // Space || event.keyCode > 47) { - this.setAttribute("modified", "1"); + this.modified = true; this.confirmedValue = null; + this.cname = null; this.uid = null; + this.container = null; this.hasfreebusy = false; - attendeesEditor.currentField = this; - if (this.value.length > 0 && !attendeesEditor.delayedSearch) { - attendeesEditor.delayedSearch = true; - setTimeout("performSearch()", attendeesEditor.delay); + if (this.searchTimeout) { + window.clearTimeout(this.searchTimeout); + } + if (this.value.length > 0) { + var thisInput = this; + this.searchTimeout = setTimeout(function() + {performSearch(thisInput); + thisInput = null;}, + attendeesEditor.delay); } else if (this.value.length == 0) { if (document.currentPopupMenu) hideMenu(document.currentPopupMenu); } } - else if (event.keyCode == 13) { - preventDefault(event); - if (this.confirmedValue) - this.value = this.confirmedValue; - $(this).selectText(0, this.value.length); - if (document.currentPopupMenu) - hideMenu(document.currentPopupMenu); - attendeesEditor.selectedIndex = -1; - if (this.uid) { - this.hasfreebusy = false; - this.writeAttribute("modified", "1"); - this.blur(); // triggers checkAttendee function call - } - } else if ($('attendeesMenu').getStyle('visibility') == 'visible') { - attendeesEditor.currentField = this; if (event.keyCode == Event.KEY_UP) { // Up arrow if (attendeesEditor.selectedIndex > 0) { var attendees = $('attendeesMenu').select("li"); attendees[attendeesEditor.selectedIndex--].removeClassName("selected"); - attendees[attendeesEditor.selectedIndex].addClassName("selected"); - this.value = this.confirmedValue = attendees[attendeesEditor.selectedIndex].readAttribute("address"); - this.uid = attendees[attendeesEditor.selectedIndex].uid; + var attendee = attendees[attendeesEditor.selectedIndex]; + attendee.addClassName("selected"); + this.value = this.confirmedValue = attendee.address; + this.uid = attendee.uid; + this.isList = attendee.isList; + this.cname = attendee.cname; + this.container = attendee.container; } } else if (event.keyCode == Event.KEY_DOWN) { // Down arrow @@ -101,30 +166,27 @@ function onContactKeydown(event) { if (attendeesEditor.selectedIndex >= 0) attendees[attendeesEditor.selectedIndex].removeClassName("selected"); attendeesEditor.selectedIndex++; - attendees[attendeesEditor.selectedIndex].addClassName("selected"); - this.value = this.confirmedValue = attendees[attendeesEditor.selectedIndex].readAttribute("address"); - this.uid = attendees[attendeesEditor.selectedIndex].uid; + var attendee = attendees[attendeesEditor.selectedIndex]; + attendee.addClassName("selected"); + this.value = this.confirmedValue = attendee.address; + this.isList = attendee.isList; + this.uid = attendee.uid; + this.cname = attendee.cname; + this.container = attendee.container; } } } } -function performSearch() { +function performSearch(input) { // Perform address completion - if (attendeesEditor.currentField) { - if (document.contactLookupAjaxRequest) { - // Abort any pending request - document.contactLookupAjaxRequest.aborted = true; - document.contactLookupAjaxRequest.abort(); - } - if (attendeesEditor.currentField.value.trim().length > 0) { - var urlstr = ( UserFolderURL + "Contacts/contactSearch?search=" - + escape(attendeesEditor.currentField.value) ); - document.contactLookupAjaxRequest = - triggerAjaxRequest(urlstr, performSearchCallback, attendeesEditor.currentField); - } + if (input.value.trim().length > 0) { + var urlstr = (UserFolderURL + + "Contacts/allContactSearch?excludeGroups=1&search=" + + escape(input.value)); + triggerAjaxRequest(urlstr, performSearchCallback, input); } - attendeesEditor.delayedSearch = false; + input.searchTimeout = null; } function performSearchCallback(http) { @@ -139,6 +201,7 @@ function performSearchCallback(http) { var data = http.responseText.evalJSON(true); if (data.contacts.length > 1) { + list.input = input; $(list.childNodesWithTag("li")).each(function(item) { item.remove(); }); @@ -146,25 +209,42 @@ function performSearchCallback(http) { // Populate popup menu for (var i = 0; i < data.contacts.length; i++) { var contact = data.contacts[i]; - var completeEmail = contact["name"] + " <" + contact["email"] + ">"; - var node = new Element('li', { 'address': completeEmail }); + var isList = (contact["c_component"] && + contact["c_component"] == "vlist"); + var completeEmail = contact["c_cn"].trim(); + if (!isList) { + if (completeEmail) + completeEmail += " <" + contact["c_mail"] + ">"; + else + completeEmail = contact["c_mail"]; + } + var node = createElement('li'); + list.appendChild(node); + node.address = completeEmail; + log("node.address: " + node.address); + node.uid = contact["c_uid"]; + node.isList = isList; + if (isList) { + node.cname = contact["c_name"]; + node.container = contact["container"]; + } var matchPosition = completeEmail.toLowerCase().indexOf(data.searchText.toLowerCase()); var matchBefore = completeEmail.substring(0, matchPosition); var matchText = completeEmail.substring(matchPosition, matchPosition + data.searchText.length); var matchAfter = completeEmail.substring(matchPosition + data.searchText.length); - list.appendChild(node); - node.uid = contact["uid"]; node.appendChild(document.createTextNode(matchBefore)); node.appendChild(new Element('strong').update(matchText)); node.appendChild(document.createTextNode(matchAfter)); if (contact["contactInfo"]) - node.appendChild(document.createTextNode(" (" + contact["contactInfo"] + ")")); - $(node).observe("mousedown", onAttendeeResultClick); + node.appendChild(document.createTextNode(" (" + + contact["contactInfo"] + ")")); + node.observe("mousedown", + onAttendeeResultClick.bindAsEventListener(node)); } // Show popup menu - var offsetScroll = Element.cumulativeScrollOffset(attendeesEditor.currentField); - var offset = Element.cumulativeOffset(attendeesEditor.currentField); + var offsetScroll = Element.cumulativeScrollOffset(input); + var offset = Element.cumulativeOffset(input); var top = offset[1] - offsetScroll[1] + node.offsetHeight + 3; var height = 'auto'; var heightDiff = window.height() - offset[1]; @@ -190,52 +270,73 @@ function performSearchCallback(http) { if (data.contacts.length == 1) { // Single result var contact = data.contacts[0]; - if (contact["uid"].length > 0) { - var row = $(input.parentNode.parentNode); - input.uid = contact["uid"]; - if (input.uid == OwnerLogin) { - row.removeAttribute("role"); - row.setAttribute("partstat", "accepted"); - row.addClassName("organizer-row"); - row.removeClassName("attendee-row"); - row.isOrganizer = true; - } else { - row.removeAttribute("partstat"); - row.setAttribute("role", "req-participant"); - row.addClassName("attendee-row"); - row.removeClassName("organizer-row"); - row.isOrganizer = false; - } + input.uid = contact["c_uid"]; + var row = $(input.parentNode.parentNode); + if (input.uid == OwnerLogin) { + row.removeAttribute("role"); + row.setAttribute("partstat", "accepted"); + row.addClassName("organizer-row"); + row.removeClassName("attendee-row"); + row.isOrganizer = true; + } else { + row.removeAttribute("partstat"); + row.setAttribute("role", "req-participant"); + row.addClassName("attendee-row"); + row.removeClassName("organizer-row"); + row.isOrganizer = false; } - var completeEmail = contact["name"] + " <" + contact["email"] + ">"; - if (contact["name"].substring(0, input.value.length).toUpperCase() - == input.value.toUpperCase()) + var isList = (contact["c_component"] && + contact["c_component"] == "vlist"); + if (isList) { + input.cname = contact["c_name"]; + input.container = contact["container"]; + } + var completeEmail = contact["c_cn"].trim(); + if (!isList) { + if (completeEmail) + completeEmail += " <" + contact["c_mail"] + ">"; + else + completeEmail = contact["c_mail"]; + } + if ((input.value == contact["c_mail"]) + || (contact["c_cn"].substring(0, input.value.length).toUpperCase() + == input.value.toUpperCase())) { input.value = completeEmail; + } else // The result matches email address, not user name input.value += ' >> ' + completeEmail; + input.isList = isList; input.confirmedValue = completeEmail; var end = input.value.length; $(input).selectText(start, end); attendeesEditor.selectedIndex = -1; + + if (input.checkAfterLookup) { + input.checkAfterLookup = false; + input.modified = true; + input.hasfreebusy = false; + checkAttendee(input); + } } } } else if (document.currentPopupMenu) hideMenu(document.currentPopupMenu); - document.contactLookupAjaxRequest = null; } } function onAttendeeResultClick(event) { - if (attendeesEditor.currentField) { - attendeesEditor.currentField.uid = this.uid; - attendeesEditor.currentField.value = $(this).readAttribute("address"); - attendeesEditor.currentField.confirmedValue = attendeesEditor.currentField.value; - attendeesEditor.currentField.blur(); // triggers checkAttendee function call - } + var input = this.parentNode.input; + input.uid = this.uid; + input.cname = this.cname; + input.container = this.container; + input.isList = this.isList; + input.confirmedValue = input.value = this.address; + checkAttendee(input); + this.parentNode.input = null; } function resetFreeBusyZone() { @@ -331,13 +432,23 @@ function rotateAttendeeStatus(row) { } } -function newAttendee(event) { +function onNewAttendeeClick(event) { + newAttendee(); + event.stop(); +} + +function newAttendee(previousAttendee) { var table = $("freeBusyAttendees"); var tbody = table.tBodies[0]; var model = tbody.rows[tbody.rows.length - 1]; - var futureRow = tbody.rows[tbody.rows.length - 2]; + var nextRowIndex = tbody.rows.length - 2; + if (previousAttendee) { + nextRowIndex = previousAttendee.rowIndex + 1; + } + var nextRow = tbody.rows[nextRowIndex]; var newRow = $(model.cloneNode(true)); - tbody.insertBefore(newRow, futureRow); + tbody.insertBefore(newRow, nextRow); + var result = newRow; var statusTD = newRow.down(".attendeeStatus"); if (statusTD) { @@ -348,8 +459,8 @@ function newAttendee(event) { $(newRow).removeClassName("attendeeModel"); var input = newRow.down("input"); - input.observe("keydown", onContactKeydown); - input.observe("blur", checkAttendee); + input.observe("keydown", onContactKeydown.bindAsEventListener(input)); + input.observe("blur", onInputBlur); input.focussed = true; input.activate(); @@ -357,49 +468,64 @@ function newAttendee(event) { table = $("freeBusyData"); tbody = table.tBodies[0]; model = tbody.rows[tbody.rows.length - 1]; - futureRow = tbody.rows[tbody.rows.length - 2]; + nextRow = tbody.rows[nextRowIndex]; newRow = $(model.cloneNode(true)); - tbody.insertBefore(newRow, futureRow); + tbody.insertBefore(newRow, nextRow); newRow.removeClassName("dataModel"); var attendeesDiv = $$('TABLE#freeBusy TD.freeBusyAttendees DIV').first(); var dataDiv = $$('TABLE#freeBusy TD.freeBusyData DIV').first(); dataDiv.scrollTop = attendeesDiv.scrollTop; + + return result; } -function checkAttendee() { // log ("checkAttendee"); - if (document.currentPopupMenu) - hideMenu(document.currentPopupMenu); - - if (document.currentPopupMenu && !this.confirmedValue) { - // Hack for IE7; blur event is triggered on input field when - // selecting a menu item - var visible = $(document.currentPopupMenu).getStyle('visibility') != 'hidden'; - if (visible) - return; - } - - this.focussed = false; - var row = this.parentNode.parentNode; +function checkAttendee(input) { + var row = $(input.parentNode.parentNode); var tbody = row.parentNode; - if (tbody && this.value.trim().length == 0) { + if (tbody && input.value.trim().length == 0) { var dataTable = $("freeBusyData").tBodies[0]; var dataRow = dataTable.rows[row.sectionRowIndex]; tbody.removeChild(row); dataTable.removeChild(dataRow); } - else if (this.readAttribute("modified") == "1") { - if (!this.hasfreebusy) { - if (this.uid && this.confirmedValue) - this.value = this.confirmedValue; - displayFreeBusyForNode(this); - this.hasfreebusy = true; + else if (input.modified) { + if (!row.hasClassName("needs-action")) { + row.addClassName("needs-action"); + row.removeClassName("declined"); + row.removeClassName("accepted"); } - this.setAttribute("modified", "0"); + if (!input.hasfreebusy) { + if (input.uid && input.confirmedValue) { + input.value = input.confirmedValue; + } + displayFreeBusyForNode(input); + input.hasfreebusy = true; + } + input.modified = false; + } +} + +function onInputBlur(event) { + if (document.currentPopupMenu && !this.confirmedValue) { + // Hack for IE7; blur event is triggered on input field when + // selecting a menu item + var visible = $(document.currentPopupMenu).getStyle('visibility') != 'hidden'; + if (visible) { + log("XXX we return"); + return; + } + } + + if (document.currentPopupMenu) + hideMenu(document.currentPopupMenu); + + if (this.isList) { + resolveListAttendees(this, false); + } else { + checkAttendee(this); } - - attendeesEditor.currentField = null; } function displayFreeBusyForNode(input) { @@ -407,36 +533,33 @@ function displayFreeBusyForNode(input) { var nodes = $("freeBusyData").tBodies[0].rows[rowIndex].cells; log ("displayFreeBusyForNode index " + rowIndex + " (" + nodes.length + " cells)"); if (input.uid) { - if (document.contactFreeBusyAjaxRequest) { log ("busy -- delay " + rowIndex); - awaitingFreeBusyRequests.push(input); } - else { - for (var i = 0; i < nodes.length; i++) { - $(nodes[i]).removeClassName("noFreeBusy"); - $(nodes[i]).innerHTML = ('' - + '' - + '' - + ''); + for (var i = 0; i < nodes.length; i++) { + var node = $(nodes[i]); + node.removeClassName("noFreeBusy"); + while (node.firstChild) { + node.removeChild(node.firstChild); + } + for (var j = 0; j < 4; j++) { + createElement("span", null, "freeBusyZoneElement", + null, null, node); } -// if (document.contactFreeBusyAjaxRequest) { -// // Abort any pending request -// document.contactFreeBusyAjaxRequest.aborted = true; -// document.contactFreeBusyAjaxRequest.abort(); -// } - var sd = $('startTime_date').valueAsShortDateString(); - var ed = $('endTime_date').valueAsShortDateString(); - var urlstr = ( UserFolderURL + "../" + input.uid - + "/freebusy.ifb/ajaxRead?" - + "sday=" + sd + "&eday=" + ed + "&additional=" + - additionalDays ); log (urlstr); - document.contactFreeBusyAjaxRequest - = triggerAjaxRequest(urlstr, - updateFreeBusyDataCallback, - input); } + var sd = $('startTime_date').valueAsShortDateString(); + var ed = $('endTime_date').valueAsShortDateString(); + var urlstr = (UserFolderURL + "../" + input.uid + + "/freebusy.ifb/ajaxRead?" + + "sday=" + sd + "&eday=" + ed + "&additional=" + + additionalDays); + triggerAjaxRequest(urlstr, + updateFreeBusyDataCallback, + input); } else { for (var i = 0; i < nodes.length; i++) { - $(nodes[i]).addClassName("noFreeBusy"); - $(nodes[i]).update(); + var node = $(nodes[i]); + node.addClassName("noFreeBusy"); + while (node.firstChild) { + node.removeChild(node.firstChild); + } } } } @@ -467,15 +590,12 @@ function updateFreeBusyDataCallback(http) { var slots = http.responseText.split(","); var rowIndex = input.parentNode.parentNode.sectionRowIndex; var nodes = $("freeBusyData").tBodies[0].rows[rowIndex].cells; - log ("received " + slots.length + " slots for " + rowIndex + " with " + nodes.length + " cells"); + // log ("received " + slots.length + " slots for " + rowIndex + " with " + nodes.length + " cells"); for (var i = 0; i < slots.length; i++) { if (slots[i] != '0') setSlot(nodes, i, slots[i]); } } - document.contactFreeBusyAjaxRequest = null; - if (awaitingFreeBusyRequests.length > 0) { log ("1"); - displayFreeBusyForNode(awaitingFreeBusyRequests.shift()); } } } @@ -484,8 +604,8 @@ function resetAllFreeBusys() { var inputs = table.getElementsByTagName("input"); for (var i = 0; i < inputs.length - 1; i++) { - var currentInput = inputs[i]; log ("reset fb " + currentInput.uid); - currentInput.hasfreebusy = false; log ("2"); + var currentInput = inputs[i]; + currentInput.hasfreebusy = false; displayFreeBusyForNode(currentInput); } } @@ -626,8 +746,8 @@ function onEditorOkClick(event) { if (inputs[i].uid) uid = inputs[i].uid; if (!(name && name.length > 0)) - if (inputs[i].uid) - name = inputs[i].uid; + if (uid.length > 0) + name = uid; else name = email; var attendee = attendees["email"]; @@ -791,7 +911,6 @@ function prepareAttendees() { attendees.keys().each(function(atKey) { var attendee = attendees.get(atKey); - // attendee = $H(attendee); var row = $(modelAttendee.cloneNode(true)); tbodyAttendees.insertBefore(row, newAttendeeRow); row.removeClassName("attendeeModel"); @@ -822,13 +941,14 @@ function prepareAttendees() { value = ""; value += "<" + attendee["email"] + ">"; input.value = value; - if (uid) - input.uid = uid; + input.uid = attendee["uid"]; + input.cname = attendee["cname"]; input.setAttribute("name", ""); - input.setAttribute("modified", "0"); - input.observe("blur", checkAttendee); - input.observe("keydown", onContactKeydown); - + input.modified = false; + input.observe("blur", onInputBlur); + input.observe("keydown", onContactKeydown.bindAsEventListener(input) +); + row = $(modelData.cloneNode(true)); tbodyData.insertBefore(row, newDataRow); row.removeClassName("dataModel"); @@ -838,7 +958,7 @@ function prepareAttendees() { // Activate "Add attendee" button var links = tableAttendees.select("TR.futureAttendee TD A"); - links.first().observe("click", newAttendee); + links.first().observe("click", onNewAttendeeClick); } function onWindowResize(event) { @@ -934,7 +1054,7 @@ function initTimeWidgets(widgets) { function onAdjustTime(event) { var endDate = window.getEndDate(); var startDate = window.getStartDate(); - if ($(this).readAttribute("id").startsWith("start")) { + if (this.id.startsWith("start")) { // Start date was changed var delta = window.getShadowStartDate().valueOf() - startDate.valueOf();