1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * This file contains the contact controller class. 27 * 28 */ 29 30 /** 31 * Creates the contact controller. 32 * @class 33 * This class represents the contact controller. 34 * 35 * @param {DwtShell} container the containing shell 36 * @param {ZmApp} abApp the containing app 37 * @param {constant} type controller type 38 * @param {string} sessionId the session id 39 * 40 * @extends ZmListController 41 */ 42 ZmContactController = function(container, abApp, type, sessionId) { 43 44 ZmListController.apply(this, arguments); 45 46 this._listeners[ZmOperation.SAVE] = this._saveListener.bind(this); 47 this._listeners[ZmOperation.CANCEL] = this._cancelListener.bind(this); 48 49 this._tabGroupDone = {}; 50 this._elementsToHide = ZmAppViewMgr.LEFT_NAV; 51 }; 52 53 ZmContactController.prototype = new ZmListController(); 54 ZmContactController.prototype.constructor = ZmContactController; 55 56 ZmContactController.prototype.isZmContactController = true; 57 ZmContactController.prototype.toString = function() { return "ZmContactController"; }; 58 59 60 ZmContactController.getDefaultViewType = 61 function() { 62 return ZmId.VIEW_CONTACT; 63 }; 64 ZmContactController.prototype.getDefaultViewType = ZmContactController.getDefaultViewType; 65 66 /** 67 * Shows the contact. 68 * 69 * @param {ZmContact} contact the contact 70 * @param {Boolean} isDirty <code>true</code> to mark the contact as dirty 71 * @param {Boolean} isBack <code>true</code> in case of DL, we load (or reload) all the DL info, so we have to call back here. isBack indicates this is after the reload so we can continue. 72 */ 73 ZmContactController.prototype.show = 74 function(contact, isDirty, isBack) { 75 if (contact.id && contact.isDistributionList() && !isBack) { 76 //load the full DL info available for the owner, for edit. 77 var callback = this.show.bind(this, contact, isDirty, true); //callback HERE 78 contact.clearDlInfo(); 79 contact.gatherExtraDlStuff(callback); 80 return; 81 } 82 83 this._contact = contact; 84 if (isDirty) { 85 this._contactDirty = true; 86 } 87 this.setList(contact.list); 88 89 if (!this.getCurrentToolbar()) { 90 this._initializeToolBar(this._currentViewId); 91 } 92 this._resetOperations(this.getCurrentToolbar(), 1); // enable all buttons 93 94 this._createView(this._currentViewId); 95 96 this._setViewContents(); 97 this._initializeTabGroup(this._currentViewId); 98 this._app.pushView(this._currentViewId); 99 this.updateTabTitle(); 100 }; 101 102 ZmContactController.prototype._createView = 103 function(viewId) { 104 if (this._contactView) { 105 return; 106 } 107 var view = this._contactView = this._createContactView(); 108 //Note - I store this in this._view just to be consistent with certain calls such as for ZmBaseController.prototype._initializeTabGroup. Even though there's no real reason to keep an array of views per type since each controller would only have one view and therefor one type 109 this._view[viewId] = view; 110 111 var callbacks = {}; 112 callbacks[ZmAppViewMgr.CB_PRE_HIDE] = this._preHideCallback.bind(this); 113 callbacks[ZmAppViewMgr.CB_PRE_UNLOAD] = this._preUnloadCallback.bind(this); 114 callbacks[ZmAppViewMgr.CB_POST_SHOW] = this._postShowCallback.bind(this); 115 var elements = this.getViewElements(null, view, this._toolbar[viewId]); 116 117 this._app.createView({ viewId: viewId, 118 viewType: this._currentViewType, 119 elements: elements, 120 hide: this._elementsToHide, 121 controller: this, 122 callbacks: callbacks, 123 tabParams: this._getTabParams()}); 124 }; 125 126 ZmContactController.prototype._postShowCallback = 127 function() { 128 //have to call it since it's overridden in ZmBaseController to do nothing. 129 ZmController.prototype._postShowCallback.call(this); 130 if (this._contactView.postShow) { 131 this._contactView.postShow(); 132 } 133 }; 134 135 ZmContactController.prototype._getDefaultTabText= 136 function() { 137 return this._contact.isDistributionList() 138 ? ZmMsg.distributionList 139 : this._isGroup() 140 ? ZmMsg.group 141 : ZmMsg.contact; 142 }; 143 144 ZmContactController.prototype._getTabParams = 145 function() { 146 var text = this._isGroup() ? ZmMsg.group : ZmMsg.contact; 147 return {id:this.tabId, 148 image:"CloseGray", 149 hoverImage:"Close", 150 text: null, //we update it using updateTabTitle since before calling _setViewContents _getFullName does not return the name 151 textPrecedence:77, 152 tooltip: text, 153 style: DwtLabel.IMAGE_RIGHT}; 154 }; 155 156 ZmContactController.prototype.updateTabTitle = 157 function() { 158 var tabTitle = this._contactView._getFullName(true); 159 if (!tabTitle) { 160 tabTitle = this._getDefaultTabText(); 161 } 162 tabTitle = tabTitle.substr(0, ZmAppViewMgr.TAB_BUTTON_MAX_TEXT); 163 164 appCtxt.getAppViewMgr().setTabTitle(this._currentViewId, tabTitle); 165 }; 166 167 168 169 ZmContactController.prototype.getKeyMapName = 170 function() { 171 return ZmKeyMap.MAP_EDIT_CONTACT; 172 }; 173 174 ZmContactController.prototype.handleKeyAction = 175 function(actionCode) { 176 DBG.println("ZmContactController.handleKeyAction"); 177 switch (actionCode) { 178 179 case ZmKeyMap.SAVE: 180 var tb = this.getCurrentToolbar(); 181 var saveButton = tb.getButton(ZmOperation.SAVE); 182 if (!saveButton.getEnabled()) { 183 break; 184 } 185 this._saveListener(); 186 break; 187 188 case ZmKeyMap.CANCEL: 189 this._cancelListener(); 190 break; 191 } 192 return true; 193 }; 194 195 /** 196 * Enables the toolbar. 197 * 198 * @param {Boolean} enable <code>true</code> to enable 199 */ 200 ZmContactController.prototype.enableToolbar = 201 function(enable) { 202 if (enable) { 203 this._resetOperations(this.getCurrentToolbar(), 1); 204 } else { 205 this.getCurrentToolbar().enableAll(enable); 206 } 207 }; 208 209 // Private methods (mostly overrides of ZmListController protected methods) 210 211 /** 212 * @private 213 */ 214 ZmContactController.prototype._getToolBarOps = 215 function() { 216 return [ZmOperation.SAVE, ZmOperation.CANCEL, 217 ZmOperation.SEP, 218 ZmOperation.PRINT, ZmOperation.DELETE, 219 ZmOperation.SEP, 220 ZmOperation.TAG_MENU]; 221 }; 222 223 /** 224 * @private 225 */ 226 ZmContactController.prototype._getActionMenuOps = 227 function() { 228 return null; 229 }; 230 231 /** 232 * @private 233 */ 234 ZmContactController.prototype._isGroup = 235 function() { 236 return this._contact.isGroup(); 237 }; 238 239 240 ZmContactController.prototype._createContactView = 241 function() { 242 return this._isGroup() 243 ? new ZmGroupView(this._container, this) 244 : new ZmEditContactView(this._container, this); 245 }; 246 247 /** 248 * @private 249 */ 250 ZmContactController.prototype._initializeToolBar = 251 function(view) { 252 ZmListController.prototype._initializeToolBar.call(this, view); 253 254 var tb = this._toolbar[view]; 255 256 // change the cancel button to "close" if editing existing contact 257 var cancelButton = tb.getButton(ZmOperation.CANCEL); 258 if (this._contact.id == undefined || (this._contact.isGal && !this._contact.isDistributionList())) { 259 cancelButton.setText(ZmMsg.cancel); 260 } else { 261 cancelButton.setText(ZmMsg.close); 262 } 263 264 var saveButton = tb.getButton(ZmOperation.SAVE); 265 if (saveButton) { 266 saveButton.setToolTipContent(ZmMsg.saveContactTooltip); 267 } 268 269 appCtxt.notifyZimlets("initializeToolbar", [this._app, tb, this, view], {waitUntilLoaded:true}); 270 }; 271 272 /** 273 * @private 274 */ 275 ZmContactController.prototype._getTagMenuMsg = 276 function() { 277 return ZmMsg.AB_TAG_CONTACT; 278 }; 279 280 /** 281 * @private 282 */ 283 ZmContactController.prototype._setViewContents = 284 function() { 285 var cv = this._contactView; 286 cv.set(this._contact, this._contactDirty); 287 if (this._contactDirty) { 288 delete this._contactDirty; 289 } 290 291 }; 292 293 /** 294 * @private 295 */ 296 ZmContactController.prototype._paginate = 297 function(view, bPageForward) { 298 // TODO? - page to next/previous contact 299 }; 300 301 /** 302 * @private 303 */ 304 ZmContactController.prototype._resetOperations = 305 function(parent, num) { 306 if (!parent) return; 307 if (!this._contact.id) { 308 // disble all buttons except SAVE and CANCEL 309 parent.enableAll(false); 310 parent.enable([ZmOperation.SAVE, ZmOperation.CANCEL], true); 311 } 312 else if (this._contact.isGal) { 313 //GAL item or DL. 314 parent.enableAll(false); 315 parent.enable([ZmOperation.SAVE, ZmOperation.CANCEL], true); 316 //for editing a GAL contact - need to check special case for DLs that are owned by current user and if current user has permission to delete on this domain. 317 var deleteAllowed = ZmContactList.deleteGalItemsAllowed([this._contact]); 318 parent.enable(ZmOperation.DELETE, deleteAllowed); 319 } else if (this._contact.isReadOnly()) { 320 parent.enableAll(true); 321 parent.enable(ZmOperation.TAG_MENU, false); 322 } else { 323 ZmListController.prototype._resetOperations.call(this, parent, num); 324 } 325 }; 326 327 /** 328 * @private 329 */ 330 ZmContactController.prototype._saveListener = function(ev, bIsPopCallback) { 331 332 var fileAsChanged = false; 333 var view = this._contactView; 334 if (view instanceof DwtForm) { 335 view.validate(); 336 } 337 338 if (!view.isValid()) { 339 var invalidItems = view.getInvalidItems(); 340 // This flag will be set to false when the view.validate() detects some invalid fields (other than EMAIL) which does not have an error message. If the EMAIL field is the only invalid one, ignore the error and move on. 341 var onlyEmailInvalid = true; 342 for (var i = 0; i < invalidItems.length; i++) { 343 msg = view.getErrorMessage(invalidItems[i]); 344 var isInvalidEmailAddr = (invalidItems[i].indexOf("EMAIL") != -1); 345 if (AjxUtil.isString(msg) && !isInvalidEmailAddr) { 346 msg = msg ? AjxMessageFormat.format(ZmMsg.errorSavingWithMessage, msg) : ZmMsg.errorSaving; 347 var msgDlg = appCtxt.getMsgDialog(); 348 msgDlg.setMessage(msg, DwtMessageDialog.CRITICAL_STYLE); 349 msgDlg.popup(); 350 return; 351 } 352 onlyEmailInvalid = onlyEmailInvalid && isInvalidEmailAddr; 353 } 354 if (!onlyEmailInvalid) { 355 return; 356 } 357 } 358 359 var mods = view.getModifiedAttrs(); 360 view.enableInputs(false); 361 362 var contact = view.getContact(); 363 if (mods && AjxUtil.arraySize(mods) > 0) { 364 365 // bug fix #22041 - when moving betw. shared/local folders, dont modify 366 // the contact since it will be created/deleted into the new folder 367 var newFolderId = mods[ZmContact.F_folderId]; 368 var newFolder = newFolderId ? appCtxt.getById(newFolderId) : null; 369 if (contact.id != null && newFolderId && (contact.isShared() || (newFolder && newFolder.link)) && !contact.isGal) { 370 // update existing contact with new attrs 371 for (var a in mods) { 372 if (a != ZmContact.F_folderId && a != ZmContact.F_groups) { 373 contact.attr[a] = mods[a]; 374 } 375 } 376 // set folder will do the right thing for this shared contact 377 contact._setFolder(newFolderId); 378 } 379 else { 380 if (contact.id && (!contact.isGal || contact.isDistributionList())) { 381 if (view.isEmpty()) { //If contact empty, alert the user 382 var ed = appCtxt.getMsgDialog(); 383 ed.setMessage(ZmMsg.emptyContactSave, DwtMessageDialog.CRITICAL_STYLE); 384 ed.popup(); 385 view.enableInputs(true); 386 bIsPopCallback = true; 387 } 388 else { 389 var contactFileAsBefore = ZmContact.computeFileAs(contact), 390 contactFileAsAfter = ZmContact.computeFileAs(AjxUtil.hashUpdate(AjxUtil.hashCopy(contact.getAttrs()), mods, true)), 391 fileAsBefore = contactFileAsBefore ? contactFileAsBefore.toLowerCase()[0] : null, 392 fileAsAfter = contactFileAsAfter ? contactFileAsAfter.toLowerCase()[0] : null; 393 this._doModify(contact, mods); 394 if (fileAsBefore !== fileAsAfter) { 395 fileAsChanged = true; 396 } 397 } 398 } 399 else { 400 var isEmpty = true; 401 for (var a in mods) { 402 if (mods[a]) { 403 isEmpty = false; 404 break; 405 } 406 } 407 if (isEmpty) { 408 var msg = this._isGroup() ? ZmMsg.emptyGroup : ZmMsg.emptyContact; 409 appCtxt.setStatusMsg(msg, ZmStatusView.LEVEL_WARNING); 410 } 411 else { 412 if (contact.isDistributionList()) { 413 contact.create(mods); 414 } 415 else { 416 var clc = AjxDispatcher.run("GetContactListController"); 417 var list = (clc && clc.getList()) || new ZmContactList(null); 418 fileAsChanged = true; 419 this._doCreate(list, mods); 420 } 421 } 422 } 423 } 424 } 425 else { 426 if (contact.isDistributionList()) { 427 //in this case, we need to pop the view since we did not call the server to modify the DL. 428 this.popView(); 429 } 430 // bug fix #5829 - differentiate betw. an empty contact and saving 431 // an existing contact w/o editing 432 if (view.isEmpty()) { 433 var msg = this._isGroup() 434 ? ZmMsg.emptyGroup 435 : ZmMsg.emptyContact; 436 appCtxt.setStatusMsg(msg, ZmStatusView.LEVEL_WARNING); 437 } 438 else { 439 var msg = contact.isDistributionList() 440 ? ZmMsg.dlSaved 441 : this._isGroup() 442 ? ZmMsg.groupSaved 443 : ZmMsg.contactSaved; 444 appCtxt.setStatusMsg(msg, ZmStatusView.LEVEL_INFO); 445 } 446 } 447 448 if (!bIsPopCallback && !contact.isDistributionList()) { 449 //in the DL case it might fail so wait to pop the view when we receive success from server. 450 this.popView(); 451 } 452 else { 453 view.enableInputs(true); 454 } 455 if (fileAsChanged) // bug fix #45069 - if the contact is new, change the search to "all" instead of displaying contacts beginning with a specific letter 456 ZmContactAlphabetBar.alphabetClicked(null); 457 458 return true; 459 }; 460 461 ZmContactController.prototype.popView = 462 function() { 463 this._app.popView(true); 464 if (this._contactView) { //not sure why _contactView is undefined sometimes. Maybe it's a different instance of ZmContactController. 465 this._contactView.cleanup(); 466 } 467 }; 468 469 470 /** 471 * @private 472 */ 473 ZmContactController.prototype._cancelListener = 474 function(ev) { 475 this._app.popView(); 476 }; 477 478 /** 479 * @private 480 */ 481 ZmContactController.prototype._printListener = 482 function(ev) { 483 var url = "/h/printcontacts?id=" + this._contact.id; 484 if (appCtxt.isOffline) { 485 var acctName = this._contact.getAccount().name; 486 url+="&acct=" + acctName ; 487 } 488 window.open(appContextPath+url, "_blank"); 489 }; 490 491 /** 492 * @private 493 */ 494 ZmContactController.prototype._doDelete = 495 function(items, hardDelete, attrs, skipPostProcessing) { 496 ZmListController.prototype._doDelete.call(this, items, hardDelete, attrs); 497 if (items.isDistributionList()) { //items === this._contact here 498 //do not pop the view as we are not sure the user will confirm the hard delete 499 return; 500 } 501 appCtxt.getApp(ZmApp.CONTACTS).updateIdHash(items, true); 502 503 if (!skipPostProcessing) { 504 // disable input fields (to prevent blinking cursor from bleeding through) 505 this._contactView.enableInputs(false); 506 this._app.popView(true); 507 } 508 }; 509 510 /** 511 * @private 512 */ 513 ZmContactController.prototype._preHideCallback = 514 function(view, force) { 515 ZmController.prototype._preHideCallback.call(this); 516 517 if (force) return true; 518 519 var view = this._contactView; 520 if (!view.isDirty()) { 521 view.cleanup(); 522 return true; 523 } 524 525 var ps = this._popShield = appCtxt.getYesNoCancelMsgDialog(); 526 ps.reset(); 527 ps.setMessage(ZmMsg.askToSave, DwtMessageDialog.WARNING_STYLE); 528 ps.registerCallback(DwtDialog.YES_BUTTON, this._popShieldYesCallback, this); 529 ps.registerCallback(DwtDialog.NO_BUTTON, this._popShieldNoCallback, this); 530 ps.popup(view._getDialogXY()); 531 532 return false; 533 }; 534 535 /** 536 * @private 537 */ 538 ZmContactController.prototype._preUnloadCallback = 539 function(view) { 540 return this._contactView.clean || !this._contactView.isDirty(); 541 }; 542 543 /** 544 * @private 545 */ 546 ZmContactController.prototype._popShieldYesCallback = 547 function() { 548 this._popShield.popdown(); 549 if (this._saveListener(null, true)) { 550 this._popShieldCallback(); 551 } 552 }; 553 554 /** 555 * @private 556 */ 557 ZmContactController.prototype._popShieldNoCallback = 558 function() { 559 this._popShield.popdown(); 560 this._popShieldCallback(); 561 }; 562 563 /** 564 * @private 565 */ 566 ZmContactController.prototype._popShieldCallback = function() { 567 appCtxt.getAppViewMgr().showPendingView(true); 568 this._contactView.cleanup(); 569 }; 570 571 /** 572 * @private 573 */ 574 ZmContactController.prototype._menuPopdownActionListener = 575 function(ev) { 576 // bug fix #3719 - do nothing 577 }; 578 579 /** 580 * @private 581 */ 582 ZmContactController.prototype._getDefaultFocusItem = 583 function() { 584 return this._contactView._getDefaultFocusItem(); 585 }; 586