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 /**
 25  * @overview
 26  */
 27 
 28 /**
 29  * @class
 30  * Represents an undoable action (e.g. move an item)
 31  * This class is a generic superclass that does very little on its own; the real work is being done in subclasses
 32  * 
 33  * @extends		ZmModel
 34  */
 35 
 36 ZmAction = function() {
 37 	ZmModel.call(this, ZmEvent.S_ACTION);
 38 	this._complete = false;
 39 };
 40 
 41 ZmAction.prototype = new ZmModel;
 42 ZmAction.prototype.constructor = ZmAction;
 43 
 44 ZmAction.ACTION_ZMACTION = "ZmAction";
 45 ZmAction.ACTION_ZMITEMACTION = "ZmItemAction";
 46 ZmAction.ACTION_ZMITEMMOVEACTION = "ZmItemMoveAction";
 47 ZmAction.ACTION_ZMITEMTRASHACTION = "ZmItemTrashAction";
 48 ZmAction.ACTION_ZMORGANIZERACTION = "ZmOrganizerAction";
 49 ZmAction.ACTION_ZMORGANIZERMOVEACTION = "ZmOrganizerMoveAction";
 50 ZmAction.ACTION_ZMCOMPOSITEACTION = "ZmCompositeAction";
 51 
 52 ZmAction.prototype.type = ZmAction.ACTION_ZMITEMACTION;
 53 
 54 ZmAction.prototype.toString = function() {
 55 	return "ZmAction";
 56 };
 57 
 58 ZmAction.prototype.undo = function() {
 59 	//override me
 60 };
 61 
 62 ZmAction.prototype.redo = function() {
 63 	//override me
 64 };
 65 
 66 ZmAction.prototype.setComplete = function() {
 67 	if (!this._complete) {
 68 		this._complete = true;
 69 		this._notify(ZmEvent.E_COMPLETE);
 70 	}
 71 };
 72 
 73 ZmAction.prototype.getComplete = function() {
 74 	return this._complete;
 75 };
 76 
 77 ZmAction.prototype.onComplete = function(callback) {
 78 	if (this._complete) {
 79 		callback.run(this);
 80 	} else {
 81 		this.addChangeListener(new AjxListener(this, this._handleComplete, [callback]));
 82 	}
 83 };
 84 
 85 ZmAction.prototype._handleComplete = function(callback, event) {
 86 	if (event.event===ZmEvent.E_COMPLETE) {
 87 		callback.run(this);
 88 	}
 89 };
 90 
 91 /**
 92  * @class
 93  * Represents an undoable action on an item
 94  * This class is a generic superclass that does very little on its own; the real work is being done in subclasses
 95  * 
 96  * @extends		ZmAction
 97  *
 98  * @param {ZmItem}	item	The item to perform the action on
 99  * @param {String}	op		The operation to perform (e.g. "move" or "trash")	
100  */
101 
102 ZmItemAction = function(item, op) {
103 	if (!arguments.length) return;
104 	ZmAction.call(this);
105 	this._item = item;
106 	this._op = op;
107 };
108 
109 ZmItemAction.prototype = new ZmAction;
110 ZmItemAction.prototype.constructor = ZmItemAction;
111 ZmItemAction.prototype.type = ZmAction.ACTION_ZMITEMACTION;
112 
113 ZmItemAction.prototype.toString = function() {
114 	return "ZmItemAction";
115 };
116 
117 ZmItemAction.prototype.getItem = function() {
118 	return this._item;
119 };
120 
121 ZmItemAction.prototype.getOp = function() {
122 	return this._op;
123 };
124 
125 /**
126  * @class
127  * Represents an undoable action on an organizer
128  * This class is a generic superclass that does very little on its own; the real work is being done in subclasses
129  * 
130  * @extends		ZmAction
131  *
132  * @param {ZmOrganizer}		organizer	The organizer to perform the action on
133  * @param {String}			op			The operation to perform (e.g. "move")
134  */
135 
136 ZmOrganizerAction = function(organizer, op) {
137 	if (!arguments.length) return;
138 	ZmAction.call(this);
139 	this._organizer = organizer;
140 	this._op = op;
141 };
142 
143 ZmOrganizerAction.prototype = new ZmAction;
144 ZmOrganizerAction.prototype.constructor = ZmOrganizerAction;
145 ZmOrganizerAction.prototype.type = ZmAction.ACTION_ZMORGANIZERACTION;
146 
147 ZmOrganizerAction.prototype.toString = function() {
148 	return "ZmOrganizerAction";
149 };
150 
151 ZmOrganizerAction.prototype.getOrganizer = function() {
152 	return this._organizer;
153 };
154 
155 ZmOrganizerAction.prototype.getOp = function() {
156 	return this._op;
157 };
158 
159 /**
160  * @class
161  * Represents an undoable move action on an item
162  * 
163  * @extends		ZmItemAction
164  *
165  * @param {ZmItem}	item			Item to perform the move on
166  * @param {int}		fromFolderId	Original folder id of the item
167  * @param {int}		toFolderId		Destination folder id of the item
168  * @param {String}	op				The operation to perform (e.g. "move")
169  */
170 
171 ZmItemMoveAction = function(item, fromFolderId, toFolderId, op) {
172 	ZmItemAction.call(this, item, op);
173 	this._fromFolderId = fromFolderId;
174 	this._toFolderId = toFolderId;
175 };
176 
177 ZmItemMoveAction.prototype = new ZmItemAction;
178 ZmItemMoveAction.prototype.constructor = ZmItemMoveAction;
179 
180 ZmItemMoveAction.prototype.type = ZmAction.ACTION_ZMITEMMOVEACTION;
181 
182 ZmItemMoveAction.prototype.toString = function() {
183 	return "ZmItemMoveAction";
184 };
185 
186 ZmItemMoveAction.UNDO_MSG = {
187 	"move" : ZmMsg.actionUndoMove,
188 	"trash": ZmMsg.actionUndoTrash,
189 	"spam": ZmMsg.actionUndoMarkAsJunk,
190 	"!spam": ZmMsg.actionUndoMarkAsNotJunk
191 };
192 
193 ZmItemMoveAction.prototype.getFromFolderId = function() {
194 	return this._fromFolderId;
195 };
196 
197 ZmItemMoveAction.prototype.getToFolderId = function() {
198 	return this._toFolderId;
199 };
200 
201 ZmItemMoveAction.prototype._doMove = function(callback, errorCallback, folderId) {
202 
203 	var items = ZmItemMoveAction._realizeItems(this._item), // probably unnecessary since conv forces multipleUndo
204 		list = items[0] && items[0].list;
205 
206 	list.moveItems({
207 		items:			items,
208 		folder:			appCtxt.getById(folderId),
209 		noUndo:			true,
210 		finalCallback:	this._handleDoMove.bind(this, this._item.folderId, folderId),
211 		fromFolderId:   this._toFolderId
212 	});
213 };
214 
215 ZmItemMoveAction.prototype._handleDoMove = function(oldFolderId, newFolderId, params) {
216 	var lists = [];
217 	for (var id in params.idHash) {
218 		var item = params.idHash[id];
219 		if (item instanceof ZmConv)
220 			item.folderId = newFolderId;
221 		var list = item && item.list;
222 		if (AjxUtil.indexOf(lists, list)==-1)
223 			lists.push(list);
224 	}
225 	for (var i=0; i<lists.length; i++) {
226 		lists[i]._notify(ZmEvent.E_MOVE, {oldFolderId:oldFolderId});
227 	}
228 	ZmListController.handleProgress({state:ZmListController.PROGRESS_DIALOG_CLOSE});
229 	ZmBaseController.showSummary(params.actionSummary);
230 };
231 
232 ZmItemMoveAction.prototype.undo = function(callback, errorCallback) {
233 	this._doMove(callback, errorCallback, this._fromFolderId);
234 };
235 
236 ZmItemMoveAction.prototype.redo = function(callback, errorCallback) {
237 	this._doMove(callback, errorCallback, this._toFolderId);
238 };
239 
240 ZmItemMoveAction.multipleUndo = function(actions, redo, fromFolderId) {
241 
242 	var sortingTable = {};
243 	for (var i = 0; i < actions.length; i++) {
244 		var action = actions[i];
245 		if (action instanceof ZmItemMoveAction) {
246 			var from = action.getFromFolderId();
247 			var to = action.getToFolderId();
248 			var item = action.getItem();
249 			var type = (item && item.list && item.list.type) || 0;
250 			if (!sortingTable[from]) sortingTable[from] = {};
251 			if (!sortingTable[from][to]) sortingTable[from][to] = {};
252 			if (!sortingTable[from][to][type]) sortingTable[from][to][type] = [];
253 			sortingTable[from][to][type].push(action);
254 		}
255 	}
256 
257 	for (var from in sortingTable) {
258 		for (var to in sortingTable[from]) {
259 			for (var type in sortingTable[from][to]) {
260 				var subset = sortingTable[from][to][type];
261 				var items = [];
262 				var list = null;
263 				for (var i = 0; i < subset.length; i++) {
264 					var action = subset[i];
265 					var item = action.getItem();
266 					items.push(item);
267 				}
268 				items = ZmItemMoveAction._realizeItems(items);
269 				list = items[0] && items[0].list;
270 				if (list) {
271 					list.moveItems({
272 						items:          items,
273 						folder:         appCtxt.getById(redo ? to : from),
274 						noUndo:         true,
275 						fromFolderId:   fromFolderId
276 					});
277 				}
278 			}
279 		}
280 	}
281 };
282 
283 ZmItemMoveAction.multipleRedo = function(actions) {
284 	ZmItemMoveAction.multipleUndo(actions, true);
285 };
286 
287 // Creates ZmMailMsg out of anonymous msg-like objects
288 ZmItemMoveAction._realizeItems = function(items) {
289 
290 	var list, msg;
291 	return AjxUtil.map(AjxUtil.toArray(items), function(item) {
292 		if (item.isConvMsg) {
293 			list = list || new ZmMailList(ZmItem.MSG);
294 			msg = new ZmMailMsg(item.id, list, true);
295 			msg.folderId = item.folderId;
296 			return msg;
297 		}
298 		else {
299 			return item;
300 		}
301 	});
302 };
303 
304 /**
305  * @class
306  * Represents an undoable move action on an organizer
307  * 
308  * @extends		ZmOrganizerAction
309  *
310  * @param {ZmOrganizer}	organizer		Organizer to perform the move on
311  * @param {int}			fromFolderId	Original parent folder id of the organizer
312  * @param {int}			toFolderId		Destination parent folder id of the organizer
313  * @param {String}		op				The operation to perform (e.g. "move")
314  */
315 
316 ZmOrganizerMoveAction = function(organizer, fromFolderId, toFolderId, op) {
317 	ZmOrganizerAction.call(this, organizer, op);
318 	this._fromFolderId = fromFolderId;
319 	this._toFolderId = toFolderId;
320 };
321 
322 ZmOrganizerMoveAction.prototype = new ZmOrganizerAction;
323 ZmOrganizerMoveAction.prototype.constructor = ZmOrganizerMoveAction;
324 
325 ZmOrganizerMoveAction.prototype.type = ZmAction.ACTION_ZMORGANIZERMOVEACTION;
326 
327 ZmOrganizerMoveAction.prototype.toString = function() {
328 	return "ZmOrganizerMoveAction";
329 };
330 
331 ZmOrganizerMoveAction.prototype.getFromFolderId = function() {
332 	return this._fromFolderId;
333 };
334 
335 ZmOrganizerMoveAction.prototype.getToFolderId = function() {
336 	return this._toFolderId;
337 };
338 
339 ZmOrganizerMoveAction.prototype._doMove = function(callback, errorCallback, folderId) {
340 	var folder = appCtxt.getById(folderId);
341 	if (folder) {
342 		this._organizer.move(folder, true);
343 	}
344 };
345 
346 ZmOrganizerMoveAction.prototype.undo = function(callback, errorCallback) {
347 	this._doMove(callback, errorCallback, this._fromFolderId);
348 };
349 
350 ZmOrganizerMoveAction.prototype.redo = function(callback, errorCallback) {
351 	this._doMove(callback, errorCallback, this._toFolderId);
352 };
353 
354 ZmOrganizerMoveAction.multipleUndo = function(actions, redo) {
355 	for (var i=0; i<actions.length; i++) {
356 		var action = actions[i];
357 		if (action instanceof ZmOrganizerMoveAction) {
358 			action._doMove(null, null, redo ? this._toFolderId : this._fromFolderId);
359 		}
360 	}
361 };
362 
363 ZmOrganizerMoveAction.multipleRedo = function(actions) {
364 	ZmItemMoveAction.multipleUndo(actions, true);
365 };
366 
367 /**
368  * @class
369  * Represents a collection of undoable actions that will be performed as a whole
370  * 
371  * @extends		ZmAction
372  *
373  */
374 
375 ZmCompositeAction = function(toFolderId) {
376 	ZmAction.call(this);
377 	this._actions = {};
378 	this._toFolderId = toFolderId;
379 };
380 
381 ZmCompositeAction.prototype = new ZmAction;
382 ZmCompositeAction.prototype.constructor = ZmCompositeAction;
383 ZmCompositeAction.prototype.type = ZmAction.ACTION_ZMCOMPOSITEACTION;
384 
385 ZmCompositeAction.prototype.toString = function() {
386 	return "ZmCompositeAction";
387 };
388 
389 /**
390  * Add an action the the collection
391  *
392  * @param	{ZmAction}	action	An action to add
393  */
394 ZmCompositeAction.prototype.addAction = function(action) {
395 	if (action && action!=this && action instanceof ZmAction) {
396 		var type = action.type;
397 		if (!this._actions[type])
398 			this._actions[type] = [];
399 		this._actions[type].push(action);
400 	}
401 };
402 
403 ZmCompositeAction.prototype.getActions = function(type) {
404 	return this._actions[type] || [];
405 };
406 
407 ZmCompositeAction.prototype.hasActions = function(type) {
408 	return this._actions[type] && this._actions[type].length>0;
409 };
410 
411 ZmCompositeAction.prototype.undo = function(callback, errorCallback) {
412 
413 	if (this.hasActions(ZmAction.ACTION_ZMITEMMOVEACTION)) {
414 		ZmItemMoveAction.multipleUndo(this.getActions(ZmAction.ACTION_ZMITEMMOVEACTION), null, this._toFolderId);
415 	}
416 
417 	if (this.hasActions(ZmAction.ACTION_ZMORGANIZERMOVEACTION)) {
418 		ZmOrganizerMoveAction.multipleUndo(this.getActions(ZmAction.ACTION_ZMORGANIZERMOVEACTION));
419 	}
420 
421 	if (this.hasActions(ZmAction.ACTION_ZMCOMPOSITEACTION) || this.hasActions(ZmAction.ACTION_ZMITEMACTION)) {
422 		var actions = this.getActions(ZmAction.ACTION_ZMCOMPOSITEACTION).concat(this.getActions(ZmAction.ACTION_ZMITEMACTION));
423 		for (var i=0; i<actions.length; i++) {
424 			actions[i].undo();
425 		}
426 	}
427 };
428 
429 ZmCompositeAction.prototype.redo = function(callback, errorCallback) {
430 
431 	if (this.hasActions(ZmAction.ACTION_ZMITEMMOVEACTION)) {
432 		ZmItemMoveAction.multipleRedo(this.getActions(ZmAction.ACTION_ZMITEMMOVEACTION));
433 	}
434 
435 	if (this.hasActions(ZmAction.ACTION_ZMORGANIZERMOVEACTION)) {
436 		ZmOrganizerMoveAction.multipleRedo(this.getActions(ZmAction.ACTION_ZMORGANIZERMOVEACTION));
437 	}
438 
439 	if (this.hasActions(ZmAction.ACTION_ZMCOMPOSITEACTION) || this.hasActions(ZmAction.ACTION_ZMITEMACTION) || this.hasActions(ZmAction.ACTION_ZMORGANIZERACTION)) {
440 		var actions = this.getActions(ZmAction.ACTION_ZMCOMPOSITEACTION).concat(this.getActions(ZmAction.ACTION_ZMITEMACTION)).concat(this.getActions(ZmAction.ACTION_ZMORGANIZERACTION));
441 		for (var i=0; i<actions.length; i++) {
442 			actions[i].redo();
443 		}
444 	}
445 };
446