diff --git a/ChangeLog b/ChangeLog index 6156ad7ea..234ebfc9b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,19 @@ +2008-07-15 Francis Lachapelle + + * UI/Contacts/UIxContactFolderActions.m: new class for actions + on contacts of a specific address book. + + * UI/Contacts/UIxContactFolderActions.m + ([_moveContacts:toFolder:andKeepCopy:]): private method to + move and optionnaly copy one or many contacts to another + address book. + + * UI/Contacts/UIxContactFolderActions.m ([WOActionResults + copyAction]): copy one or many contacts to another address book. + + * UI/Contacts/UIxContactFolderActions.m ([WOActionResults + moveAction]): move one or many contacts to another address book. + 2008-07-14 Wolfgang Sourdeau * SoObjects/SOGo/SOGoContentObject.m ([SOGoContentObject diff --git a/NEWS b/NEWS index 6d2476ce0..42b644972 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,8 @@ - added address completion in the web mail editor - implemented support for CalDAV methods which were missing for supporting iCal 3 - added support to write to multiple contacts from the Address Book module +- added support to move and copy one or many contacts to another +address book in the Address Book module - added icons to folders in Address Book module - fixed various bugs occuring with Safari 3.1 - fixed various bugs occuring with Firefox 3 diff --git a/UI/Contacts/English.lproj/Localizable.strings b/UI/Contacts/English.lproj/Localizable.strings index 0ec4a7408..9d61eb57d 100644 --- a/UI/Contacts/English.lproj/Localizable.strings +++ b/UI/Contacts/English.lproj/Localizable.strings @@ -133,3 +133,10 @@ = "The selected contact has no email address."; "Please select a contact." = "Please select a contact."; + +/* Error messages for move and copy */ + +"SoAccessDeniedException" = "You cannot write to this address book."; +"Forbidden" = "You cannot write to this address book."; +"Invalid Contact" = "The selected contact no longer exists."; +"Unknown Destination Folder" = "The chosen destination address book no longer exists."; \ No newline at end of file diff --git a/UI/Contacts/French.lproj/Localizable.strings b/UI/Contacts/French.lproj/Localizable.strings index 157703669..6c8cfd8b4 100644 --- a/UI/Contacts/French.lproj/Localizable.strings +++ b/UI/Contacts/French.lproj/Localizable.strings @@ -145,3 +145,10 @@ = "Cette personne n'a pas d'adresse courriel."; "Please select a contact." = "Veuillez sélectionner un contact.."; + +/* Error messages for move and copy */ + +"SoAccessDeniedException" = "Vous ne pouvez pas ajouter de fiches à ce carnet."; +"Forbidden" = "Vous ne pouvez pas ajouter de fiches à ce carnet."; +"Invalid Contact" = "La fiche sélectionnée n'existe plus."; +"Unknown Destination Folder" = "Le carnet d'adresses choisi n'existe plus."; \ No newline at end of file diff --git a/UI/Contacts/UIxContactFolderActions.h b/UI/Contacts/UIxContactFolderActions.h index 1321163d4..0a915e0f6 100644 --- a/UI/Contacts/UIxContactFolderActions.h +++ b/UI/Contacts/UIxContactFolderActions.h @@ -31,8 +31,8 @@ { } -- (WOResponse *) copyAction; -- (WOResponse *) moveAction; +- (id ) copyAction; +- (id ) moveAction; @end diff --git a/UI/Contacts/UIxContactFolderActions.m b/UI/Contacts/UIxContactFolderActions.m index 4baad2d9c..91444c0f5 100644 --- a/UI/Contacts/UIxContactFolderActions.m +++ b/UI/Contacts/UIxContactFolderActions.m @@ -24,6 +24,9 @@ #import #import +#import +#import +#import #import #import #import @@ -46,83 +49,157 @@ @implementation UIxContactFolderActions -- (id) init +- (NSException*) _moveContacts: (NSArray*) contactsId + toFolder: (NSString*) destinationFolderId + andKeepCopy: (BOOL) keepCopy { - if ((self = [super init])) - { - } - - return self; -} - -- (void) dealloc -{ - [super dealloc]; -} - -- (WOResponse *) copyAction -{ - WORequest *request; - NSArray *contactsId; - NSEnumerator *uids; - NSString *folderId, *uid, *newUid; - SOGoContactFolders *folders; - SOGoParentFolder *folder, *destinationFolder; - id contact; - SOGoContentObject *newContact; NGVCard *card; + NSEnumerator *uids; NSException *ex; + NSString *uid, *newUid; + id contact; + SOGoContactFolders *folders; + SOGoParentFolder *sourceFolder, *destinationFolder; + SOGoContentObject *newContact; + SoSecurityManager *sm; + WORequest *request; unsigned int errorCount; + sm = [SoSecurityManager sharedSecurityManager]; request = [context request]; + ex = nil; errorCount = 0; - if ((folderId = [request formValueForKey: @"folder"]) && - (contactsId = [request formValuesForKey: @"uid"])) + // Search the specified destination folder + sourceFolder = [self clientObject]; + folders = [(SOGoUserFolder*)[[sourceFolder container] container] privateContacts: @"Contacts" + inContext: nil]; + destinationFolder = [folders lookupName: destinationFolderId + inContext: nil + acquire: NO]; + if (destinationFolder) { - // Search the specified destination folder - folder = [self clientObject]; - folders = [(SOGoUserFolder*)[[folder container] container] privateContacts: @"Contacts" - inContext: nil]; - destinationFolder = [folders lookupName: folderId - inContext: nil - acquire: NO]; - if (destinationFolder) + // Verify write access to the folder + ex = [sm validatePermission: SoPerm_AddDocumentsImagesAndFiles + onObject: destinationFolder + inContext: context]; + if (ex == nil) { - uids = [contactsId objectEnumerator]; + uids = [contactsId objectEnumerator]; uid = [uids nextObject]; - + while (uid) { - contact = [folder lookupName: uid - inContext: [self context] - acquire: NO]; - if (![(NSObject*)contact isKindOfClass: [NSException class]]) + // Search the contact ID + contact = [sourceFolder lookupName: uid + inContext: [self context] + acquire: NO]; + if ([(NSObject*)contact isKindOfClass: [NSException class]]) + errorCount++; + else { - newUid = [SOGoObject globallyUniqueObjectId]; card = [contact vCard]; - [card setUid: newUid]; - - //[contact setContent: [card versitString]]; - //[contact copyTo: destinationFolder]; // verify if exception - newContact = [SOGoContentObject objectWithName: newUid - inContainer: destinationFolder]; + if (keepCopy) + { + // Change the contact UID + newUid = [SOGoObject globallyUniqueObjectId]; + [card setUid: newUid]; + newContact = [SOGoContentObject objectWithName: newUid + inContainer: destinationFolder]; + } + else + // Don't change the contact UID + newContact = [SOGoContentObject objectWithName: [contact nameInContainer] + inContainer: (SOGoGCSFolder*)destinationFolder]; + ex = [newContact saveContentString: [card versitString]]; + + if (ex == nil && !keepCopy) + // Delete the original contact if necessary + ex = [contact delete]; if (ex != nil) errorCount++; - } + } uid = [uids nextObject]; } } } + else + ex = [NSException exceptionWithName: @"UnkownDestinationFolder" + reason: @"Unknown Destination Folder" + userInfo: nil]; - return [self responseWithStatus: 204]; + if (errorCount > 0) + // At least one contact was not copied + ex = [NSException exceptionWithHTTPStatus: 400 + reason: @"Invalid Contact"]; + else if (ex != nil) + // Destination address book doesn't exist or is not writable + ex = [NSException exceptionWithHTTPStatus: 403 + reason: [ex name]]; + + return ex; } -- (WOResponse *) moveAction +- (id ) copyAction { - return [self responseWithStatus: 204]; + WORequest *request; + id response; + NSString *destinationFolderId; + NSArray *contactsId; + NSException *ex; + + request = [context request]; + ex = nil; + + if ((destinationFolderId = [request formValueForKey: @"folder"]) && + (contactsId = [request formValuesForKey: @"uid"])) + { + ex = [self _moveContacts: contactsId + toFolder: destinationFolderId + andKeepCopy: YES]; + if (ex != nil) + response = (id)ex; + } + else + response = [NSException exceptionWithHTTPStatus: 400 + reason: @"missing 'folder' and/or 'uid' parameter"]; + + if (ex == nil) + response = [self responseWith204]; + + return response; +} + +- (id ) moveAction +{ + WORequest *request; + id response; + NSString *destinationFolderId; + NSArray *contactsId; + NSException *ex; + + request = [context request]; + ex = nil; + + if ((destinationFolderId = [request formValueForKey: @"folder"]) && + (contactsId = [request formValuesForKey: @"uid"])) + { + ex = [self _moveContacts: contactsId + toFolder: destinationFolderId + andKeepCopy: NO]; + if (ex != nil) + response = (id)ex; + } + else + response = [NSException exceptionWithHTTPStatus: 400 + reason: @"missing 'folder' and/or 'uid' parameter"]; + + if (ex == nil) + response = [self responseWith204]; + + return response; } @end diff --git a/UI/Contacts/product.plist b/UI/Contacts/product.plist index 5c04b6ff3..7cd004d77 100644 --- a/UI/Contacts/product.plist +++ b/UI/Contacts/product.plist @@ -97,12 +97,12 @@ actionName = "saveUserRights"; }; copy = { - protectedBy = "View"; + protectedBy = "Access Contents Information"; actionClass = "UIxContactFolderActions"; actionName = "copy"; }; move = { - protectedBy = "View"; + protectedBy = "Delete Objects"; actionClass = "UIxContactFolderActions"; actionName = "move"; }; @@ -137,15 +137,10 @@ actionName = "canAccessContent"; }; copy = { - protectedBy = ""; + protectedBy = "Access Contents Information"; actionClass = "UIxContactFolderActions"; actionName = "copy"; }; - move = { - protectedBy = ""; - actionClass = "UIxContactFolderActions"; - actionName = "move"; - }; }; }; diff --git a/UI/WebServerResources/ContactsUI.js b/UI/WebServerResources/ContactsUI.js index f8c2a1ce3..e6745d55f 100644 --- a/UI/WebServerResources/ContactsUI.js +++ b/UI/WebServerResources/ContactsUI.js @@ -185,9 +185,11 @@ function onAddressBooksContextMenu(event) { } function onContactContextMenu(event) { + var contactsList = $("contactsList"); var menu = $("contactMenu"); menu.observe("hideMenu", onContactContextMenuHide); - popupMenu(event, "contactMenu", this); + if (contactsList) + popupMenu(event, "contactMenu", contactsList.getSelectedRows()); } function onContactContextMenuHide(event) { @@ -231,6 +233,50 @@ function onFolderMenuHide(event) { topNode.selectedEntry.selectElement(); } +function _onContactMenuAction(folderItem, action, refresh) { + var selectedFolders = $("contactFolders").getSelectedNodes(); + var folderId = $(folderItem).readAttribute("folderId").substring(1); + if (Object.isArray(document.menuTarget) && selectedFolders.length > 0) { + var selectedFolderId = $(selectedFolders[0]).readAttribute("id"); + var contactIds = $(document.menuTarget).collect(function(row) { + return row.getAttribute("id"); + }); + + var url = ApplicationBaseURL + selectedFolderId + "/" + action + "?folder=" + folderId + "&uid=" + + contactIds.join("&uid="); + + if (refresh) + triggerAjaxRequest(url, actionContactCallback, selectedFolderId); + else + triggerAjaxRequest(url, actionContactCallback); + } +} + +function onContactMenuCopy(event) { + _onContactMenuAction(this, "copy", false); +} + +function onContactMenuMove(event) { + _onContactMenuAction(this, "move", true); +} + +function actionContactCallback(http) { + if (http.readyState == 4) + if (isHttpStatus204(http.status)) { + var refreshFolderId = http.callbackData; + if (refreshFolderId) + openContactsFolder(refreshFolderId, true); + } + else { + var html = new Element("div").update(http.responseText); + var error = html.select("p").first().firstChild.nodeValue.trim(); + log("actionContactCallback failed: error " + http.status + " (" + error + ")"); + if (error) + window.alert(labels[error]); + refreshCurrentFolder(); + } +} + function loadContact(idx) { if (document.contactAjaxRequest) { document.contactAjaxRequest.aborted = true; @@ -392,7 +438,7 @@ function onToolbarDeleteSelectedContacts(event) { new Ajax.Request(url, { method: 'post', onFailure: function(transport) { - log("Ajax error: can't delete contact"); + window.alert(labels["You cannot delete the selected contact(s)"]); refreshCurrentFolder(); }, onSuccess: function(transport) { @@ -555,10 +601,12 @@ function appendAddressBook(name, folder) { li.setAttribute("id", folder); li.setAttribute("owner", owner); + li.addClassName("local"); li.appendChild(document.createTextNode(name .replace("<", "<", "g") .replace(">", ">", "g"))); setEventsOnAddressBook(li); + updateAddressBooksMenus(); } return result; @@ -720,6 +768,59 @@ function configureAddressBooks() { } } +function onAddressBookMenuPrepareVisibility() { + var selectedFolder = $("contactFolders").getSelectedNodes()[0]; + if (selectedFolder) { + var selectedFolderId = selectedFolder.readAttribute("id"); + $(this).select("li").each(function(menuEntry) { + if (menuEntry.readAttribute("folderId") == selectedFolderId) + menuEntry.addClassName("disabled"); + else + menuEntry.removeClassName("disabled"); + }); + } +} + +function updateAddressBooksMenus() { + var contactFoldersList = $("contactFolders"); + if (contactFoldersList) { + var contactFolders = contactFoldersList.select("li"); + var contactActions = new Hash({ move: onContactMenuMove, + copy: onContactMenuCopy }); + contactActions.keys().each(function(key) { + var callbacks = new Array(); + var menuId = key + "ContactMenu"; + var menuDIV = $(menuId); + if (menuDIV) + menuDIV.parentNode.removeChild(menuDIV); + + menuDIV = document.createElement("div"); + pageContent = $("pageContent"); + pageContent.appendChild(menuDIV); + + var menu = document.createElement("ul"); + menuDIV.appendChild(menu); + + $(menuDIV).addClassName("menu"); + menuDIV.setAttribute("id", menuId); + + var submenuIds = new Array(); + for (var i = 0; i < contactFolders.length; i++) { + if (contactFolders[i].hasClassName("local")) { + var menuEntry = new Element("li", + { folderId: contactFolders[i].readAttribute("id"), + owner: contactFolders[i].readAttribute("owner") } + ).update(contactFolders[i].innerHTML); + menu.appendChild(menuEntry); + callbacks.push(contactActions.get(key)); + } + } + menuDIV.prepareVisibility = onAddressBookMenuPrepareVisibility; + initMenu(menuDIV, callbacks); + }); + } +} + function setEventsOnAddressBook(folder) { var node = $(folder); @@ -797,23 +898,42 @@ function onAddressBooksMenuPrepareVisibility() { } function onContactMenuPrepareVisibility() { - var contactId = document.menuTarget.getAttribute('id'); - var contactRow = $(contactId); - var elements = $(this).down("ul").childElements(); + var contactRows = document.menuTarget; + var selectedFolder = $("contactFolders").getSelectedNodes()[0]; + var options = { write: false, + aim: false }; + var elements = $(this).down("ul").childElements(); var writeOption = elements[2]; - var emailCell = contactRow.down('td', 1); - if (emailCell.firstChild) + var aimOption = elements[3]; + var deleteOption = elements[5]; + var moveOption = elements[7]; + + $A(contactRows).each(function(contactRow) { + var emailCell = contactRow.down('td', 1); + options.write |= (emailCell.firstChild != null); + var aimCell = contactRow.down('td', 2); + options.aim |= (aimCell.firstChild != null); + }); + + if (options.write) writeOption.removeClassName("disabled"); else writeOption.addClassName("disabled"); - - var aimOption = elements[3]; - var aimCell = contactRow.down('td', 2); - if (aimCell.firstChild) + if (options.aim) aimOption.removeClassName("disabled"); else aimOption.addClassName("disabled"); + + if ($(selectedFolder).hasClassName("remote")) { + // Remote address books are always read-only + deleteOption.addClassName("disabled"); + moveOption.addClassName("disabled"); + } + else { + deleteOption.removeClassName("disabled"); + moveOption.removeClassName("disabled"); + } } function getMenus() { @@ -823,7 +943,8 @@ function getMenus() { onMenuSharing); menus["contactMenu"] = new Array(onMenuEditContact, "-", onMenuWriteToContact, onMenuAIMContact, - "-", onMenuDeleteContact); + "-", onMenuDeleteContact, "-", + "moveContactMenu", "copyContactMenu"); menus["searchMenu"] = new Array(setSearchCriteria); var contactFoldersMenu = $("contactFoldersMenu"); @@ -850,6 +971,7 @@ function initContacts(event) { configureSelectionButtons(); configureAbToolbar(); configureAddressBooks(); + updateAddressBooksMenus(); // initDnd(); var table = $("contactsList"); diff --git a/UI/WebServerResources/generic.js b/UI/WebServerResources/generic.js index 7982a14f1..e4de1289e 100644 --- a/UI/WebServerResources/generic.js +++ b/UI/WebServerResources/generic.js @@ -780,7 +780,7 @@ function backtrace() { } function popupSubmenu(event) { - if (this.submenu && this.submenu != "") { + if (this.submenu && this.submenu != "" && !$(this).hasClassName("disabled")) { var submenuNode = $(this.submenu); var parentNode = getParentMenu(this); if (parentNode.submenu)