1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 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) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Create a new, empty appointment list.
 26  * @constructor
 27  * @class
 28  * This class represents a list of appointments.
 29  * 
 30  * @extends		ZmList
 31  */
 32 ZmApptList = function() {
 33 	ZmList.call(this, ZmItem.APPT);
 34 };
 35 
 36 ZmApptList.prototype = new ZmList;
 37 ZmApptList.prototype.constructor = ZmApptList;
 38 
 39 ZmApptList.prototype.isZmApptList = true;
 40 ZmApptList.prototype.toString = function() { return "ZmApptList"; };
 41 
 42 ZmApptList.prototype.loadFromSummaryJs =
 43 function(appts, noCache) {
 44 	if (!appts) { return; }
 45 
 46 	for (var i = 0; i < appts.length; i++) {
 47 		var apptNode = appts[i];
 48 		var instances = apptNode ? apptNode.inst : null;
 49 		if (instances) {
 50 			var args = {list:this};
 51 			for (var j = 0; j < instances.length; j++) {
 52 				var appt = ZmAppt.createFromDom(apptNode, args, instances[j], noCache);
 53 				if (appt) this.add(appt);
 54 			}
 55 		}
 56 	}
 57 }
 58 
 59 ZmApptList.prototype.indexOf =
 60 function(obj) {
 61 	return this._vector.indexOf(obj);
 62 };
 63 
 64 ZmApptList.sortVector = 
 65 function(vec) {
 66 	vec.sort(ZmCalBaseItem.compareByTimeAndDuration);
 67 };
 68 
 69 /**
 70  * Merges all the sorted vectors in the specified array into a single sorted vector.
 71  * 
 72  * @param	{AjxVector}	vecArray		the array
 73  * @return	{AjxVector}	the resulting array
 74  */
 75 ZmApptList.mergeVectors = 
 76 function(vecArray) {
 77 	var result = new AjxVector();	
 78 	if(!vecArray) {  return result; }
 79 
 80 	// clone the single array case!
 81 	if (vecArray.length == 1) return vecArray[0].clone();
 82 	for (var i=0; i < vecArray.length; i++) result.addList(vecArray[i]);
 83 	ZmApptList.sortVector(result);
 84 	return result;
 85 };
 86 
 87 ZmApptList.toVector =
 88 function(apptList, startTime, endTime, fanoutAllDay, includeReminders) {
 89 	var result = new AjxVector();
 90 	var list = apptList.getVector();
 91 	var size = list.size();
 92 	for (var i = 0; i < size; i++) {
 93 		var ao = list.get(i);
 94 		var folder = ao.getFolder();
 95 		if (ao.isInRange(startTime, endTime) || (ao.isAlarmInRange() && includeReminders)) {
 96 			if (ao.isAllDayEvent() && !fanoutAllDay) {
 97 				result.add(ZmAppt.quickClone(ao));
 98 			} else {
 99 				ZmApptList._fanout(ao, result, startTime, endTime, fanoutAllDay, includeReminders);
100 			}
101 		}
102 	}
103 	ZmApptList.sortVector(result);
104 	return result;
105 };
106 
107 /**
108  * fanout multi-day appoints into multiple single day appts. This has nothing to do with recurrence...
109  * TODO: should be more efficient by not fanning out appts in if part of the while if they are not in the range.
110  * 
111  * @private
112  */
113 ZmApptList._fanout =
114 function(orig, result, startTime, endTime, fanoutAllDay, includeReminders) {
115 	var appt = ZmAppt.quickClone(orig);
116 	var fanoutNum = 0;
117 
118 	// HACK: Avoid "strange" appt durations that occur at transition
119 	//       days for timezones w/ DST. For example, going from DST to
120 	//       STD, the duration for a single day is 25 hours; while the
121 	//       transition from STD to DST, the duration is 23 hours. So
122 	//       we advance 12 hours (just to be safe) and then subtract
123 	//       off the extra hours.
124 	var origEndTime = orig.getEndTime();
125 	if (appt.isAllDayEvent()) {
126 		var origEndDate = new Date(origEndTime);
127 		origEndDate.setHours(0, 0, 0, 0);
128 
129 		appt.setEndDate(origEndDate);
130 		origEndTime = origEndDate.getTime();
131 	}
132 
133     //while (appt.isInRange(startTime,endTime) || (appt.isAlarmInRange() && includeReminders)) {
134     while (orig.isInRange(startTime,endTime) || (appt.isAlarmInRange() && includeReminders)) {
135 		if (appt.isMultiDay()) {
136 			var apptStartTime = appt.getStartTime();
137 			// bug 12205: If someone mistypes "2007" as "200", we get into
138 			//            a seemingly never-ending loop trying to fanout
139 			//            every day even *before* the startTime of the view.
140 			var nextDay = new Date(apptStartTime);
141 			nextDay.setDate(nextDay.getDate()+1);
142 			if (origEndTime < nextDay.getTime()) {
143 				nextDay = new Date(origEndTime);
144 			}
145 			nextDay.setHours(0,0,0,0);
146             if(AjxDateUtil.isDayShifted(nextDay)) {
147                 AjxDateUtil.rollToNextDay(nextDay);
148             }
149 
150             var slice = ZmAppt.quickClone(appt);
151             slice._fanoutFirst = (fanoutNum == 0);
152             slice._orig = orig;
153             slice.setEndDate(nextDay);
154             slice._fanoutLast = (slice.getEndTime() == origEndTime);
155             slice._fanoutNum = fanoutNum;
156             slice.uniqStartTime = slice.getStartTime();					// need to construct uniq id later
157             result.add(slice);
158 
159 			fanoutNum++;
160 			appt.setStartDate(nextDay);
161 			if (appt.getStartTime() >= appt.getEndTime())
162 				break;
163 		} else {
164 			if (orig.isInRange(startTime,endTime)  || (appt.isAlarmInRange() && includeReminders) ) {
165 				appt._fanoutFirst = fanoutNum == 0;
166 				appt._fanoutLast = appt.getEndTime() == origEndTime;
167 				if (!appt._fanoutFirst)
168 					appt._orig = orig;
169 				appt._fanoutNum = fanoutNum;
170 				appt.uniqStartTime = appt.getStartTime();						// need to construct uniq id later
171 				result.add(appt);
172 			}
173 			break;
174 		}
175 	}
176 };
177 
178 /**
179  * Gets a new appointment list containing only appointment in the given range.
180  * 
181  * @param	{Date}	startTime		the start time
182  * @param	{Date}	endTime		the end time
183  * @author {ZmApptList}	the new list
184  */
185 ZmApptList.prototype.getSubset =
186 function(startTime, endTime) {
187 	var result  = new ZmApptList();
188 	var list = this.getVector();
189 	var size = list.size();
190 	for (var i=0; i < size; i++) {
191 		var ao = list.get(i);
192 		if (ao.isInRange(startTime, endTime)) {
193 			result.add(ao);
194 		}
195 	}
196 	return result;
197 };
198 
199 
200 /**
201  * Moves a list of items to the given folder.
202  * <p>
203  * Search results are treated as though they're in a temporary folder, so that they behave as
204  * they would if they were in any other folder such as Inbox. When items that are part of search
205  * results are moved, they will disappear from the view, even though they may still satisfy the
206  * search.
207  * </p>
208  *
209  * @param	{Hash}			params					a hash of parameters
210  * @param	{Array}			params.items			a list of items to move
211  * @param	{ZmFolder}		params.folder			the destination folder
212  * @param	{Hash}			params.attrs			the additional attrs for SOAP command
213  * @param	{AjxCallback}	params.callback			the callback to run after each sub-request
214  * @param	{closure}		params.finalCallback	the callback to run after all items have been processed
215  * @param	{int}			params.count			the starting count for number of items processed
216  * @param	{boolean}		params.noUndo			true if the action is not undoable (e.g. performed as an undo)
217  * @param	{String}		params.actionTextKey	key for optional text to display in the confirmation toast instead of the default summary. May be set explicitly to null to disable the confirmation toast entirely
218  */
219 ZmApptList.prototype.moveItems =
220 function(params) {
221 	params = Dwt.getParams(arguments, ["items", "folder", "attrs", "callback", "errorCallback" ,"finalCallback", "noUndo", "actionTextKey"]);
222 
223 	var params1 = AjxUtil.hashCopy(params);
224 	params1.items = AjxUtil.toArray(params.items);
225 	params1.attrs = params.attrs || {};
226 	if (params1.folder.id == ZmFolder.ID_TRASH) {
227 		params1.actionTextKey = (params.actionTextKey !== null) ? (params.actionTextKey || 'actionTrash') : null;
228 		params1.action = "trash";
229         //This code snippet differs from the ZmList.moveItems
230         var currentView = appCtxt.getCurrentView();
231         if(currentView) {
232             var viewController = currentView.getController();
233             if(viewController) {
234                 //Since it is a drag and drop, only one item can be dragged - so get the first element from array
235                 return viewController._deleteAppointment(params1.items[0]);
236             }
237         }
238 	} else {
239 		params1.actionTextKey = (params.actionTextKey !== null) ? (params.actionTextKey || 'actionMove') : null;
240 		params1.actionArg = params.folder.getName(false, false, true);
241 		params1.action = "move";
242 		params1.attrs.l = params.folder.id;
243 	}
244 
245     if (appCtxt.multiAccounts) {
246 		// Reset accountName for multi-account to be the respective account if we're
247 		// moving a draft out of Trash.
248 		// OR,
249 		// check if we're moving to or from a shared folder, in which case, always send
250 		// request on-behalf-of the account the item originally belongs to.
251         var folderId = params.items[0].getFolderId();
252         var fromFolder = appCtxt.getById(folderId);
253 		if ((params.items[0].isDraft && params.folder.id == ZmFolder.ID_DRAFTS) ||
254 			(params.folder.isRemote()) || (fromFolder.isRemote()))
255 		{
256 			params1.accountName = params.items[0].getAccount().name;
257 		}
258 	}
259 
260     //Error Callback
261     params1.errorCallback = params.errorCallback;
262 
263 	this._itemAction(params1);
264 };
265