1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 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) 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 task application class. 27 */ 28 29 /** 30 * Creates the task application. 31 * @class 32 * This class represents the task application. 33 * 34 * @param {DwtControl} container the container 35 * 36 * @extends ZmApp 37 */ 38 ZmTasksApp = function(container) { 39 ZmApp.call(this, ZmApp.TASKS, container); 40 }; 41 42 ZmTasksApp.prototype = new ZmApp; 43 ZmTasksApp.prototype.constructor = ZmTasksApp; 44 45 ZmTasksApp.prototype.isZmTasksApp = true; 46 ZmTasksApp.prototype.toString = function() { return "ZmTasksApp"; }; 47 48 49 // Organizer and item-related constants 50 ZmEvent.S_TASK = ZmId.ITEM_TASK; 51 ZmItem.TASK = ZmEvent.S_TASK; 52 ZmOrganizer.TASKS = ZmEvent.S_TASK; 53 54 // App-related constants 55 ZmApp.TASKS = ZmId.APP_TASKS; 56 ZmApp.CLASS[ZmApp.TASKS] = "ZmTasksApp"; 57 ZmApp.SETTING[ZmApp.TASKS] = ZmSetting.TASKS_ENABLED; 58 ZmApp.LOAD_SORT[ZmApp.TASKS] = 45; 59 ZmApp.QS_ARG[ZmApp.TASKS] = "tasks"; 60 61 ZmTasksApp.REMINDER_START_DELAY = 10000; 62 63 64 // Construction 65 66 ZmTasksApp.prototype._defineAPI = 67 function() { 68 AjxDispatcher.setPackageLoadFunction("TasksCore", new AjxCallback(this, this._postLoadCore)); 69 AjxDispatcher.setPackageLoadFunction("Tasks", new AjxCallback(this, this._postLoad, ZmOrganizer.TASKS)); 70 AjxDispatcher.registerMethod("GetTaskListController", ["TasksCore", "Tasks"], new AjxCallback(this, this.getTaskListController)); 71 AjxDispatcher.registerMethod("GetTaskController", ["TasksCore", "Tasks"], new AjxCallback(this, this.getTaskController)); 72 }; 73 74 ZmTasksApp.prototype._registerOperations = 75 function() { 76 // ZmOperation.registerOp(ZmId.OP_MOUNT_TASK_FOLDER, {textKey:"mountTaskFolder", image:"TaskList"}); 77 ZmOperation.registerOp(ZmId.OP_NEW_TASK, {textKey:"newTask", tooltipKey:"newTaskTooltip", image:"NewTask", shortcut:ZmKeyMap.NEW_TASK}); 78 ZmOperation.registerOp(ZmId.OP_NEW_TASK_FOLDER, {textKey:"newTaskFolder", tooltipKey:"newTaskFolderTooltip", image:"NewTaskList"}); 79 ZmOperation.registerOp(ZmId.OP_SHARE_TASKFOLDER, {textKey:"shareTaskFolder", image:"TaskList"}); 80 ZmOperation.registerOp(ZmId.OP_PRINT_TASK, {textKey:"printTask", image:"Print", shortcut:ZmKeyMap.PRINT}, ZmSetting.PRINT_ENABLED); 81 ZmOperation.registerOp(ZmId.OP_PRINT_TASKFOLDER, {textKey:"printTaskFolder", image:"Print"}, ZmSetting.PRINT_ENABLED); 82 ZmOperation.registerOp(ZmId.OP_SORTBY_MENU, {tooltipKey:"viewTooltip", textKey:"taskFilterBy", image:"SplitPane", textPrecedence:80}); 83 ZmOperation.registerOp(ZmId.OP_MARK_AS_COMPLETED, {tooltipKey:"markAsCompleted", textKey:"markAsCompleted", image:"CheckboxChecked", textPrecedence:80}); 84 }; 85 86 ZmTasksApp.prototype._registerSettings = 87 function(settings) { 88 settings = settings || appCtxt.getSettings(); 89 settings.registerSetting("READING_PANE_LOCATION_TASKS", {name:"zimbraPrefTasksReadingPaneLocation", type:ZmSetting.T_PREF, dataType:ZmSetting.D_STRING, defaultValue:ZmSetting.RP_BOTTOM, isImplicit:true}); 90 settings.registerSetting("TASKS_FILTERBY", {name:"zimbraPrefTasksFilterBy", type:ZmSetting.T_PREF, dataType:ZmSetting.D_STRING, defaultValue:ZmSetting.TASK_FILTER_ALL, isImplicit:true}); 91 if (!appCtxt.get(ZmSetting.HIGHLIGHT_OBJECTS)) 92 settings.registerSetting("HIGHLIGHT_OBJECTS", {name:"zimbraMailHighlightObjectsMaxSize", type:ZmSetting.T_COS, dataType:ZmSetting.D_INT, defaultValue:70}); 93 }; 94 95 ZmTasksApp.prototype._registerItems = 96 function() { 97 ZmItem.registerItem(ZmItem.TASK, 98 {app: ZmApp.TASKS, 99 nameKey: "task", 100 icon: "TasksApp", 101 soapCmd: "ItemAction", 102 itemClass: "ZmTask", 103 node: "task", 104 organizer: ZmOrganizer.TASKS, 105 dropTargets: [ZmOrganizer.TAG, ZmOrganizer.TASKS], 106 searchType: "task", 107 resultsList: 108 AjxCallback.simpleClosure(function(search) { 109 AjxDispatcher.require("TasksCore"); 110 return new ZmList(ZmItem.TASK, search); 111 }, this) 112 }); 113 }; 114 115 ZmTasksApp.prototype._registerOrganizers = 116 function() { 117 ZmOrganizer.registerOrg(ZmOrganizer.TASKS, 118 {app: ZmApp.TASKS, 119 nameKey: "tasksFolder", 120 defaultFolder: ZmFolder.ID_TASKS, 121 soapCmd: "FolderAction", 122 firstUserId: 256, 123 orgClass: "ZmTaskFolder", 124 orgPackage: "TasksCore", 125 treeController: "ZmTaskTreeController", 126 labelKey: "taskLists", 127 itemsKey: "tasks", 128 folderKey: "tasksFolder", 129 hasColor: true, 130 defaultColor: ZmOrganizer.C_NONE, 131 treeType: ZmOrganizer.FOLDER, 132 views: ["task"], 133 createFunc: "ZmOrganizer.create", 134 compareFunc: "ZmFolder.sortCompareNonMail", 135 deferrable: true, 136 newOp: ZmOperation.NEW_TASK_FOLDER, 137 displayOrder: 100 138 }); 139 }; 140 141 ZmTasksApp.prototype._setupSearchToolbar = 142 function() { 143 ZmSearchToolBar.addMenuItem(ZmItem.TASK, 144 {msgKey: "tasks", 145 tooltipKey: "searchTasks", 146 icon: "TasksApp", 147 shareIcon: "SharedTaskList", 148 setting: ZmSetting.TASKS_ENABLED, 149 id: ZmId.getMenuItemId(ZmId.SEARCH, ZmId.ITEM_TASK), 150 disableOffline:true 151 }); 152 }; 153 154 ZmTasksApp.prototype._registerApp = 155 function() { 156 var newItemOps = {}; 157 newItemOps[ZmOperation.NEW_TASK] = "task"; 158 159 var newOrgOps = {}; 160 newOrgOps[ZmOperation.NEW_TASK_FOLDER] = "tasksFolder"; 161 162 var actionCodes = {}; 163 actionCodes[ZmKeyMap.NEW_TASK] = ZmOperation.NEW_TASK; 164 165 ZmApp.registerApp(ZmApp.TASKS, 166 {mainPkg: "Tasks", 167 nameKey: "tasks", 168 icon: "TasksApp", 169 textPrecedence: 20, 170 chooserTooltipKey: "goToTasks", 171 defaultSearch: ZmItem.TASK, 172 organizer: ZmOrganizer.TASKS, 173 overviewTrees: [ZmOrganizer.TASKS, ZmOrganizer.SEARCH, ZmOrganizer.TAG], 174 newItemOps: newItemOps, 175 newOrgOps: newOrgOps, 176 actionCodes: actionCodes, 177 searchTypes: [ZmItem.TASK], 178 gotoActionCode: ZmKeyMap.GOTO_TASKS, 179 newActionCode: ZmKeyMap.NEW_TASK, 180 chooserSort: 35, 181 defaultSort: 25, 182 searchResultsTab: true 183 }); 184 }; 185 186 // App API 187 188 ZmTasksApp.prototype.postNotify = 189 function(notify) { 190 if (this._checkReplenishListView) { 191 var tasks = notify.modified && notify.modified.task; 192 var item; 193 if (tasks) { 194 item = tasks[0].items[0]; 195 } 196 this._checkReplenishListView._checkReplenish(item, true); 197 this._checkReplenishListView = null; 198 } 199 }; 200 201 ZmTasksApp.prototype.handleOp = 202 function(op, params) { 203 switch (op) { 204 case ZmOperation.NEW_TASK: { 205 params = params || {}; 206 var loadCallback = new AjxCallback(this, this._handleLoadNewTask, [params]); 207 AjxDispatcher.require(["TasksCore", "Tasks"], false, loadCallback, null, true); 208 break; 209 } 210 case ZmOperation.NEW_TASK_FOLDER: { 211 var loadCallback = new AjxCallback(this, this._handleLoadNewTaskFolder); 212 AjxDispatcher.require(["TasksCore", "Tasks"], false, loadCallback, null, true); 213 break; 214 } 215 } 216 }; 217 218 ZmTasksApp.prototype._handleLoadNewTask = 219 function(params) { 220 params.folderId = params.folderId || this.getTaskListController()._folderId; 221 AjxDispatcher.run("GetTaskController").show((new ZmTask(null, null, params && params.folderId))); 222 }; 223 224 ZmTasksApp.prototype._handleLoadNewTaskFolder = 225 function() { 226 appCtxt.getAppViewMgr().popView(true, ZmId.VIEW_LOADING); // pop "Loading..." page 227 var dialog = appCtxt.getNewTaskFolderDialog(); 228 if (!this._newTaskFolderCb) { 229 this._newTaskFolderCb = new AjxCallback(this, this._newTaskFolderCallback); 230 } 231 ZmController.showDialog(dialog, this._newTaskFolderCb); 232 }; 233 234 /** 235 * Checks for the creation of a tasks folder or a mount point to one. 236 * 237 * @param {Hash} creates a hash of create notifications 238 * @param {Boolean} force if <code>true</code>, force the create 239 * 240 */ 241 ZmTasksApp.prototype.createNotify = 242 function(creates, force) { 243 if (!creates["folder"] && !creates["task"] && !creates["link"]) { return; } 244 if (!force && this._deferNotifications("create", creates)) { return; } 245 246 for (var name in creates) { 247 var list = creates[name]; 248 if (!list) { continue; } 249 250 for (var i = 0; i < list.length; i++) { 251 var create = list[i]; 252 if (appCtxt.cacheGet(create.id)) { continue; } 253 254 if (name == "folder") { 255 this._handleCreateFolder(create, ZmOrganizer.TASKS); 256 } else if (name == "link") { 257 this._handleCreateLink(create, ZmOrganizer.TASKS); 258 } else if (name == "task") { 259 // bug fix #29833 - always attempt to process new tasks 260 var taskList = AjxDispatcher.run("GetTaskListController").getList(); 261 if (taskList && 262 !AjxUtil.isEmpty(create.inv) && 263 !AjxUtil.isEmpty(create.inv[0].comp)) { 264 var filter = taskList.controller.getAllowableTaskStatus(); 265 var taskStatus = create.inv[0].comp[0].status; 266 if (!filter || filter.indexOf(taskStatus) !== -1) { 267 taskList.notifyCreate(create); 268 } 269 } 270 } 271 } 272 } 273 }; 274 275 // Public methods 276 277 ZmTasksApp.prototype.launch = 278 function(params, callback) { 279 this._setLaunchTime(this.toString(), new Date()); 280 var loadCallback = new AjxCallback(this, this._handleLoadLaunch, callback); 281 AjxDispatcher.require(["TasksCore", "Tasks"], true, loadCallback, null, true); 282 }; 283 284 ZmTasksApp.prototype._handleLoadLaunch = 285 function(callback) { 286 var acct = this._getExternalAccount(); 287 this.search(null, null, null, null, (acct && acct.name)); 288 if (callback) { callback.run(); } 289 }; 290 291 ZmTasksApp.prototype.getNewButtonProps = 292 function() { 293 return { 294 text: ZmMsg.newTask, 295 tooltip: ZmMsg.createNewTask, 296 icon: "NewTask", 297 iconDis: "NewTaskDis", 298 defaultId: ZmOperation.NEW_TASK, 299 disabled: !this.containsWritableFolder() 300 }; 301 }; 302 303 /** 304 * Shows the search results. 305 * 306 * @param {Hash} results the search results 307 * @param {AjxCallback} callback the callback 308 */ 309 ZmTasksApp.prototype.showSearchResults = 310 function(results, callback, searchResultsController) { 311 var loadCallback = this._handleLoadShowSearchResults.bind(this, results, callback, searchResultsController); 312 AjxDispatcher.require("Tasks", false, loadCallback, null, true); 313 }; 314 315 ZmTasksApp.prototype._handleLoadShowSearchResults = 316 function(results, callback, searchResultsController) { 317 var folderId = results && results.search && results.search.isSimple() && results.search.folderId || null; 318 var sessionId = searchResultsController ? searchResultsController.getCurrentViewId() : ZmApp.MAIN_SESSION; 319 var controller = AjxDispatcher.run("GetTaskListController", sessionId, searchResultsController); 320 controller.show(results, folderId); 321 this._setLoadedTime(this.toString(), new Date()); 322 if (callback) { 323 callback.run(controller); 324 } 325 }; 326 327 ZmTasksApp.prototype.runRefresh = 328 function() { 329 if (window.ZmTaskListController === undefined) { //app not loaded yet - no need to update anything. 330 return; 331 } 332 AjxDispatcher.run("GetTaskListController").runRefresh(); 333 }; 334 335 336 // common API shared by calendar app 337 338 /** 339 * Gets the list controller. 340 * 341 * @return {ZmTaskListController} the controller 342 */ 343 ZmTasksApp.prototype.getListController = 344 function() { 345 return AjxDispatcher.run("GetTaskListController"); 346 }; 347 348 /** 349 * Gets the list controller. 350 * 351 * @return {ZmTaskListController} the controller 352 */ 353 ZmTasksApp.prototype.getTaskListController = 354 function(sessionId, searchResultsController) { 355 return this.getSessionController({controllerClass: "ZmTaskListController", 356 sessionId: sessionId || ZmApp.MAIN_SESSION, 357 searchResultsController: searchResultsController}); 358 }; 359 360 /** 361 * Gets the controller. 362 * 363 * @return {ZmTaskController} the controller 364 */ 365 ZmTasksApp.prototype.getTaskController = 366 function(sessionId) { 367 return this.getSessionController({controllerClass: "ZmTaskController", 368 sessionId: sessionId}); 369 }; 370 371 /** 372 * Creates a task from a mail item. 373 * 374 * @param {ZmMailMsg} msg the message 375 * @param {Date} date the date 376 */ 377 ZmTasksApp.prototype.newTaskFromMailItem = 378 function(msg, date) { 379 var subject = msg.subject || ""; 380 if (msg instanceof ZmConv) { 381 msg = msg.getFirstHotMsg(); 382 } 383 msg.load({getHtml:false, callback:new AjxCallback(this, this._msgLoadedCallback, [msg, date, subject])}); 384 }; 385 386 /** 387 * @private 388 */ 389 ZmTasksApp.prototype._msgLoadedCallback = 390 function(mailItem, date, subject) { 391 var t = new ZmTask(); 392 t.setEndDate(AjxDateUtil.roundTimeMins(date, 30)); 393 t.setFromMailMessage(mailItem, subject); 394 this.getTaskController().show(t, ZmCalItem.MODE_NEW, true); 395 }; 396 397 /** 398 * Performs a search. 399 * 400 * @param {ZmFolder} folder the folder 401 * @param {Date} startDate the start date 402 * @param {Date} endDate the end date 403 * @param {AjxCallback} callback the callback 404 * @param {String} accountName the account name 405 */ 406 ZmTasksApp.prototype.search = 407 function(folder, startDate, endDate, callback, accountName) { 408 var query = folder ? folder.createQuery() : ""; 409 query = query || (appCtxt.isExternalAccount() ? "inid:" + this.getDefaultFolderId() : "in:tasks"); 410 411 var params = { 412 query: query, 413 types: [ZmItem.TASK], 414 limit: this.getLimit(), 415 searchFor: ZmItem.TASK, 416 callback: callback, 417 accountName: (accountName || (folder && folder.getAccount().name)) 418 }; 419 var sc = appCtxt.getSearchController(); 420 sc.searchAllAccounts = false; 421 sc.search(params); 422 }; 423 424 /** 425 * @private 426 */ 427 ZmTasksApp.prototype._newTaskFolderCallback = 428 function(parent, name, color) { 429 var dialog = appCtxt.getNewTaskFolderDialog(); 430 dialog.popdown(); 431 var oc = appCtxt.getOverviewController(); 432 oc.getTreeController(ZmOrganizer.TASKS)._doCreate(parent, name, color); 433 }; 434 435 /** 436 * Gets the list of checked calendar ids. If calendar packages are not loaded, 437 * gets the list from deferred folder ids. 438 * 439 * @param {Boolean} localOnly if <code>true</code>, use local calendar only 440 * @return {Array} an array of ids 441 */ 442 ZmTasksApp.prototype.getTaskFolderIds = 443 function(localOnly) { 444 var folderIds = []; 445 if (AjxDispatcher.loaded("TasksCore")) { 446 folderIds = AjxDispatcher.run("GetTaskListController").getTaskFolderIds(localOnly); 447 } else { 448 // will be used in reminder dialog 449 this._folderNames = {}; 450 for (var i = 0; i < this._deferredFolders.length; i++) { 451 var params = this._deferredFolders[i]; 452 //var str = (params && params.obj && params.obj.f) ? params.obj.f : ""; 453 //if (str && (str.indexOf(ZmOrganizer.FLAG_CHECKED) != -1)) { 454 if (localOnly && params.obj.zid != null) { 455 continue; 456 } 457 folderIds.push(params.obj.id); 458 // _folderNames are used when deferred folders are not created 459 // and calendar name is required. example: calendar name 460 // requirement in reminder module 461 this._folderNames[params.obj.id] = params.obj.name; 462 //} 463 } 464 } 465 return folderIds; 466 }; 467 468 /** 469 * Gets the name of the calendar with specified id. 470 * 471 * @param {String} id the id of the task 472 * @return {String} the name 473 */ 474 ZmTasksApp.prototype.getTaskFolderName = 475 function(id) { 476 return appCtxt.getById(id) ? appCtxt.getById(id).name : this._folderNames[id]; 477 }; 478 479 480 /** 481 * Gets the reminder controller. 482 * 483 * @return {ZmReminderController} the controller 484 */ 485 ZmTasksApp.prototype.getReminderController = 486 function() { 487 if (!this._reminderController) { 488 AjxDispatcher.require("TasksCore"); 489 var taskMgr = appCtxt.getTaskManager(); 490 this._reminderController = taskMgr.getReminderController(); 491 this._reminderController._calController = taskMgr; 492 this._reminderController.refresh(); 493 } 494 return this._reminderController; 495 }; 496 497 498 /** 499 * Creates a new button with a reminder options as its menu. 500 * 501 * @param {DwtComposite} parent the parent 502 * @param {String} buttonId the button id to fetch inside DOM and append DwtButton to 503 * @param {AjxListener} buttonListener the listener to call when date button is pressed 504 * @param {AjxListener} menuSelectionListener the listener to call when date is selected in {@link DwtCalendar} 505 */ 506 ZmTasksApp.createpCompleteButton = 507 function(parent, buttonId, buttonListener, menuSelectionListener) { 508 // create button 509 var pCompleteButton = new DwtButton({parent:parent}); 510 pCompleteButton.addDropDownSelectionListener(buttonListener); 511 pCompleteButton.setData(Dwt.KEY_ID, buttonId); 512 pCompleteButton.setSize("25"); 513 514 // create menu for button 515 var pCompleteMenu = new DwtMenu({parent:pCompleteButton, style:DwtMenu.DROPDOWN_STYLE}); 516 pCompleteMenu.setSize("100"); 517 pCompleteButton.setMenu(pCompleteMenu, true); 518 519 var formatter = new AjxMessageFormat(AjxMsg.percentageString); 520 for (var i = 0; i <= 100; i += ZmTask.PCOMPLETE_INT) { 521 var mi = new DwtMenuItem({parent: pCompleteMenu, style: DwtMenuItem.NO_STYLE}); 522 mi.setText((formatter.format(i))); 523 mi.setData("value", i); 524 if(menuSelectionListener) mi.addSelectionListener(menuSelectionListener); 525 } 526 527 // reparent and cleanup 528 pCompleteButton.reparentHtmlElement(buttonId); 529 delete buttonId; 530 531 return pCompleteButton; 532 }; 533