1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2006, 2007, 2008, 2009, 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) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @overview
 26  * This file contains the address book tree controller class.
 27  * 
 28  */
 29 
 30 /**
 31  * Creates an address book tree controller.
 32  * @class
 33  * This class is a controller for the tree view used by the address book 
 34  * application. This class uses the support provided by {@link ZmOperation}. 
 35  *
 36  * @author Parag Shah
 37  * 
 38  * @extends		ZmFolderTreeController
 39  */
 40 ZmAddrBookTreeController = function() {
 41 
 42 	ZmFolderTreeController.call(this, ZmOrganizer.ADDRBOOK);
 43 
 44 	this._listeners[ZmOperation.NEW_ADDRBOOK]	= this._newListener.bind(this);
 45 	this._listeners[ZmOperation.SHARE_ADDRBOOK]	= this._shareAddrBookListener.bind(this);
 46 
 47 	this._app = appCtxt.getApp(ZmApp.CONTACTS);
 48 };
 49 
 50 ZmAddrBookTreeController.prototype = new ZmFolderTreeController;
 51 ZmAddrBookTreeController.prototype.constructor = ZmAddrBookTreeController;
 52 
 53 ZmAddrBookTreeController.prototype.isZmAddrBookTreeController = true;
 54 ZmAddrBookTreeController.prototype.toString = function() { return "ZmAddrBookTreeController"; };
 55 
 56 // Public methods
 57 
 58 /**
 59  * Shows the controller and returns the resulting tree view.
 60  * 
 61  * @param	{Hash}	params		 a hash of parameters
 62  * @return	{ZmTreeView}	the tree view
 63  */
 64 ZmAddrBookTreeController.prototype.show =
 65 function(params) {
 66 	params.include = {};
 67 	params.include[ZmFolder.ID_TRASH] = true;
 68     params.showUnread = false;
 69     var treeView = ZmFolderTreeController.prototype.show.call(this, params);
 70 
 71 	// contacts app has its own Trash folder so listen for change events
 72 	var trash = this.getDataTree().getById(ZmFolder.ID_TRASH);
 73 	if (trash) {
 74 		trash.addChangeListener(new AjxListener(this, this._trashChangeListener, treeView));
 75 	}
 76 
 77 	return treeView;
 78 };
 79 
 80 /**
 81  * @private
 82  */
 83 ZmAddrBookTreeController.prototype._trashChangeListener =
 84 function(treeView, ev) {
 85 	var organizers = ev.getDetail("organizers");
 86 	if (!organizers && ev.source) {
 87 		organizers = [ev.source];
 88 	}
 89 
 90 	// handle one organizer at a time
 91 	for (var i = 0; i < organizers.length; i++) {
 92 		var organizer = organizers[i];
 93 
 94 		if (organizer.id == ZmFolder.ID_TRASH &&
 95 			ev.event == ZmEvent.E_MODIFY)
 96 		{
 97 			var fields = ev.getDetail("fields");
 98 			if (fields && (fields[ZmOrganizer.F_TOTAL] || fields[ZmOrganizer.F_SIZE])) {
 99 				var ti = treeView.getTreeItemById(organizer.id);
100 				if (ti) ti.setToolTipContent(organizer.getToolTip(true));
101 			}
102 		}
103 	}
104 };
105 
106 /**
107  * Enables/disables operations based on the given organizer ID.
108  * 
109  * @private
110  */
111 ZmAddrBookTreeController.prototype.resetOperations =
112 function(parent, type, id) {
113 	var deleteText = ZmMsg.del;
114 	var addrBook = appCtxt.getById(id);
115 	var nId = addrBook ? addrBook.nId : ZmOrganizer.normalizeId(id);
116 	var isTrash = (nId == ZmFolder.ID_TRASH);
117 
118 	var isDLs = (nId == ZmFolder.ID_DLS);
119 
120 	this.setVisibleIfExists(parent, ZmOperation.EMPTY_FOLDER, nId == ZmFolder.ID_TRASH);
121 
122 	if (isTrash) {
123 		parent.enableAll(false);
124 		parent.enable(ZmOperation.DELETE_WITHOUT_SHORTCUT, false);
125 		var hasContent = ((addrBook.numTotal > 0) || (addrBook.children && (addrBook.children.size() > 0)));
126 		parent.enable(ZmOperation.EMPTY_FOLDER,hasContent);
127 		parent.getOp(ZmOperation.EMPTY_FOLDER).setText(ZmMsg.emptyTrash);
128 	}
129 	else if (isDLs) {
130 		parent.enableAll(false);
131 	}
132 	else {
133 		parent.enableAll(true);        
134 		if (addrBook) {
135 
136 			parent.enable([ZmOperation.NEW_ADDRBOOK], !addrBook.isReadOnly());
137 
138 			if (addrBook.isSystem() || appCtxt.isExternalAccount()) {
139 				parent.enable([ZmOperation.DELETE_WITHOUT_SHORTCUT, ZmOperation.RENAME_FOLDER], false);
140 			} else if (addrBook.link) {
141 				parent.enable([ZmOperation.SHARE_ADDRBOOK], !addrBook.link || addrBook.isAdmin());
142 			}
143 			if (appCtxt.isOffline) {
144 				var acct = addrBook.getAccount();
145 				parent.enable([ZmOperation.SHARE_ADDRBOOK], !acct.isMain && acct.isZimbraAccount);
146 			}
147 		}
148 	}
149 
150 	if (addrBook) {
151 		parent.enable(ZmOperation.EXPAND_ALL, (addrBook.size() > 0));
152 	}
153 
154 	var op = parent.getOp(ZmOperation.DELETE_WITHOUT_SHORTCUT);
155 	if (op) {
156 		op.setText(deleteText);
157 	}
158 	this._enableRecoverDeleted(parent, isTrash);
159 
160 	// we always enable sharing in case we're in multi-mbox mode
161 	this._resetButtonPerSetting(parent, ZmOperation.SHARE_ADDRBOOK, appCtxt.get(ZmSetting.SHARING_ENABLED));
162 };
163 
164 /**
165  * override to take care of not allowing dropping DLs do folders
166  * @param ev
167  * @private
168  */
169 ZmAddrBookTreeController.prototype._dropListener =
170 function(ev) {
171 	var items = AjxUtil.toArray(ev.srcData.data);
172 	for (var i = 0; i < items.length; i++) {
173 		var item = items[i];
174 		if (!item.isZmContact) {
175 			continue;
176 		}
177 		if (item.isDistributionList()) {
178 			ev.doIt = false;
179 			return;
180 		}
181 	}
182 	// perform default action
183 	ZmFolderTreeController.prototype._dropListener.apply(this, arguments);
184 };
185 
186 
187 // Protected methods
188 
189 /**
190  * @private
191  */
192 ZmAddrBookTreeController.prototype._getAllowedSubTypes =
193 function() {
194 	var types = {};
195 	types[ZmOrganizer.SEARCH] = true;
196 	types[this.type] = true;
197 	return types;
198 };
199 
200 ZmAddrBookTreeController.prototype._getSearchTypes =
201 function(ev) {
202 	return [ZmItem.CONTACT];
203 };
204 
205 /**
206  * Returns a list of desired header action menu operations.
207  * 
208  * @private
209  */
210 ZmAddrBookTreeController.prototype._getHeaderActionMenuOps =
211 function() {
212 	var ops = null;
213 	if (appCtxt.get(ZmSetting.NEW_ADDR_BOOK_ENABLED)) {
214 		ops = [ZmOperation.NEW_ADDRBOOK, ZmOperation.FIND_SHARES];
215 	}
216 	return ops;
217 };
218 
219 /**
220  * Returns a list of desired action menu operations.
221  * 
222  * @private
223  */
224 ZmAddrBookTreeController.prototype._getActionMenuOps = function() {
225 
226 	var ops = [];
227 	if (appCtxt.get(ZmSetting.NEW_ADDR_BOOK_ENABLED)) {
228 		ops.push(ZmOperation.NEW_ADDRBOOK);
229 	}
230 	ops.push(
231 		ZmOperation.EMPTY_FOLDER,
232 		ZmOperation.RECOVER_DELETED_ITEMS,
233 		ZmOperation.SHARE_ADDRBOOK,
234 		ZmOperation.DELETE_WITHOUT_SHORTCUT,
235 		ZmOperation.RENAME_FOLDER,
236 		ZmOperation.EDIT_PROPS,
237 		ZmOperation.EXPAND_ALL
238 	);
239 
240 	return ops;
241 };
242 
243 /**
244  * Returns a title for moving a folder.
245  * 
246  * @private
247  */
248 ZmAddrBookTreeController.prototype._getMoveDialogTitle =
249 function() {
250 	return AjxMessageFormat.format(ZmMsg.moveAddrBook, this._pendingActionData.name);
251 };
252 
253 /**
254  * Returns the dialog for organizer creation.
255  * 
256  * @private
257  */
258 ZmAddrBookTreeController.prototype._getNewDialog =
259 function() {
260 	return appCtxt.getNewAddrBookDialog();
261 };
262 
263 
264 // Listeners
265 
266 /**
267  * @private
268  */
269 ZmAddrBookTreeController.prototype._shareAddrBookListener = 
270 function(ev) {
271 	this._pendingActionData = this._getActionedOrganizer(ev);
272 	appCtxt.getSharePropsDialog().popup(ZmSharePropsDialog.NEW, this._pendingActionData);
273 };
274 
275 ZmAddrBookTreeController.dlFolderClicked =
276 function() {
277 	var request = {
278 		_jsns: "urn:zimbraAccount",
279 		"ownerOf": 1,
280 		attrs: "zimbraDistributionListUnsubscriptionPolicy,zimbraDistributionListSubscriptionPolicy,zimbraHideInGal"
281 	};
282 
283 	var jsonObj = {GetAccountDistributionListsRequest: request};
284 	var respCallback = ZmAddrBookTreeController._handleAccountDistributionListResponse;
285 	appCtxt.getAppController().sendRequest({jsonObj: jsonObj, asyncMode: true, callback: respCallback});
286 };
287 
288 /**
289  * Called when a left click occurs (by the tree view listener). The folder that
290  * was clicked may be a search, since those can appear in the folder tree. The
291  * appropriate search will be performed.
292  *
293  * @param {ZmOrganizer}	folder		the folder or search that was clicked
294  * 
295  * @private
296  */
297 ZmAddrBookTreeController.prototype._itemClicked =
298 function(folder) {
299 	if (folder.id == ZmFolder.ID_DLS) {
300 		ZmAddrBookTreeController.dlFolderClicked();
301 	}
302 	else if (folder.type == ZmOrganizer.SEARCH) {
303 		// if the clicked item is a search (within the folder tree), hand
304 		// it off to the search tree controller
305 		var stc = this._opc.getTreeController(ZmOrganizer.SEARCH);
306 		stc._itemClicked(folder);
307 	}
308 	else {
309 		var capp = appCtxt.getApp(ZmApp.CONTACTS);
310 		capp.currentSearch = null;
311 		var query = capp.currentQuery = folder.createQuery();
312 		var sc = appCtxt.getSearchController();
313 		sc.setDefaultSearchType(ZmItem.CONTACT);
314 		var acct = folder.getAccount();
315 		var params = {
316 			query: query,
317 			searchFor: ZmItem.CONTACT,
318 			fetch: true,
319 			sortBy: ZmSearch.NAME_ASC,
320 			callback: new AjxCallback(this, this._handleSearchResponse, [folder]),
321 			accountName: (acct && acct.name)
322 		};
323 		sc.search(params);
324 
325 		if (folder.id != ZmFolder.ID_TRASH) {
326 			var clc = AjxDispatcher.run("GetContactListController");
327 			var view = clc.getCurrentView();
328 			if (view) {
329 				view.getAlphabetBar().reset();
330 			}
331 		}
332 	}
333 };
334 
335 /**
336  * @private
337  */
338 ZmAddrBookTreeController.prototype._handleSearchResponse =
339 function(folder, result) {
340 	// bug fix #19307 - Trash is special when in Contacts app since it
341 	// is a FOLDER type in ADDRBOOK tree. So reset selection if clicked
342 	if (folder.nId == ZmFolder.ID_TRASH) {
343 		this._treeView[this._app.getOverviewId()].setSelected(folder, true);
344 	}
345 };
346 
347 /**
348  * @private
349  */
350 ZmAddrBookTreeController._handleAccountDistributionListResponse =
351 function(result) {
352 
353 	var contactList = new ZmContactList(null, true, ZmItem.CONTACT);
354 	var dls = result._data.GetAccountDistributionListsResponse.dl;
355 	if (dls) {
356 		for (var i = 0; i < dls.length; i++) {
357 			var dl = dls[i];
358 			var attrs = dl._attrs;
359 			attrs.email = dl.name; // in this case the email comes in the "name" property.
360 			attrs.type = "group";
361 			
362 			attrs[ZmContact.F_dlDisplayName] = dl.d || dl.name;
363 			contactList.addFromDom(dl);
364 		}
365 	}
366 	var clc = AjxDispatcher.run("GetContactListController");
367 	clc.show(contactList, true, ZmFolder.ID_DLS);
368 };
369