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