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 defines a list of items. 27 */ 28 29 /** 30 * Creates an empty list of items of the given type. 31 * @class 32 * This class represents a list of items ({@link ZmItem} objects). Any SOAP method that can be 33 * applied to a list of item IDs is represented here, so that we can perform an action 34 * on multiple items with just one CSFE call. For the sake of convenience, a hash 35 * matching item IDs to items is maintained. Items are assumed to have an 'id' 36 * property. 37 * <br/> 38 * <br/> 39 * The calls are made asynchronously. We are assuming that any action taken will result 40 * in a notification, so the action methods generally do not have an async callback 41 * chain and thus are leaf nodes. An exception is moving conversations. We don't 42 * know enough from the ensuing notifications (which only indicate that messages have 43 * moved), we need to update the UI based on the response. 44 * 45 * @author Conrad Damon 46 * 47 * @param {constant} type the item type 48 * @param {ZmSearch} search the search that generated this list 49 * 50 * @extends ZmModel 51 */ 52 ZmList = function(type, search) { 53 54 if (arguments.length == 0) return; 55 ZmModel.call(this, type); 56 57 this.type = type; 58 this.search = search; 59 60 this._vector = new AjxVector(); 61 this._hasMore = false; 62 this._idHash = {}; 63 64 var tagList = appCtxt.getTagTree(); 65 if (tagList) { 66 this._tagChangeListener = new AjxListener(this, this._tagTreeChangeListener); 67 tagList.addChangeListener(this._tagChangeListener); 68 } 69 70 this.id = "LIST" + ZmList.NEXT++; 71 appCtxt.cacheSet(this.id, this); 72 }; 73 74 ZmList.prototype = new ZmModel; 75 ZmList.prototype.constructor = ZmList; 76 77 ZmList.prototype.isZmList = true; 78 ZmList.prototype.toString = function() { return "ZmList"; }; 79 80 81 ZmList.NEXT = 1; 82 83 // for item creation 84 ZmList.ITEM_CLASS = {}; 85 86 // node names for item types 87 ZmList.NODE = {}; 88 89 // item types based on node name (reverse map of above) 90 ZmList.ITEM_TYPE = {}; 91 92 ZmList.CHUNK_SIZE = 100; // how many items to act on at a time via a server request 93 ZmList.CHUNK_PAUSE = 500; // how long to pause to allow UI to catch up 94 95 96 /** 97 * Gets the item. 98 * 99 * @param {int} index the index 100 * @return {ZmItem} the index 101 */ 102 ZmList.prototype.get = 103 function(index) { 104 return this._vector.get(index); 105 }; 106 107 /** 108 * Adds an item to the list. 109 * 110 * @param {ZmItem} item the item to add 111 * @param {int} index the index at which to add the item (defaults to end of list) 112 */ 113 ZmList.prototype.add = 114 function(item, index) { 115 this._vector.add(item, index); 116 if (item.id) { 117 this._idHash[item.id] = item; 118 } 119 }; 120 121 /** 122 * Removes an item from the list. 123 * 124 * @param {ZmItem} item the item to remove 125 */ 126 ZmList.prototype.remove = 127 function(item) { 128 this._vector.remove(item); 129 if (item.id) { 130 delete this._idHash[item.id]; 131 } 132 }; 133 134 /** 135 * Creates an item from the given arguments. A subclass may override 136 * <code>sortIndex()</code> to add it to a particular point in the list. By default, it 137 * will be added at the end. 138 * 139 * <p> 140 * The item will invoke a SOAP call, which generates a create notification from the 141 * server. That will be handled by notifyCreate(), which will call _notify() 142 * so that views can be updated. 143 * </p> 144 * 145 * @param {Hash} args a hash of arugments to pass along to the item constructor 146 * @return {ZmItem} the newly created item 147 */ 148 ZmList.prototype.create = 149 function(args) { 150 var item; 151 var obj = eval(ZmList.ITEM_CLASS[this.type]); 152 if (obj) { 153 item = new obj(this); 154 item.create(args); 155 } 156 157 return item; 158 }; 159 160 /** 161 * Returns the number of items in the list. 162 * 163 * @return {int} the number of items 164 */ 165 ZmList.prototype.size = 166 function() { 167 return this._vector.size(); 168 }; 169 170 /** 171 * Returns the index of the given item in the list. 172 * 173 * @param {ZmItem} item the item 174 * @return {int} the index 175 */ 176 ZmList.prototype.indexOf = 177 function(item) { 178 return this._vector.indexOf(item); 179 }; 180 181 /** 182 * Gets if there are more items for this search. 183 * 184 * @return {Boolean} <code>true</code> if there are more items 185 */ 186 ZmList.prototype.hasMore = 187 function() { 188 return this._hasMore; 189 }; 190 191 /** 192 * Sets the "more" flag for this list. 193 * 194 * @param {Boolean} bHasMore <code>true</code> if there are more items 195 */ 196 ZmList.prototype.setHasMore = 197 function(bHasMore) { 198 this._hasMore = bHasMore; 199 }; 200 201 /** 202 * Returns the list as an array. 203 * 204 * @return {Array} an array of {ZmItem} objects 205 */ 206 ZmList.prototype.getArray = 207 function() { 208 return this._vector.getArray(); 209 }; 210 211 /** 212 * Returns the list as a vector. 213 * 214 * @return {AjxVector} a vector of {ZmItem} objects 215 */ 216 ZmList.prototype.getVector = 217 function() { 218 return this._vector; 219 }; 220 221 /** 222 * Gets the item with the given id. 223 * 224 * @param {String} id an item id 225 * 226 * @return {ZmItem} the item 227 */ 228 ZmList.prototype.getById = 229 function(id) { 230 return this._idHash[id]; 231 }; 232 233 /** 234 * Clears the list, including the id hash. 235 * 236 */ 237 ZmList.prototype.clear = 238 function() { 239 // First, let each item run its clear() method 240 var a = this.getArray(); 241 for (var i = 0; i < a.length; i++) { 242 a[i].clear(); 243 } 244 245 this._evtMgr.removeAll(ZmEvent.L_MODIFY); 246 this._vector.removeAll(); 247 for (var id in this._idHash) { 248 this._idHash[id] = null; 249 } 250 this._idHash = {}; 251 }; 252 253 /** 254 * Populates the list with elements created from the response to a SOAP command. Each 255 * node in the response should represent an item of the list's type. Items are added 256 * in the order they are received; no sorting is done. 257 * 258 * @param {Object} respNode an XML node whose children are item nodes 259 */ 260 ZmList.prototype.set = 261 function(respNode) { 262 this.clear(); 263 var nodes = respNode.childNodes; 264 var args = {list:this}; 265 for (var i = 0; i < nodes.length; i++) { 266 var node = nodes[i]; 267 if (node.nodeName == ZmList.NODE[this.type]) { 268 /// TODO: take this out, let view decide whether to show items in Trash 269 if (parseInt(node.getAttribute("l")) == ZmFolder.ID_TRASH && (this.type != ZmItem.CONTACT)) { continue; } 270 var obj = eval(ZmList.ITEM_CLASS[this.type]); 271 if (obj) { 272 this.add(obj.createFromDom(node, args)); 273 } 274 } 275 } 276 }; 277 278 /** 279 * Adds an item to the list from the given XML node. 280 * 281 * @param {Object} node an XML node 282 * @param {Hash} args an optional list of arguments to pass to the item contructor 283 */ 284 ZmList.prototype.addFromDom = 285 function(node, args) { 286 args = args || {}; 287 args.list = this; 288 var obj = eval(ZmList.ITEM_CLASS[this.type]); 289 if (obj) { 290 this.add(obj.createFromDom(node, args)); 291 } 292 }; 293 294 /** 295 * Gets a vector containing a subset of items of this list. 296 * 297 * @param {int} offset the starting index 298 * @param {int} limit the size of sublist 299 * @return {AjxVector} the vector 300 */ 301 ZmList.prototype.getSubList = 302 function(offset, limit) { 303 var subVector = null; 304 var end = (offset + limit > this.size()) ? this.size() : offset + limit; 305 var subList = this.getArray(); 306 if (offset < end) { 307 subVector = AjxVector.fromArray(subList.slice(offset, end)); 308 } 309 return subVector; 310 }; 311 312 /** 313 * Caches the list. 314 * 315 * @param {int} offset the index 316 * @param {AjxVector} newList the new list 317 */ 318 ZmList.prototype.cache = 319 function(offset, newList) { 320 this.getVector().merge(offset, newList); 321 // reparent each item within new list, and add it to ID hash 322 var list = newList.getArray(); 323 for (var i = 0; i < list.length; i++) { 324 var item = list[i]; 325 item.list = this; 326 if (item.id) { 327 this._idHash[item.id] = item; 328 } 329 } 330 }; 331 332 // Actions 333 334 /** 335 * Sets and unsets a flag for each of a list of items. 336 * 337 * @param {Hash} params a hash of parameters 338 * @param {Array} params.items a list of items to set/unset a flag for 339 * @param {String} params.op the name of the flag operation ("flag" or "read") 340 * @param {Boolean|String} params.value whether to set the flag, or for "update" the flags string 341 * @param {AjxCallback} params.callback the callback to run after each sub-request 342 * @param {closure} params.finalCallback the callback to run after all items have been processed 343 * @param {int} params.count the starting count for number of items processed 344 * @param {String} params.actionTextKey pattern for generating action summarykey to action summary message 345 */ 346 ZmList.prototype.flagItems = 347 function(params) { 348 349 params = Dwt.getParams(arguments, ["items", "op", "value", "callback"]); 350 351 params.items = AjxUtil.toArray(params.items); 352 353 if (params.op == "update") { 354 params.action = params.op; 355 params.attrs = {f:params.value}; 356 } else { 357 params.action = params.value ? params.op : "!" + params.op; 358 } 359 360 if (appCtxt.multiAccounts) { 361 // check if we're flagging item from remote folder, in which case, always send 362 // request on-behalf-of the account the item originally belongs to. 363 var folderId = this.search.folderId; 364 var fromFolder = folderId && appCtxt.getById(folderId); 365 if (fromFolder && fromFolder.isRemote()) { 366 params.accountName = params.items[0].getAccount().name; 367 } 368 } 369 370 this._itemAction(params); 371 }; 372 373 /** 374 * Tags or untags a list of items. A sanity check is done first, so that items 375 * aren't tagged redundantly, and so we don't try to remove a nonexistent tag. 376 * 377 * @param {Hash} params a hash of parameters 378 * @param {Array} params.items a list of items to tag/untag 379 * @param {String} params.tagId ID of tag to add/remove 380 * @param {String} params.tag the tag to add/remove from each item (optional) 381 * @param {Boolean} params.doTag <code>true</code> if adding the tag, <code>false</code> if removing it 382 * @param {AjxCallback} params.callback the callback to run after each sub-request 383 * @param {closure} params.finalCallback the callback to run after all items have been processed 384 * @param {int} params.count the starting count for number of items processed 385 */ 386 ZmList.prototype.tagItems = 387 function(params) { 388 389 params = Dwt.getParams(arguments, ["items", "tagId", "doTag"]); 390 391 var tagName = params.tagName || (params.tag && params.tag.name); 392 393 //todo - i hope this is no longer needed. I think the item we apply the tag to should determine the tag id on the server side. 394 // // for multi-account mbox, normalize tagId 395 // if (appCtxt.multiAccounts && !appCtxt.getActiveAccount().isMain) { 396 // tagId = ZmOrganizer.normalizeId(tagId); 397 // } 398 399 // only tag items that don't have the tag, and untag ones that do 400 // always tag a conv, because we don't know if all items in the conv have the tag yet 401 var items = AjxUtil.toArray(params.items); 402 var items1 = [], doTag = params.doTag; 403 if (items[0] && items[0] instanceof ZmItem) { 404 for (var i = 0; i < items.length; i++) { 405 var item = items[i]; 406 if ((doTag && (!item.hasTag(tagName) || item.type == ZmItem.CONV)) || (!doTag && item.hasTag(tagName))) { 407 items1.push(item); 408 } 409 } 410 } else { 411 items1 = items; 412 } 413 params.items = items1; 414 params.attrs = {tn: tagName}; 415 params.action = doTag ? "tag" : "!tag"; 416 params.actionTextKey = doTag ? 'actionTag' : 'actionUntag'; 417 params.actionArg = params.tag && params.tag.name; 418 419 this._itemAction(params); 420 }; 421 422 /** 423 * Removes all tags from a list of items. 424 * 425 * @param {Hash} params a hash of parameters 426 * @param {Array} params.items a list of items to tag/untag 427 * @param {AjxCallback} params.callback the callback to run after each sub-request 428 * @param {closure} params.finalCallback the callback to run after all items have been processed 429 * @param {int} params.count the starting count for number of items processed 430 */ 431 ZmList.prototype.removeAllTags = 432 function(params) { 433 434 params = (params && params.items) ? params : {items:params}; 435 436 var items = AjxUtil.toArray(params.items); 437 var items1 = []; 438 if (items[0] && items[0] instanceof ZmItem) { 439 for (var i = 0; i < items.length; i++) { 440 var item = items[i]; 441 if (item.tags && item.tags.length) { 442 items1.push(item); 443 } 444 } 445 } else { 446 items1 = items; 447 } 448 449 params.items = items1; 450 params.action = "update"; 451 params.attrs = {t: ""}; 452 params.actionTextKey = 'actionRemoveTags'; 453 454 this._itemAction(params); 455 }; 456 457 /** 458 * Moves a list of items to the given folder. 459 * <p> 460 * Search results are treated as though they're in a temporary folder, so that they behave as 461 * they would if they were in any other folder such as Inbox. When items that are part of search 462 * results are moved, they will disappear from the view, even though they may still satisfy the 463 * search. 464 * </p> 465 * 466 * @param {Hash} params a hash of parameters 467 * @param {Array} params.items a list of items to move 468 * @param {ZmFolder} params.folder the destination folder 469 * @param {Hash} params.attrs the additional attrs for SOAP command 470 * @param {AjxCallback} params.callback the callback to run after each sub-request 471 * @param {closure} params.finalCallback the callback to run after all items have been processed 472 * @param {int} params.count the starting count for number of items processed 473 * @param {boolean} params.noUndo true if the action is not undoable (e.g. performed as an undo) 474 */ 475 ZmList.prototype.moveItems = 476 function(params) { 477 478 params = Dwt.getParams(arguments, ["items", "folder", "attrs", "callback", "errorCallback" ,"finalCallback", "noUndo"]); 479 480 var params1 = AjxUtil.hashCopy(params); 481 params1.items = AjxUtil.toArray(params.items); 482 params1.attrs = params.attrs || {}; 483 params1.childWin = params.childWin; 484 params1.closeChildWin = params.closeChildWin; 485 486 if (params1.folder.id == ZmFolder.ID_TRASH) { 487 params1.actionTextKey = 'actionTrash'; 488 params1.action = "trash"; 489 } else { 490 params1.actionTextKey = 'actionMove'; 491 params1.actionArg = params.folder.getName(false, false, true); 492 params1.action = "move"; 493 params1.attrs.l = params.folder.id; 494 } 495 params1.callback = new AjxCallback(this, this._handleResponseMoveItems, [params]); 496 if (params.noToast) { 497 params1.actionTextKey = null; 498 } 499 500 if (appCtxt.multiAccounts) { 501 // Reset accountName for multi-account to be the respective account if we're 502 // moving a draft out of Trash. 503 // OR, 504 // check if we're moving to or from a shared folder, in which case, always send 505 // request on-behalf-of the account the item originally belongs to. 506 507 var folderId = params.items[0].getFolderId && params.items[0].getFolderId(); 508 509 // on bulk delete, when the second chunk loads try to get folderId from the item id. 510 if (!folderId) { 511 var itemId = params.items[0] && params.items[0].id; 512 folderId = itemId && appCtxt.getById(itemId) && appCtxt.getById(itemId).folderId; 513 } 514 var fromFolder = appCtxt.getById(folderId); 515 if ((params.items[0].isDraft && params.folder.id == ZmFolder.ID_DRAFTS) || 516 (params.folder.isRemote()) || (fromFolder && fromFolder.isRemote())) 517 { 518 params1.accountName = params.items[0].getAccount().name; 519 } 520 } 521 //Error Callback 522 params1.errorCallback = params.errorCallback; 523 524 if (this._handleDeleteFromSharedFolder(params, params1)) { 525 return; 526 } 527 528 this._itemAction(params1); 529 }; 530 531 ZmList.prototype._handleDeleteFromSharedFolder = 532 function(params, params1) { 533 534 // Bug 26103: when deleting an item in a folder shared to us, save a copy in our own trash 535 if (params.folder && params.folder.id == ZmFolder.ID_TRASH) { 536 var fromFolder; 537 var toCopy = []; 538 for (var i = 0; i < params.items.length; i++) { 539 var item = params.items[i]; 540 var index = item.id.indexOf(":"); 541 if (index != -1) { //might be shared 542 var acctId = item.id.substring(0, index); 543 if (!appCtxt.accountList.getAccount(acctId)) { 544 fromFolder = appCtxt.getById(item.folderId); 545 // Don't do the copy if the source folder is shared with view only rights 546 if (fromFolder && !fromFolder.isReadOnly()) { 547 toCopy.push(item); 548 } 549 } 550 } 551 } 552 if (toCopy.length) { 553 var params2 = { 554 items: toCopy, 555 folder: params.folder, // Should refer to our own trash folder 556 finalCallback: this._itemAction.bind(this, params1, null), 557 actionTextKey: null 558 }; 559 this.copyItems(params2); 560 return true; 561 } 562 } 563 }; 564 565 /** 566 * @private 567 */ 568 ZmList.prototype._handleResponseMoveItems = 569 function(params, result) { 570 571 var movedItems = result.getResponse(); 572 if (movedItems && movedItems.length && (movedItems[0] instanceof ZmItem)) { 573 this.moveLocal(movedItems, params.folder.id); 574 for (var i = 0; i < movedItems.length; i++) { 575 var item = movedItems[i]; 576 var details = {oldFolderId:item.folderId}; 577 item.moveLocal(params.folder.id); 578 //ZmModel.prototype._notify.call(item, ZmEvent.E_MOVE, details); 579 } 580 // batched change notification 581 //todo - it's probably possible that different items have different _lists they are in 582 // thus getting the lists just from the first item is not enough. But hopefully good 583 // enough for the most common cases. Prior to this fix it was only taking the current list 584 // the first item is in, so this is already better. :) 585 var item = movedItems[0]; 586 for (var listId in item._list) { 587 var ac = window.parentAppCtxt || appCtxt; //always get the list in the parent window. The child might be closed or closing, causing bugs. 588 var list = ac.getById(listId); 589 if (!list) { 590 continue; 591 } 592 list._evt.batchMode = true; 593 list._evt.item = item; // placeholder 594 list._evt.items = movedItems; 595 list._notify(ZmEvent.E_MOVE, details); 596 } 597 } 598 599 if (params.callback) { 600 params.callback.run(result); 601 } 602 }; 603 604 /** 605 * Copies a list of items to the given folder. 606 * 607 * @param {Hash} params the hash of parameters 608 * @param {Array} params.items a list of items to move 609 * @param {ZmFolder} params.folder the destination folder 610 * @param {Hash} params.attrs the additional attrs for SOAP command 611 * @param {closure} params.finalCallback the callback to run after all items have been processed 612 * @param {int} params.count the starting count for number of items processed 613 * @param {String} params.actionTextKey key to optional text to display in the confirmation toast instead of the default summary. May be set explicitly to null to disable the confirmation toast 614 */ 615 ZmList.prototype.copyItems = 616 function(params) { 617 618 params = Dwt.getParams(arguments, ["items", "folder", "attrs", "actionTextKey"]); 619 620 params.items = AjxUtil.toArray(params.items); 621 params.attrs = params.attrs || {}; 622 if (!appCtxt.isExternalAccount()) { 623 params.attrs.l = params.folder.id; 624 params.action = "copy"; 625 params.actionTextKey = 'itemCopied'; 626 } 627 else { 628 params.action = 'trash'; 629 } 630 params.actionArg = params.folder.getName(false, false, true); 631 params.callback = new AjxCallback(this, this._handleResponseCopyItems, params); 632 633 if (appCtxt.multiAccounts && params.folder.isRemote()) { 634 params.accountName = params.items[0].getAccount().name; 635 } 636 637 this._itemAction(params); 638 }; 639 640 /** 641 * @private 642 */ 643 ZmList.prototype._handleResponseCopyItems = 644 function(params, result) { 645 var resp = result.getResponse(); 646 if (resp.length > 0) { 647 if (params.actionTextKey) { 648 var msg = AjxMessageFormat.format(ZmMsg[params.actionTextKey], resp.length); 649 appCtxt.getAppController().setStatusMsg(msg); 650 } 651 } 652 }; 653 654 /** 655 * Deletes one or more items from the list. Normally, deleting an item just 656 * moves it to the Trash (soft delete). However, if it's already in the Trash, 657 * it will be removed from the data store (hard delete). 658 * 659 * @param {Hash} params a hash of parameters 660 * @param {Array} params.items list of items to delete 661 * @param {Boolean} params.hardDelete <code>true</code> to force physical removal of items 662 * @param {Object} params.attrs additional attrs for SOAP command 663 * @param {window} params.childWin the child window this action is happening in 664 * @param {closure} params.finalCallback the callback to run after all items have been processed 665 * @param {int} params.count the starting count for number of items processed 666 * @param {Boolean} params.confirmDelete the user confirmed hard delete 667 */ 668 ZmList.prototype.deleteItems = 669 function(params) { 670 671 params = Dwt.getParams(arguments, ["items", "hardDelete", "attrs", "childWin"]); 672 673 var items = params.items = AjxUtil.toArray(params.items); 674 675 // figure out which items should be moved to Trash, and which should actually be deleted 676 var toMove = []; 677 var toDelete = []; 678 if (params.hardDelete) { 679 toDelete = items; 680 } else if (items[0] && items[0] instanceof ZmItem) { 681 for (var i = 0; i < items.length; i++) { 682 var item = items[i]; 683 var folderId = item.getFolderId(); 684 var folder = appCtxt.getById(folderId); 685 if (folder && folder.isHardDelete()) { 686 toDelete.push(item); 687 } else { 688 toMove.push(item); 689 } 690 } 691 } else { 692 toMove = items; 693 } 694 695 if (toDelete.length && !params.confirmDelete) { 696 params.confirmDelete = true; 697 var callback = ZmList.prototype.deleteItems.bind(this, params); 698 this._popupDeleteWarningDialog(callback, toMove.length, toDelete.length); 699 return; 700 } 701 702 params.callback = params.childWin && new AjxCallback(this._handleDeleteNewWindowResponse, params.childWin); 703 704 // soft delete - items moved to Trash 705 if (toMove.length) { 706 if (appCtxt.multiAccounts) { 707 var accounts = this._filterItemsByAccount(toMove); 708 if (!params.callback) { 709 params.callback = new AjxCallback(this, this._deleteAccountItems, [accounts, params]); 710 } 711 this._deleteAccountItems(accounts, params); 712 } 713 else { 714 params.items = toMove; 715 params.folder = appCtxt.getById(ZmFolder.ID_TRASH); 716 this.moveItems(params); 717 } 718 } 719 720 // hard delete - items actually deleted from data store 721 if (toDelete.length) { 722 params.items = toDelete; 723 params.action = "delete"; 724 params.actionTextKey = 'actionDelete'; 725 this._itemAction(params); 726 } 727 }; 728 729 730 ZmList.prototype._popupDeleteWarningDialog = 731 function(callback, onlySome, count) { 732 var dialog = appCtxt.getOkCancelMsgDialog(); 733 dialog.reset(); 734 dialog.setMessage(AjxMessageFormat.format(ZmMsg[onlySome ? "confirmDeleteSomeForever" : "confirmDeleteForever"], [count]), DwtMessageDialog.WARNING_STYLE); 735 dialog.registerCallback(DwtDialog.OK_BUTTON, this._deleteWarningDialogListener.bind(this, callback, dialog)); 736 dialog.associateEnterWithButton(DwtDialog.OK_BUTTON); 737 dialog.popup(null, DwtDialog.OK_BUTTON); 738 }; 739 740 ZmList.prototype._deleteWarningDialogListener = 741 function(callback, dialog) { 742 dialog.popdown(); 743 callback(); 744 }; 745 746 747 /** 748 * @private 749 */ 750 ZmList.prototype._deleteAccountItems = 751 function(accounts, params) { 752 var items; 753 for (var i in accounts) { 754 items = accounts[i]; 755 break; 756 } 757 758 if (items) { 759 delete accounts[i]; 760 761 var ac = window.parentAppCtxt || window.appCtxt; 762 params.accountName = ac.accountList.getAccount(i).name; 763 params.items = items; 764 params.folder = appCtxt.getById(ZmFolder.ID_TRASH); 765 766 this.moveItems(params); 767 } 768 }; 769 770 /** 771 * @private 772 */ 773 ZmList.prototype._filterItemsByAccount = 774 function(items) { 775 // separate out the items based on which account they belong to 776 var accounts = {}; 777 if (items[0] && items[0] instanceof ZmItem) { 778 for (var i = 0; i < items.length; i++) { 779 var item = items[i]; 780 var acctId = item.getAccount().id; 781 if (!accounts[acctId]) { 782 accounts[acctId] = []; 783 } 784 accounts[acctId].push(item); 785 } 786 } else { 787 var id = appCtxt.accountList.mainAccount.id; 788 accounts[id] = items; 789 } 790 791 return accounts; 792 }; 793 794 /** 795 * @private 796 */ 797 ZmList.prototype._handleDeleteNewWindowResponse = 798 function(childWin, result) { 799 if (childWin) { 800 childWin.close(); 801 } 802 }; 803 804 /** 805 * Applies the given list of modifications to the item. 806 * 807 * @param {ZmItem} item the item to modify 808 * @param {Hash} mods hash of new properties 809 * @param {AjxCallback} callback the callback 810 */ 811 ZmList.prototype.modifyItem = 812 function(item, mods, callback) { 813 item.modify(mods, callback); 814 }; 815 816 // Notification handling 817 818 /** 819 * Create notification. 820 * 821 * @param {Object} node not used 822 */ 823 ZmList.prototype.notifyCreate = 824 function(node) { 825 var obj = eval(ZmList.ITEM_CLASS[this.type]); 826 if (obj) { 827 var item = obj.createFromDom(node, {list:this}); 828 this.add(item, this._sortIndex(item)); 829 this.createLocal(item); 830 this._notify(ZmEvent.E_CREATE, {items: [item]}); 831 } 832 }; 833 834 // Local change handling 835 836 // These generic methods allow a derived class to perform the appropriate internal changes 837 838 /** 839 * Modifies the items (local). 840 * 841 * @param {Array} items an array of items 842 * @param {Object} mods a hash of properties to modify 843 */ 844 ZmList.prototype.modifyLocal = function(items, mods) {}; 845 846 /** 847 * Creates the item (local). 848 * 849 * @param {ZmItem} item the item to create 850 */ 851 ZmList.prototype.createLocal = function(item) {}; 852 853 // These are not currently used; will need support in ZmItem if they are. 854 ZmList.prototype.flagLocal = function(items, flag, state) {}; 855 ZmList.prototype.tagLocal = function(items, tag, state) {}; 856 ZmList.prototype.removeAllTagsLocal = function(items) {}; 857 858 // default action is to remove each deleted item from this list 859 /** 860 * Deletes the items (local). 861 * 862 * @param {Array} items an array of items 863 */ 864 ZmList.prototype.deleteLocal = 865 function(items) { 866 for (var i = 0; i < items.length; i++) { 867 this.remove(items[i]); 868 } 869 }; 870 871 // default action is to remove each moved item from this list 872 /** 873 * Moves the items (local). 874 * 875 * @param {Array} items an array of items 876 * @param {String} folderId the folder id 877 */ 878 ZmList.prototype.moveLocal = 879 function(items, folderId) { 880 for (var i = 0; i < items.length; i++) { 881 this.remove(items[i]); 882 } 883 }; 884 885 /** 886 * Performs an action on items via a SOAP request. 887 * 888 * @param {Hash} params a hash of parameters 889 * @param {Array} params.items a list of items to act upon 890 * @param {String} params.action the SOAP operation 891 * @param {Object} params.attrs a hash of additional attrs for SOAP request 892 * @param {AjxCallback} params.callback the async callback 893 * @param {closure} params.finalCallback the callback to run after all items have been processed 894 * @param {AjxCallback} params.errorCallback the async error callback 895 * @param {String} params.accountName the account to send request on behalf of 896 * @param {int} params.count the starting count for number of items processed 897 * @param {ZmBatchCommand} batchCmd if set, request data is added to batch request 898 * @param {boolean} params.noUndo true if the action is performed as an undo (not undoable) 899 * @param {boolean} params.safeMove true if the action wants to resolve any conflicts before completion 900 */ 901 ZmList.prototype._itemAction = 902 function(params, batchCmd) { 903 904 var result = this._getIds(params.items); 905 var idHash = result.hash; 906 var idList = result.list; 907 if (!(idList && idList.length)) { 908 if (params.callback) { 909 params.callback.run(new ZmCsfeResult([])); 910 } 911 if (params.finalCallback) { 912 params.finalCallback(params); 913 } 914 return; 915 } 916 917 DBG.println("sa", "ITEM ACTION: " + idList.length + " items"); 918 var type; 919 if (params.items.length == 1 && params.items[0] && params.items[0].type) { 920 type = params.items[0].type; 921 } else { 922 type = this.type; 923 } 924 if (!type) { return; } 925 926 // set accountName for multi-account to be the main "local" account since we 927 // assume actioned ID's will always be fully qualified 928 if (!params.accountName && appCtxt.multiAccounts) { 929 params.accountName = appCtxt.accountList.mainAccount.name; 930 } 931 932 var soapCmd = ZmItem.SOAP_CMD[type] + "Request"; 933 var useJson = batchCmd ? batchCmd._useJson : true ; 934 var request, action; 935 if (useJson) { 936 request = {}; 937 var urn = this._getActionNamespace(); 938 request[soapCmd] = {_jsns:urn}; 939 var action = request[soapCmd].action = {}; 940 action.op = params.action; 941 for (var attr in params.attrs) { 942 action[attr] = params.attrs[attr]; 943 } 944 } else { 945 request = AjxSoapDoc.create(soapCmd, this._getActionNamespace()); 946 action = request.set("action"); 947 action.setAttribute("op", params.action); 948 for (var attr in params.attrs) { 949 action.setAttribute(attr, params.attrs[attr]); 950 } 951 } 952 var ac = window.parentAppCtxt || appCtxt; 953 var actionController = ac.getActionController(); 954 var undoPossible = !params.noUndo && (this.type != ZmItem.CONV || this.search && this.search.folderId); //bug 74169 - since the convs might not be fully loaded we might not know where the messages are moved from at all. so no undo. 955 var actionLogItem = (undoPossible && actionController && actionController.actionPerformed({op: params.action, ids: idList, attrs: params.attrs})) || null; 956 var respCallback = new AjxCallback(this, this._handleResponseItemAction, [params.callback, actionLogItem]); 957 958 var params1 = { 959 ids: idList, 960 idHash: idHash, 961 accountName: params.accountName, 962 request: request, 963 action: action, 964 type: type, 965 callback: respCallback, 966 finalCallback: params.finalCallback, 967 errorCallback: params.errorCallback, 968 batchCmd: batchCmd, 969 numItems: params.count || 0, 970 actionTextKey: params.actionTextKey, 971 actionArg: params.actionArg, 972 actionLogItem: actionLogItem, 973 childWin: params.childWin, 974 closeChildWin: params.closeChildWin, 975 safeMove: params.safeMove 976 }; 977 978 if (idList.length >= ZmList.CHUNK_SIZE) { 979 var pdParams = { 980 state: ZmListController.PROGRESS_DIALOG_INIT, 981 callback: new AjxCallback(this, this._cancelAction, [params1]) 982 } 983 ZmListController.handleProgress(pdParams); 984 } 985 986 this._doAction(params1); 987 }; 988 989 /** 990 * @private 991 */ 992 ZmList.prototype._handleResponseItemAction = 993 function(callback, actionLogItem, items, result) { 994 if (actionLogItem) { 995 actionLogItem.setComplete(); 996 } 997 998 if (callback) { 999 result.set(items); 1000 callback.run(result); 1001 } 1002 }; 1003 1004 /** 1005 * @private 1006 */ 1007 ZmList.prototype._doAction = 1008 function(params) { 1009 1010 var list = params.ids.splice(0, ZmList.CHUNK_SIZE); 1011 var idStr = list.join(","); 1012 var useJson = true; 1013 if (params.action.setAttribute) { 1014 params.action.setAttribute("id", idStr); 1015 useJson = false; 1016 } else { 1017 params.action.id = idStr; 1018 } 1019 var more = Boolean(params.ids.length && !params.cancelled); 1020 1021 var respCallback = new AjxCallback(this, this._handleResponseDoAction, [params]); 1022 var isOutboxFolder = this.controller && this.controller.isOutboxFolder(); 1023 var offlineCallback = this._handleOfflineResponseDoAction.bind(this, params, isOutboxFolder); 1024 1025 if (params.batchCmd) { 1026 params.batchCmd.addRequestParams(params.request, respCallback, params.errorCallback); 1027 } else { 1028 var reqParams = {asyncMode:true, callback:respCallback, errorCallback: params.errorCallback, offlineCallback: offlineCallback, accountName:params.accountName, more:more}; 1029 if (useJson) { 1030 reqParams.jsonObj = params.request; 1031 } else { 1032 reqParams.soapDoc = params.request; 1033 } 1034 if (params.safeMove) { 1035 reqParams.useChangeToken = true; 1036 } 1037 if (isOutboxFolder) { 1038 reqParams.offlineRequest = true; 1039 } 1040 DBG.println("sa", "*** do action: " + list.length + " items"); 1041 params.reqId = appCtxt.getAppController().sendRequest(reqParams); 1042 } 1043 }; 1044 1045 /** 1046 * @private 1047 */ 1048 ZmList.prototype._handleResponseDoAction = 1049 function(params, result) { 1050 1051 var summary; 1052 var response = result.getResponse(); 1053 var resp = response[ZmItem.SOAP_CMD[params.type] + "Response"]; 1054 if (resp && resp.action) { 1055 var ids = resp.action.id.split(","); 1056 if (ids) { 1057 var items = []; 1058 for (var i = 0; i < ids.length; i++) { 1059 var item = params.idHash[ids[i]]; 1060 if (item) { 1061 items.push(item); 1062 } 1063 } 1064 params.numItems += items.length; 1065 if (params.callback) { 1066 params.callback.run(items, result); 1067 } 1068 1069 if (params.actionTextKey) { 1070 summary = ZmList.getActionSummary(params); 1071 var pdParams = { 1072 state: ZmListController.PROGRESS_DIALOG_UPDATE, 1073 summary: summary 1074 } 1075 ZmListController.handleProgress(pdParams); 1076 } 1077 } 1078 } 1079 1080 if (params.ids.length && !params.cancelled) { 1081 DBG.println("sa", "item action setting up next chunk, remaining: " + params.ids.length); 1082 AjxTimedAction.scheduleAction(new AjxTimedAction(this, this._doAction, [params]), ZmItem.CHUNK_PAUSE); 1083 } else { 1084 params.reqId = null; 1085 params.actionSummary = summary; 1086 if (params.finalCallback) { 1087 // finalCallback is responsible for showing status or clearing dialog 1088 DBG.println("sa", "item action running finalCallback"); 1089 params.finalCallback(params); 1090 } else { 1091 DBG.println("sa", "no final callback"); 1092 ZmListController.handleProgress({state:ZmListController.PROGRESS_DIALOG_CLOSE}); 1093 ZmBaseController.showSummary(params.actionSummary, params.actionLogItem, params.closeChildWin); 1094 } 1095 } 1096 }; 1097 1098 /** 1099 * @private 1100 */ 1101 ZmList.prototype._handleOfflineResponseDoAction = 1102 function(params, isOutboxFolder, requestParams) { 1103 1104 var action = params.action, 1105 callback = this._handleOfflineResponseDoActionCallback.bind(this, params, isOutboxFolder, requestParams.callback); 1106 1107 if (isOutboxFolder && action.op === "trash") { 1108 var key = { 1109 methodName : "SendMsgRequest", //Outbox folder only contains offline sent emails 1110 id : action.id.split(",") 1111 }; 1112 ZmOfflineDB.deleteItemInRequestQueue(key, callback); 1113 } 1114 else { 1115 var obj = requestParams.jsonObj; 1116 obj.methodName = ZmItem.SOAP_CMD[params.type] + "Request"; 1117 obj.id = action.id; 1118 ZmOfflineDB.setItem(obj, ZmOffline.REQUESTQUEUE, callback); 1119 } 1120 }; 1121 1122 /** 1123 * @private 1124 */ 1125 ZmList.prototype._handleOfflineResponseDoActionCallback = 1126 function(params, isOutboxFolder, callback) { 1127 1128 var data = {}, 1129 header = this._generateOfflineHeader(params), 1130 result, 1131 hdr, 1132 notify; 1133 1134 data[ZmItem.SOAP_CMD[params.type] + "Response"] = params.request[ZmItem.SOAP_CMD[params.type] + "Request"]; 1135 result = new ZmCsfeResult(data, false, header); 1136 hdr = result.getHeader(); 1137 if (callback) { 1138 callback.run(result); 1139 } 1140 if (hdr) { 1141 notify = hdr.context.notify[0]; 1142 if (notify) { 1143 appCtxt._requestMgr._notifyHandler(notify); 1144 this._updateOfflineData(params, isOutboxFolder, notify); 1145 } 1146 } 1147 }; 1148 1149 /** 1150 * @private 1151 */ 1152 ZmList.prototype._generateOfflineHeader = 1153 function(params) { 1154 1155 var action = params.action, 1156 op = action.op, 1157 ids = action.id.split(","), 1158 idsLength = ids.length, 1159 id, 1160 msg, 1161 flags, 1162 folderId, 1163 folder, 1164 targetFolder, 1165 mObj, 1166 cObj, 1167 folderObj, 1168 m = [], 1169 c = [], 1170 folderArray = [], 1171 header; 1172 1173 for (var i = 0; i < idsLength; i++) { 1174 1175 id = ids[i]; 1176 msg = this.getById(id); 1177 flags = msg.flags || ""; 1178 folderId = msg.getFolderId(); 1179 folder = appCtxt.getById(folderId); 1180 mObj = { 1181 id : id 1182 }; 1183 cObj = { 1184 id : "-" + mObj.id 1185 }; 1186 folderObj = { 1187 id : folderId 1188 }; 1189 1190 switch (op) 1191 { 1192 case "flag": 1193 mObj.f = flags + "f"; 1194 break; 1195 case "!flag": 1196 mObj.f = flags.replace("f", ""); 1197 break; 1198 case "read": 1199 mObj.f = flags.replace("u", ""); 1200 folderObj.u = folder.numUnread - 1; 1201 break; 1202 case "!read": 1203 mObj.f = flags + "u"; 1204 folderObj.u = folder.numUnread + 1; 1205 break; 1206 case "trash": 1207 mObj.l = ZmFolder.ID_TRASH; 1208 break; 1209 case "spam": 1210 mObj.l = ZmFolder.ID_SPAM; 1211 break; 1212 case "!spam": 1213 mObj.l = ZmFolder.ID_INBOX;// Have to set the old folder id. Currently point to inbox 1214 break; 1215 case "move": 1216 if (action.l) { 1217 mObj.l = action.l; 1218 } 1219 folderObj.n = folder.numTotal - 1; 1220 if (msg.isUnread && folder.numUnread > 1) { 1221 folderObj.u = folder.numUnread - 1; 1222 } 1223 targetFolder = appCtxt.getById(mObj.l); 1224 folderArray.push({ 1225 id : targetFolder.id, 1226 n : targetFolder.numTotal + 1, 1227 u : (msg.isUnread ? targetFolder.numUnread + 1 : targetFolder.numUnread) 1228 }); 1229 break; 1230 case "tag": 1231 msg.tags.push(action.tn); 1232 mObj.tn = msg.tags.join(); 1233 break; 1234 case "!tag": 1235 AjxUtil.arrayRemove(msg.tags, action.tn); 1236 mObj.tn = msg.tags.join(); 1237 break; 1238 case "update": 1239 if (action.t === "") {//Removing all tag names for a msg 1240 mObj.tn = ""; 1241 mObj.t = ""; 1242 } 1243 break; 1244 } 1245 m.push(mObj); 1246 c.push(cObj); 1247 folderArray.push(folderObj); 1248 } 1249 1250 header = { 1251 context : { 1252 notify : [{ 1253 modified : { 1254 m : m, 1255 c : c, 1256 folder : folderArray 1257 } 1258 }] 1259 } 1260 }; 1261 1262 return header; 1263 }; 1264 1265 ZmList.prototype._updateOfflineData = 1266 function(params, isOutboxFolder, notify) { 1267 1268 var modified = notify.modified; 1269 if (!modified) { 1270 return; 1271 } 1272 1273 var m = modified.m; 1274 if (!m) { 1275 return; 1276 } 1277 1278 var callback = this._updateOfflineDataCallback.bind(this, params, m); 1279 ZmOfflineDB.getItem(params.action.id.split(","), ZmApp.MAIL, callback); 1280 }; 1281 1282 ZmList.prototype._updateOfflineDataCallback = 1283 function(params, msgArray, result) { 1284 result = ZmOffline.recreateMsg(result); 1285 var newMsgArray = []; 1286 result.forEach(function(res) { 1287 msgArray.forEach(function(msg) { 1288 if (msg.id === res.id) { 1289 newMsgArray.push($.extend(res, msg)); 1290 } 1291 }); 1292 }); 1293 ZmOfflineDB.setItem(newMsgArray, ZmApp.MAIL); 1294 }; 1295 1296 /** 1297 * Returns a string describing an action, intended for display as toast to tell the 1298 * user what they just did. 1299 * 1300 * @param {Object} params hash of params: 1301 * {String} type item type (ZmItem.*) 1302 * {Number} numItems number of items affected 1303 * {String} actionTextKey ZmMsg key for text string describing action 1304 * {String} actionArg (optional) additional argument 1305 * 1306 * @return {String} action summary 1307 */ 1308 ZmList.getActionSummary = 1309 function(params) { 1310 1311 var type = params.type, 1312 typeKey = ZmItem.MSG_KEY[type], 1313 typeText = ZmMsg[typeKey], 1314 capKey = AjxStringUtil.capitalizeFirstLetter(typeKey), 1315 countKey = 'type' + capKey, 1316 num = params.numItems, 1317 alternateKey = params.actionTextKey + capKey, 1318 text = ZmMsg[alternateKey] || ZmMsg[params.actionTextKey], 1319 countText = ZmMsg[countKey], 1320 arg = AjxStringUtil.htmlEncode(params.actionArg), 1321 textAuto = countText ? AjxMessageFormat.format(countText, num) : typeText, 1322 textSingular = countText ? AjxMessageFormat.format(ZmMsg[countKey], 1) : typeText; 1323 1324 return AjxMessageFormat.format(text, [ num, textAuto, arg, textSingular ]); 1325 }; 1326 1327 /** 1328 * Cancel current server request if there is one, and set flag to 1329 * stop cascade of requests. 1330 * 1331 * @param {Hash} params a hash of parameters 1332 * 1333 * @private 1334 */ 1335 ZmList.prototype._cancelAction = 1336 function(params) { 1337 params.cancelled = true; 1338 if (params.reqId) { 1339 appCtxt.getRequestMgr().cancelRequest(params.reqId); 1340 } 1341 if (params.finalCallback) { 1342 params.finalCallback(params); 1343 } 1344 ZmListController.handleProgress({state:ZmListController.PROGRESS_DIALOG_CLOSE}); 1345 }; 1346 1347 /** 1348 * @private 1349 */ 1350 ZmList.prototype._getTypedItems = 1351 function(items) { 1352 var typedItems = {}; 1353 for (var i = 0; i < items.length; i++) { 1354 var type = items[i].type; 1355 if (!typedItems[type]) { 1356 typedItems[type] = []; 1357 } 1358 typedItems[type].push(items[i]); 1359 } 1360 return typedItems; 1361 }; 1362 1363 /** 1364 * Grab the IDs out of a list of items, and return them as both a string and a hash. 1365 * 1366 * @private 1367 */ 1368 ZmList.prototype._getIds = 1369 function(list) { 1370 1371 var idHash = {}; 1372 if (list instanceof ZmItem) { 1373 list = [list]; 1374 } 1375 1376 var ids = []; 1377 if ((list && list.length)) { 1378 for (var i = 0; i < list.length; i++) { 1379 var item = list[i]; 1380 var id = item.id; 1381 if (id) { 1382 ids.push(id); 1383 idHash[id] = item; 1384 } 1385 } 1386 } 1387 1388 return {hash:idHash, list:ids}; 1389 }; 1390 1391 /** 1392 * Returns the index at which the given item should be inserted into this list. 1393 * Subclasses should override to return a meaningful value. 1394 * 1395 * @private 1396 */ 1397 ZmList.prototype._sortIndex = 1398 function(item) { 1399 return 0; 1400 }; 1401 1402 /** 1403 * @private 1404 */ 1405 ZmList.prototype._redoSearch = 1406 function(ctlr) { 1407 var sc = appCtxt.getSearchController(); 1408 sc.redoSearch(ctlr._currentSearch); 1409 }; 1410 1411 /** 1412 * @private 1413 */ 1414 ZmList.prototype._getActionNamespace = 1415 function() { 1416 return "urn:zimbraMail"; 1417 }; 1418 1419 /** 1420 * @private 1421 */ 1422 ZmList.prototype._folderTreeChangeListener = 1423 function(ev) { 1424 if (ev.type != ZmEvent.S_FOLDER) return; 1425 1426 var folder = ev.getDetail("organizers")[0]; 1427 var fields = ev.getDetail("fields"); 1428 var ctlr = appCtxt.getCurrentController(); 1429 var isCurrentList = (appCtxt.getCurrentList() == this); 1430 1431 if (ev.event == ZmEvent.E_DELETE && 1432 (ev.source instanceof ZmFolder) && 1433 ev.source.id == ZmFolder.ID_TRASH) 1434 { 1435 // user emptied trash - reset a bunch of stuff w/o having to redo the search 1436 var curView = ctlr.getListView && ctlr.getListView(); 1437 if (curView) { 1438 curView.offset = 0; 1439 } 1440 ctlr._resetNavToolBarButtons(view); 1441 } 1442 else if (isCurrentList && ctlr && ctlr._currentSearch && 1443 (ev.event == ZmEvent.E_MOVE || (ev.event == ZmEvent.E_MODIFY) && fields && fields[ZmOrganizer.F_NAME])) 1444 { 1445 // on folder rename or move, update current query if folder is part of query 1446 if (ctlr._currentSearch.replaceFolderTerm(ev.getDetail("oldPath"), folder.getPath())) { 1447 appCtxt.getSearchController().setSearchField(ctlr._currentSearch.query); 1448 } 1449 } 1450 }; 1451 1452 /** 1453 * this method is for handling changes in the tag tree itself (tag rename, delete). In some places it is named _tagChangeListener. 1454 * the ZmListView equivalent is actually called ZmListView.prototype._tagChangeListener 1455 * @private 1456 */ 1457 ZmList.prototype._tagTreeChangeListener = 1458 function(ev) { 1459 if (ev.type != ZmEvent.S_TAG) { return; } 1460 1461 var tag = ev.getDetail("organizers")[0]; 1462 var fields = ev.getDetail("fields"); 1463 var ctlr = appCtxt.getCurrentController(); 1464 if (!ctlr) { return; } 1465 1466 var a = this.getArray(); 1467 1468 if ((ev.event == ZmEvent.E_MODIFY) && fields && fields[ZmOrganizer.F_NAME]) { 1469 // on tag rename, update current query if tag is part of query 1470 var oldName = ev.getDetail("oldName"); 1471 if (ctlr._currentSearch && ctlr._currentSearch.hasTagTerm(oldName)) { 1472 ctlr._currentSearch.replaceTagTerm(oldName, tag.getName()); 1473 appCtxt.getSearchController().setSearchField(ctlr._currentSearch.query); 1474 } 1475 1476 //since we tag (and map the tags) by name, replace the tag name in the list and hash of tags. 1477 var newName = tag.name; 1478 for (var i = 0; i < a.length; i++) { 1479 var item = a[i]; //not using the following here as it didn't seem to work for contacts, the list is !isCanonical and null is returned, even though a[i] is fine ==> this.getById(a[i].id); // make sure item is realized (contact may not be) 1480 if (!item || !item.isZmItem || !item.hasTag(oldName)) { 1481 continue; //nothing to do if item does not have tag 1482 } 1483 if (item.isShared()) { 1484 continue; //overview tag rename does not affect remote items tags 1485 } 1486 var tagHash = item.tagHash; 1487 var tags = item.tags; 1488 delete tagHash[oldName]; 1489 tagHash[newName] = true; 1490 for (var j = 0 ; j < tags.length; j++) { 1491 if (tags[j] == oldName) { 1492 tags[j] = newName; 1493 break; 1494 } 1495 } 1496 } 1497 1498 1499 } else if (ev.event == ZmEvent.E_DELETE) { 1500 // Remove tag from any items that have it 1501 var hasTagListener = this._evtMgr.isListenerRegistered(ZmEvent.L_MODIFY); 1502 for (var i = 0; i < a.length; i++) { 1503 var item = this.getById(a[i].id); // make sure item is realized (contact may not be) 1504 if (item) { 1505 if (item.isShared()) { 1506 continue; //overview tag delete does not affect remote items tags 1507 } 1508 if (item.hasTag(tag.name)) { 1509 item.tagLocal(tag.name, false); 1510 if (hasTagListener) { 1511 this._notify(ZmEvent.E_TAGS, {items:[item]}); 1512 } 1513 } 1514 } 1515 } 1516 1517 // If search results are based on this tag, keep them around so that user can still 1518 // view msgs or open convs, but disable pagination and sorting since they're based 1519 // on the current query. 1520 if (ctlr._currentSearch && ctlr._currentSearch.hasTagTerm(tag.getName())) { 1521 var viewId = appCtxt.getCurrentViewId(); 1522 var viewType = appCtxt.getCurrentViewType(); 1523 ctlr.enablePagination(false, viewId); 1524 var view = ctlr.getListView && ctlr.getListView(); 1525 if (view && view.sortingEnabled) { 1526 view.sortingEnabled = false; 1527 } 1528 if (viewType == ZmId.VIEW_CONVLIST) { 1529 ctlr._currentSearch.query = "is:read is:unread"; 1530 } 1531 ctlr._currentSearch.tagId = null; 1532 appCtxt.getSearchController().setSearchField(""); 1533 } 1534 } 1535 }; 1536