1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2010, 2011, 2012, 2013, 2014, 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) 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 ZmShareSearchDialog = function(params) { 25 // initialize params 26 params.className = params.className || "ZmShareSearchDialog DwtDialog"; 27 params.title = ZmMsg.sharedFoldersAddTitle; 28 params.standardButtons = [ ZmShareSearchDialog.ADD_BUTTON, DwtDialog.CANCEL_BUTTON ]; 29 params.id = "ZmShareSearchDialog"; 30 31 // setup auto-complete 32 // NOTE: This needs to be done before default construction so 33 // NOTE: that it is available when we initialize the email 34 // NOTE: input field. 35 var acparams = { 36 dataClass: appCtxt.getAutocompleter(), 37 matchValue: ZmAutocomplete.AC_VALUE_EMAIL, 38 keyUpCallback: this._acKeyUpListener.bind(this), 39 contextId: this.toString(), 40 autocompleteType: "all" 41 }; 42 this._acAddrSelectList = new ZmAutocompleteListView(acparams); 43 44 // default construction 45 DwtDialog.call(this, params); 46 47 // set custom button label 48 this.getButton(ZmShareSearchDialog.ADD_BUTTON).setText(ZmMsg.add); 49 50 // insert form elements into tab group 51 var tabGroup = this._tabGroup; 52 tabGroup.addMemberBefore(this._form.getTabGroupMember(), tabGroup.getFirstMember()); 53 }; 54 ZmShareSearchDialog.prototype = new DwtDialog; 55 ZmShareSearchDialog.prototype.constructor = ZmShareSearchDialog; 56 57 ZmShareSearchDialog.prototype.isZmShareSearchDialog = true; 58 ZmShareSearchDialog.prototype.toString = function() { return "ZmShareSearchDialog"; }; 59 60 // 61 // Constants 62 // 63 64 ZmShareSearchDialog.ADD_BUTTON = DwtDialog.OK_BUTTON; //++DwtDialog.LAST_BUTTON; 65 66 ZmShareSearchDialog._APP_TYPES = [ZmApp.MAIL, ZmApp.CONTACTS, ZmApp.CALENDAR, ZmApp.TASKS, ZmApp.BRIEFCASE]; 67 ZmShareSearchDialog._APP_KEY = {}; 68 ZmShareSearchDialog._APP_KEY[ZmApp.MAIL] = "mailSharesOnly"; 69 ZmShareSearchDialog._APP_KEY[ZmApp.TASKS] = "taskSharesOnly"; 70 ZmShareSearchDialog._APP_KEY[ZmApp.BRIEFCASE] = "briefcaseSharesOnly"; 71 ZmShareSearchDialog._APP_KEY[ZmApp.CALENDAR] = "calendarSharesOnly"; 72 ZmShareSearchDialog._APP_KEY[ZmApp.CONTACTS] = "addrbookSharesOnly"; 73 74 // 75 // Data 76 // 77 78 ZmShareSearchDialog.prototype.CONTENT_TEMPLATE = "share.Widgets#ZmShareSearchView"; 79 80 81 // 82 // Public methods 83 // 84 85 ZmShareSearchDialog.prototype.getShares = function() { 86 var treeView = this._form.getControl("TREE"); 87 var root = this._getNode(ZmOrganizer.ID_ROOT); 88 var shares = []; 89 this._collectShares(treeView, root, shares); 90 return shares; 91 }; 92 93 // 94 // Protected methods 95 // 96 97 ZmShareSearchDialog.prototype._collectShares = function(treeView, node, shares) { 98 if (node.shareInfo) { 99 var treeItem = treeView.getTreeItemById(node.id); 100 // NOTE: Only collect shares that are checked *and* visible. 101 // NOTE: In other words, we should never mount a share that 102 // NOTE: is not visible even if the user had checked it before 103 // NOTE: applying a filter. Otherwise they would be left 104 // NOTE: wondering why it was mounted. 105 if (treeItem && treeItem.getChecked() && treeItem.getVisible()) { 106 shares.push(node.shareInfo); 107 } 108 } 109 else { 110 var children = node.children.getArray(); 111 for (var i = 0; i < children.length; i++) { 112 this._collectShares(treeView, children[i], shares); 113 } 114 } 115 }; 116 117 ZmShareSearchDialog.prototype._filterResults = function() { 118 var treeView = this._form.getControl("TREE"); 119 var root = this._getNode(ZmOrganizer.ID_ROOT); 120 var text = this._form.getValue("FILTER") || ""; 121 this._filterNode(treeView, root, text.toLowerCase()); 122 }; 123 124 ZmShareSearchDialog.prototype._filterNode = function(treeView, node, text) { 125 var nodeItem = treeView.getTreeItemById(node.id); 126 if (!nodeItem) { 127 return false; 128 } 129 // process children 130 var count = node.children.size(); 131 var app = this._form.getValue("APP") || ""; 132 var matches = false; 133 if (count > 0) { 134 //this node has children. 135 for (var i = 0; i < count; i++) { 136 var child = node.children.get(i); 137 matches = this._filterNode(treeView, child, text) || matches; //order is important! (need to call _filterNode always 138 } 139 } 140 else { 141 //this is a leaf node 142 var isInfoNode = String(node.id).match(/^-/); 143 var textMatches = !text || node.name.toLowerCase().indexOf(text) !== -1; 144 var appMatches = !app || node.shareInfo && node.shareInfo.view === app; 145 matches = !isInfoNode && textMatches && appMatches; 146 } 147 matches = matches || node.id == ZmOrganizer.ID_ROOT; 148 nodeItem.setVisible(matches); 149 return matches; 150 }; 151 152 ZmShareSearchDialog.prototype._createOrganizer = function(parent, id, name) { 153 // NOTE: The caller is responsible for adding the new node 154 // NOTE: to the parent's children. 155 return new ZmShareProxy({parent:parent,id:id,name:name,tree:(parent&&parent.tree)}); 156 }; 157 158 ZmShareSearchDialog.prototype._resetTree = function() { 159 // create new tree 160 var tree = new ZmTree(ZmOrganizer.SHARE); 161 // NOTE: The root should never be seen 162 tree.root = this._createOrganizer(null, ZmOrganizer.ID_ROOT, "[Root]"); 163 164 // setup tree view 165 var treeView = this._form.getControl("TREE"); 166 treeView.set({ dataTree: tree }); 167 var treeItem = treeView.getTreeItemById(ZmOrganizer.ID_ROOT); 168 treeItem.setVisible(false, true); 169 treeItem.setExpanded(true); 170 treeItem.enableSelection(false); 171 treeItem.showCheckBox(false); 172 }; 173 174 // Fix for bug: 79402. Passing extra param for wide search. 175 ZmShareSearchDialog.prototype._doUserSearch = function(emails, isWideSearch) { 176 this._resetTree(); 177 // collect unique email addresses 178 emails = emails.split(/\s*[;,]\s*/); 179 var emailMap = {}; 180 for (var i = 0; i < emails.length; i++) { 181 var email = AjxStringUtil.trim(emails[i]); 182 if (!email) { 183 continue; 184 } 185 if (email === appCtxt.get(ZmSetting.USERNAME)) { 186 continue; 187 } 188 emailMap[email.toLowerCase()] = email; 189 } 190 191 // build request 192 var requests = [], requestIdMap = {}; 193 var i = 0; 194 for (var emailId in emailMap) { 195 // add request 196 requests.push({ 197 _jsns: "urn:zimbraAccount", 198 requestId: i, 199 includeSelf: 0, 200 owner: { by: "name", _content: emailMap[emailId] } 201 }); 202 203 // add loading placeholder node 204 if (!this._loadingUserFormatter) { 205 this._loadingUserFormatter = new AjxMessageFormat(ZmMsg.sharedFoldersLoadingUser); 206 } 207 var text = this._loadingUserFormatter.format([email]); 208 var loadingId = [ZmShareProxy.ID_LOADING,Dwt.getNextId("share")].join(":"); 209 this._appendInfoNode(ZmOrganizer.ID_ROOT, loadingId, AjxStringUtil.htmlEncode(text)); 210 211 // remember the placeholder nodes 212 emailMap[emailId] = loadingId; 213 requestIdMap[i] = loadingId; 214 i++; 215 } 216 217 // Fix for bug: 79402. Replaces _doGroupSearch. 218 if (isWideSearch) { 219 this._appendInfoNode(ZmOrganizer.ID_ROOT, ZmShareProxy.ID_LOADING, ZmMsg.sharedFoldersLoading); 220 221 requests.push({ 222 _jsns: "urn:zimbraAccount", 223 includeSelf: 0 224 }); 225 } 226 227 // anything to do? 228 if (requests.length == 0) { 229 return; 230 } 231 232 // perform user search 233 this._setSearching(true); 234 var params = { 235 jsonObj: { 236 BatchRequest: { 237 _jsns: "urn:zimbra", 238 GetShareInfoRequest: requests 239 } 240 }, 241 asyncMode: true, 242 callback: new AjxCallback(this, this._handleUserSearchResults, [emailMap, requestIdMap]), 243 errorCallback: new AjxCallback(this, this._handleUserSearchError) 244 }; 245 appCtxt.getAppController().sendRequest(params); 246 }; 247 248 ZmShareSearchDialog.prototype._setSearching = function(searching) { 249 this._form.setEnabled(!searching); 250 }; 251 252 ZmShareSearchDialog.prototype._handleUserSearchResults = function(emailMap, requestIdMap, resp) { 253 this._setSearching(false); 254 255 // remove placeholder nodes 256 for (var email in emailMap) { 257 this._removeNode(emailMap[email]); 258 } 259 260 // add nodes for results 261 var batchResponse = AjxUtil.get(resp.getResponse(), "BatchResponse"); 262 var responses = AjxUtil.get(batchResponse, "GetShareInfoResponse"); 263 if (responses) { 264 // get list of owners with their shares, in alphabetical order 265 var owners = {}; 266 for (var i = 0; i < responses.length; i++) { 267 var response = responses[i]; 268 this._addToOwnerMap(owners, response.share); 269 } 270 owners = AjxUtil.values(owners); 271 owners.sort(ZmShareSearchDialog.__byOwnerName); 272 273 // add shares 274 this._appendShareNodes(owners); 275 } 276 277 // apply current filter 278 this._filterResults(); 279 280 // handle errors 281 var faults = AjxUtil.get(batchResponse, "Fault"); 282 if (faults) { 283 var treeView = this._form.getControl("TREE"); 284 for (var i = 0; i < faults.length; i++) { 285 var fault = faults[i]; 286 287 // replace placeholder node with error node 288 var faultNodeId = ZmShareProxy.ID_ERROR;// TODO: create unique error item id 289 var loadingNode = this._getNode(requestIdMap[fault.requestId]); 290 var faultNode = this._createOrganizer(loadingNode.parent, faultNodeId, ZmMsg.sharedFoldersError); 291 treeView.replaceNode(faultNode, loadingNode); 292 293 // set error message as tooltip 294 var treeItem = treeView.getTreeItemById(faultNodeId); 295 treeItem.showCheckBox(false); 296 treeItem.setToolTipContent(AjxStringUtil.htmlEncode(fault.Reason.Text)); 297 } 298 } 299 }; 300 301 ZmShareSearchDialog.prototype._addToOwnerMap = function(owners, shares) { 302 if (!shares) return; 303 304 for (var j = 0; j < shares.length; j++) { 305 var share = shares[j]; 306 var owner = owners[share.ownerId]; 307 if (!owner) { 308 owner = owners[share.ownerId] = { 309 ownerId: share.ownerId, 310 ownerName: share.ownerName || share.ownerEmail, 311 ownerEmail: share.ownerEmail, 312 shares: [] 313 }; 314 } 315 owner.shares.push(share); 316 } 317 }; 318 319 ZmShareSearchDialog.prototype._handleUserSearchError = function(resp) { 320 this._setSearching(false); 321 // TODO 322 }; 323 324 // node management 325 326 ZmShareSearchDialog.prototype._getNode = function(id) { 327 var treeView = this._form.getControl("TREE"); 328 var treeItem = treeView.getTreeItemById(id); 329 return treeItem && treeItem.getData(Dwt.KEY_OBJECT); 330 }; 331 332 ZmShareSearchDialog.prototype._removeNode = function(nodeId) { 333 var treeView = this._form.getControl("TREE"); 334 treeView.removeNode(this._getNode(nodeId)); 335 }; 336 337 ZmShareSearchDialog.prototype._appendChild = function(childNode, parentNode, checkable, tooltip) { 338 var treeView = this._form.getControl("TREE"); 339 var treeItem = treeView.appendChild(childNode, parentNode, null, tooltip); 340 treeItem.setExpanded(true); 341 treeItem.enableSelection(false); 342 treeItem.showCheckBox(checkable); 343 treeItem.setVisible(false); //filterResults will set visibility 344 return treeItem; 345 }; 346 347 ZmShareSearchDialog.prototype._appendShareNodes = function(owners) { 348 349 // run through owners 350 for (var j = 0; j < owners.length; j++) { 351 // create parent node, if needed 352 var owner = owners[j]; 353 var parentNode = this._getNode(owner.ownerId); 354 if (!parentNode) { 355 var root = this._getNode(ZmOrganizer.ID_ROOT); 356 parentNode = this._createOrganizer(root, owner.ownerId, owner.ownerName || owner.ownerEmail); 357 this._appendChild(parentNode, root); 358 } 359 360 // add share nodes 361 var shares = owner.shares; 362 if (shares.length > 0) { 363 shares.sort(ZmShareSearchDialog.__byFolderPath); 364 for (var i = 0; i < shares.length; i++) { 365 var share = shares[i]; 366 if (ZmFolder.HIDE_ID[share.folderId]) { 367 continue; 368 } 369 var shareId = [share.ownerId,share.folderId].join(":"); 370 if (this._getNode(shareId) != null) continue; 371 372 // NOTE: strip the leading slash from folder path 373 var folderPath = share.folderPath; 374 var shareFullPathName = share.folderId == ZmOrganizer.ID_ROOT ? ZmMsg.allApplications : folderPath.substr(1); 375 var shareNode = this._createOrganizer(parentNode, shareId, shareFullPathName); 376 shareNode.shareInfo = share; 377 378 // augment share info 379 share.icon = shareNode.getIcon(); 380 share.role = ZmShare.getRoleFromPerm(share.rights); 381 share.roleName = ZmShare.getRoleName(share.role); 382 share.roleActions = ZmShare.getRoleActions(share.role); 383 share.normalizedOwnerName = share.ownerName || share.ownerEmail; 384 share.normalizedGranteeName = share.granteeDisplayName || share.granteeName; 385 share.normalizedFolderPath = shareFullPathName; 386 share.name = folderPath.substr(folderPath.lastIndexOf("/") + 1); 387 var ownerName = share.normalizedOwnerName; 388 var indexOfAtSign = ownerName.indexOf('@'); 389 if (indexOfAtSign > -1) { 390 ownerName = ownerName.substr(0, indexOfAtSign) 391 } 392 share.defaultMountpointName = ZmShare.getDefaultMountpointName(ownerName, share.name); 393 394 // set tooltip 395 var tooltip = AjxTemplate.expand(shareNode.TOOLTIP_TEMPLATE, share); 396 this._appendChild(shareNode, parentNode, true, tooltip); 397 } 398 } 399 400 // no shares found 401 else { 402 this._appendInfoNode(parentNode, ZmShareProxy.ID_NONE_FOUND, ZmMsg.sharedFoldersNoneFound); 403 } 404 } 405 }; 406 407 ZmShareSearchDialog.prototype._appendInfoNode = function(parentId, id, text, tooltip) { 408 var parent = this._getNode(parentId); 409 var node = this._createOrganizer(parent, id, text); 410 return this._appendChild(node, parent, null, tooltip); 411 }; 412 413 // sorting 414 415 ZmShareSearchDialog.__byOwnerName = AjxCallback.simpleClosure(AjxUtil.byStringProp, window, "ownerName"); 416 ZmShareSearchDialog.__byFolderPath = AjxCallback.simpleClosure(AjxUtil.byStringProp, window, "folderPath"); 417 418 // auto-complete 419 420 ZmShareSearchDialog.prototype._acKeyUpListener = function(event, aclv, result) { 421 // TODO: Does anything need to be done here? 422 }; 423 424 // 425 // DwtDialog methods 426 // 427 428 ZmShareSearchDialog.prototype.popup = function(organizerType, addCallback, cancelCallback) { 429 this.reset(); 430 if (addCallback) this._buttonDesc[ZmShareSearchDialog.ADD_BUTTON].callback = addCallback; 431 if (cancelCallback) this._buttonDesc[DwtDialog.CANCEL_BUTTON].callback = cancelCallback; 432 433 if (appCtxt.multiAccounts) { 434 var acct = appCtxt.getActiveAccount() || appCtxt.accountList.mainAccount; 435 this._acAddrSelectList.setActiveAccount(acct); 436 } 437 438 var form = this._form; 439 form.setValue("FILTER", ""); 440 form.setValue("EMAIL", ""); 441 form.setEnabled("SEARCH", false); //disable search button by default 442 this._selectApplicationOption(); 443 this._resetTree(); 444 // Fix for bug: 79402. Do wide search. 445 this._doUserSearch("", true); 446 447 DwtDialog.prototype.popup.call(this); 448 449 form.getControl("EMAIL").focus(); 450 }; 451 452 ZmShareSearchDialog.prototype.popdown = function() { 453 if (this._acAddrSelectList) { 454 this._acAddrSelectList.reset(); 455 this._acAddrSelectList.show(false); 456 } 457 DwtDialog.prototype.popdown.call(this); 458 }; 459 460 // 461 // DwtBaseDialog methods 462 // 463 464 ZmShareSearchDialog.prototype._createHtmlFromTemplate = function(templateId, data) { 465 DwtDialog.prototype._createHtmlFromTemplate.apply(this, arguments); 466 467 // create form 468 var params = { 469 parent: this, 470 className: "ZmShareSearchView", 471 form: { 472 template: this.CONTENT_TEMPLATE, 473 items: [ 474 { id: "FILTER", type: "DwtInputField", hint: ZmMsg.sharedFoldersFilterHint, 475 onchange: "this.parent._filterResults()" 476 }, 477 { id: "TREE", type: "ZmShareTreeView", style: DwtTree.CHECKEDITEM_STYLE }, 478 { id: "EMAIL", type: "DwtInputField", hint: ZmMsg.sharedFoldersUserSearchHint }, 479 { id: "SEARCH", type: "DwtButton", label: ZmMsg.searchInput, 480 enabled: "get('EMAIL')", onclick: "this.parent._doUserSearch(get('EMAIL'))" 481 }, 482 { id: "APP", type: "DwtSelect", items: this._getAppOptions(), onchange: "this.parent._filterResults()" 483 484 } 485 ] 486 }, 487 id: "ZmShareSearchView" 488 }; 489 this._form = new DwtForm(params); 490 this._form.setScrollStyle(DwtControl.CLIP); 491 this.setView(this._form); 492 493 var inputEl = this._form.getControl("EMAIL").getInputElement(); 494 var onkeyupHandlers = [inputEl.onkeyup]; 495 if (this._acAddrSelectList) { 496 this._acAddrSelectList.handle(inputEl); 497 onkeyupHandlers.push(inputEl.onkeyup); 498 } 499 onkeyupHandlers.push(AjxCallback.simpleClosure(this._handleEmailEnter, this)); 500 501 var handler = AjxCallback.simpleClosure(ZmShareSearchDialog.__onKeyUp, window, onkeyupHandlers); 502 Dwt.setHandler(inputEl, DwtEvent.ONKEYUP, handler); 503 }; 504 505 ZmShareSearchDialog.__onKeyUp = function(handlers, htmlEvent) { 506 for (var i = 0; i < handlers.length; i++) { 507 handlers[i](htmlEvent); 508 } 509 }; 510 511 ZmShareSearchDialog.prototype._handleEmailEnter = function(htmlEvent) { 512 // TODO: on enter, run search 513 if (false) { 514 this._doUserSearch(this.getValue("EMAIL")); 515 } 516 }; 517 518 /** 519 * Gets the include applications options. 520 * 521 * @return {Array} an array of include shares options 522 */ 523 ZmShareSearchDialog.prototype._getAppOptions = function() { 524 var options = []; 525 options.push({value: "", label: ZmMsg.allApplications}); 526 for (var i = 0; i < ZmShareSearchDialog._APP_TYPES.length; i++) { 527 var appType = ZmShareSearchDialog._APP_TYPES[i]; 528 var key = ZmShareSearchDialog._APP_KEY[appType]; 529 var appEnabled = appCtxt.get(ZmApp.SETTING[appType]); 530 if (appEnabled) { 531 var shareKey = ZmApp.ORGANIZER[appType]; 532 if (AjxUtil.isArray1(ZmOrganizer.VIEWS[shareKey])) { 533 options.push({id: appType, value: ZmOrganizer.VIEWS[shareKey][0], label: ZmMsg[key]}); 534 } 535 } 536 } 537 538 return options; 539 }; 540 541 ZmShareSearchDialog.prototype._selectApplicationOption = function() { 542 var activeApp = appCtxt.getCurrentApp(); 543 var appSelect = this._form.getControl("APP"); 544 var appOptions = this._getAppOptions(); 545 546 if (!activeApp || !appSelect || !appOptions) 547 return; 548 549 for (var i=0; i<appOptions.length; i++) { 550 if (appOptions[i].hasOwnProperty('id') && 551 appOptions[i].id == activeApp.getName()) { 552 appSelect.setSelectedValue(appOptions[i].value); 553 return; 554 } 555 } 556 557 }; 558