1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2010, 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, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  */
 27 
 28 /**
 29  * @class
 30  * Creates a stack of undoable actions (ZmAction objects)
 31  *
 32  * @param	{int}	[maxLength]		The maximum size of the stack. Defaults to 0, meaning no limit
 33  *
 34  * Adding actions to a full stack will pop the oldest actions off
 35  */
 36 ZmActionStack = function(maxLength) {
 37 	this._stack = [];
 38 	this._pointer = -1;
 39 	this._maxLength = maxLength || 0; // 0 means no limit
 40 };
 41 
 42 ZmEvent.S_ACTION			= "ACTION";
 43 
 44 ZmActionStack.validTypes	= [ZmId.ORG_FOLDER, ZmId.ITEM_MSG, ZmId.ITEM_CONV,
 45                                ZmId.ITEM_CONTACT, ZmId.ITEM_GROUP,
 46                                ZmId.ITEM_BRIEFCASE, ZmId.ORG_BRIEFCASE,
 47                                ZmId.ITEM_TASK, ZmId.ORG_TASKS
 48                               ]; // Set ZmActionStack.validTypes to false to allow all item types
 49 
 50 ZmActionStack.prototype.toString = function() {
 51 	return "ZmActionStack";
 52 };
 53 
 54 /**
 55  * Logs a raw action, interpreting the params and creates a ZmAction object that is pushed onto the stack and returned
 56  * 
 57  * @param {String}	op			operation to perform. Currently supported are "move", "trash", "spam" and "!spam"
 58  * @param {Hash}	[attrs] 	attributes for the operation. Pretty much the same as what the backend expects, e.g. "l" for the destination folderId of a move
 59 
 60  * @param {String}	[items] 	array of items to perform the action for. Valid types are specified in ZmActionStack.validTypes. Only one of [items],[item],[ids] or [id] should be specified; the first one found is used, ignoring the rest.
 61  * @param {String}	[item] 		item to perform the action for, if there is only one item. Accomplishes the same as putting the item in an array and giving it as [items]
 62  * @param {String}	[ids] 		array of ids of items to perform the action for.
 63  * @param {String}	[id] 		id of item to perform the action for, if there is only one. Accomplishes the same as putting the id in an array and giving it as [ids].
 64  */
 65 ZmActionStack.prototype.logAction = function(params) {
 66 
 67 	var op = params.op,
 68 	    items = [];
 69 
 70 	if (params.items) {
 71 		for (var i = 0; i < params.items.length; i++) {
 72 			var item = params.items[i];
 73 			if (item && this._isValidType(item.type)) {
 74 				items.push(item);
 75 			}
 76 		}
 77 	}
 78 	else if (params.item) {
 79 		if (params.item && this._isValidType(params.item.type)) {
 80 			items.push(params.item);
 81 		}
 82 	}
 83 	else if (params.ids) {
 84 		for (var i = 0; i < params.ids.length; i++) {
 85 			var item = appCtxt.getById(params.ids[i]);
 86 			if (item && this._isValidType(item.type)) {
 87 				items.push(item);
 88 			}
 89 		}
 90 	}
 91 	else if (params.id) {
 92 		var item = appCtxt.getById(params.id);
 93 		if (item && this._isValidType(item.type)) {
 94 			items.push(item);
 95 		}
 96 	}
 97 
 98 	var attrs = params.attrs;
 99 
100 	// for a conv, create a list of undoable msg moves so msgs can be restored to their disparate original folders
101 	for (var i = 0; i < items.length; i++) {
102 		var item = items[i];
103 		if (item.type === ZmItem.CONV) {
104 			var tcon = attrs && attrs.tcon;
105 			for (var msgId in item.msgFolder) {
106 				var folderId = item.msgFolder[msgId],
107 					tconCode = ZmFolder.TCON_CODE[folderId];
108 
109 				// if tcon kept us from moving a msg, no need to undo it
110 				if (!tcon || tcon.indexOf(tconCode) === -1) {
111 					items.push({
112 						isConvMsg:  true,
113 						id:         msgId,
114 						type:       ZmItem.MSG,
115 						folderId:   folderId,
116 						list:       { type: ZmItem.MSG }    // hack to expose item.list.type
117 					});
118 				}
119 			}
120 		}
121 	}
122 
123 	var multi = items.length > 1;
124 
125 	var action = null;
126 	var folderId;
127 	switch (op) {
128 		case "trash":
129 			folderId = ZmFolder.ID_TRASH;
130 			break;
131 		case "spam":
132 			folderId = ZmFolder.ID_SPAM;
133 			break;
134 		case "move":
135 		case "!spam":
136 			folderId = attrs.l;
137 			break;
138 	}
139 
140 	var folder = appCtxt.getById(folderId);
141 	if (folder && !folder.isRemote()) { // Enable undo only when destination folder exists (it should!!) and is not remote (bug #51656)
142 		switch (op) {
143 			case "trash":
144 			case "move":
145 			case "spam":
146 			case "!spam":
147 				for (var i = 0; i < items.length; i++) {
148 					var item = items[i];
149 					var moveAction;
150 				
151 					if (item instanceof ZmItem) {
152 						if (!item.isShared()) { // Moving shared items is not undoable
153 							moveAction = new ZmItemMoveAction(item, item.getFolderId(), folderId, op);
154 						}
155 					}
156 					else if (item instanceof ZmOrganizer) {
157 						if (!item.isRemote()) { // Moving remote organizers is not undoable
158 							moveAction = new ZmOrganizerMoveAction(item, item.parent.id, folderId, op);
159 						}
160 					}
161 					else if (item.isConvMsg) {
162 						if (!appCtxt.isRemoteId(item.id)) {
163 							moveAction = new ZmItemMoveAction(item, item.folderId, folderId, op);
164 						}
165 					}
166 					if (moveAction) {
167 						if (multi) {
168 							if (!action) action = new ZmCompositeAction(folderId);
169 							action.addAction(moveAction);
170 						} else {
171 							action = moveAction;
172 						}
173 					}
174 				}
175 				break;
176 		}
177 		if (action) {
178 			this._push(action);
179 		}
180 	}
181 
182 	return action;
183 };
184 
185 /**
186  * Returns whether there are actions that can be undone
187  */
188 ZmActionStack.prototype.canUndo = function() {
189 	return this._pointer >= 0;
190 };
191 
192 /**
193  * Returns whether there are actions that can be redone
194  */
195 ZmActionStack.prototype.canRedo = function() {
196 	return this._pointer < this._stack.length - 1;
197 };
198 
199 
200 /**
201  * Returns whether the next undo action has completed
202  */
203 ZmActionStack.prototype.actionIsComplete = function() {
204 	return this.canUndo() && this._current().getComplete();
205 };
206 
207 /**
208  * Attaches a completion callback to the current action
209  */
210 ZmActionStack.prototype.onComplete = function(callback) {
211 	var action = this._current();
212 	if (action) {
213 		action.onComplete(callback);
214 	}
215 };
216 
217 /**
218  * Undoes the current action (if applicable) and moves the internal pointer
219  */
220 ZmActionStack.prototype.undo = function() {
221 	if (this.canUndo()) {
222 		var action = this._pop();
223 		action.undo();
224 	}
225 };
226 
227 /**
228  * Redoes the current action (if applicable) and moves the internal pointer
229  */
230 ZmActionStack.prototype.redo = function() {
231 	if (this.canRedo()) {
232 		var action = this._stack[++this._pointer];
233 		action.redo();
234 	}
235 };
236 
237 /**
238  * Puts an action into the stack at the current position
239  * If we're not at the top of the stack (ie. undoes have been performed), we kill all later actions (so redoing the undone actions is no longer possible)
240  */
241 ZmActionStack.prototype._push = function(action) {
242 	if (action && action instanceof ZmAction) {
243 		var next = this._pointer + 1;
244 		while (this._maxLength && next>=this._maxLength) {
245 			// Stack size is reached, shift off actions until we're under the limit
246 			this._stack.shift();
247 			next--;
248 		}
249 		this._stack[next] = action;
250 		this._stack.length = next+1; // Kill all actions after pointer
251 		this._pointer = next;
252 	}
253 };
254 
255 /**
256  * Returns the action at the current position and moves the pointer
257  */
258 ZmActionStack.prototype._pop = function() {
259 	return this.canUndo() ? this._stack[this._pointer--] : null;
260 };
261 
262 /**
263  * Returns the action at the current position, does not move the pointer
264  */
265 ZmActionStack.prototype._current = function() {
266 	return this.canUndo() ? this._stack[this._pointer] : null;
267 };
268 
269 /**
270  * Returns true if the given type is valid.
271  */
272 ZmActionStack.prototype._isValidType = function(type) {
273 	return !ZmActionStack.validTypes || AjxUtil.indexOf(ZmActionStack.validTypes, type) !== -1;
274 };
275