4724e73f044cdb9528451c808cca9d8751642d0d
[tine20] / tine20 / Filemanager / js / NodeTreePanel.js
1 /*
2  * Tine 2.0
3  *
4  * @package     Tinebase
5  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
6  * @author      Philipp Schüle <p.schuele@metaways.de>
7  * @copyright   Copyright (c) 2010-2012 Metaways Infosystems GmbH (http://www.metaways.de)
8  */
9
10 Ext.ns('Tine.Filemanager');
11
12 require('./nodeContextMenu');
13
14 /**
15  * @namespace Tine.Filemanager
16  * @class Tine.Filemanager.NodeTreePanel
17  * @extends Tine.widgets.container.TreePanel
18  *
19  * @author Martin Jatho <m.jatho@metaways.de>
20  */
21 Tine.Filemanager.NodeTreePanel = Ext.extend(Tine.widgets.container.TreePanel, {
22
23     filterMode : 'filterToolbar',
24
25     recordClass : Tine.Filemanager.Model.Node,
26
27     allowMultiSelection : false,
28
29     ddGroup: 'fileDDGroup',
30     enableDD: true,
31
32     initComponent: function() {
33         this.on('nodedragover', this.onNodeDragOver, this);
34
35         if (! this.app) {
36             this.app = Tine.Tinebase.appMgr.get(this.recordClass.getMeta('appName'));
37         }
38
39         if (this.readOnly) {
40             this.enableDD = false;
41         }
42
43         this.defaultContainerPath = Tine.Tinebase.container.getMyFileNodePath();
44
45         this.dragConfig = {
46             ddGroup: this.ddGroup,
47             scroll: this.ddScroll,
48             onBeforeDrag: this.onBeforeDrag.createDelegate(this)
49         };
50
51         this.dropConfig = {
52             ddGroup: this.ddGroup,
53             appendOnly: this.ddAppendOnly === true,
54             onNodeOver: this.onNodeOver.createDelegate(this)
55         };
56
57         Tine.Filemanager.NodeTreePanel.superclass.initComponent.call(this);
58
59         this.plugins = this.plugins || [];
60
61         if (!this.readOnly && this.enableDD) {
62             this.plugins.push({
63                 ptype : 'ux.browseplugin',
64                 enableFileDialog: false,
65                 multiple : true,
66                 handler : this.dropIntoTree
67             });
68         }
69
70         postal.subscribe({
71             channel: "recordchange",
72             topic: [this.recordClass.getMeta('appName'), this.recordClass.getMeta('modelName'), '*'].join('.'),
73             callback: this.onRecordChanges.createDelegate(this)
74         });
75     },
76
77     onRecordChanges: function(data, e) {
78         if (data.type == 'folder') {
79             var _ = window.lodash,
80                 me = this,
81                 path = data.path,
82                 parentPath = path.replace(new RegExp(_.escapeRegExp(data.name) + '$'), ''),
83                 node = this.getNodeById(data.id),
84                 pathChange = node && node.attributes && node.attributes.nodeRecord.get('path') != path;
85
86             if (node && e.topic.match(/\.delete/)) {
87                 try {
88                     node.cancelExpand();
89                     node.remove(true);
90                 } catch (e) {}
91                 return;
92             }
93
94             if (node) {
95                 node.setText(Ext.util.Format.htmlEncode(data.name));
96                 // NOTE: qtip dosn't work, but implementing is not worth the effort...
97                 node.qtip = Tine.Tinebase.common.doubleEncode(data.name);
98                 Ext.apply(node.attributes, data);
99                 node.attributes.nodeRecord = new this.recordClass(data);
100
101                 // in case of path change we need to reload the node (children) as well
102                 // as the path of all children changed as well
103                 if (node.hasChildNodes() && pathChange && ! node.loading) {
104                     node.reload();
105                 }
106             }
107
108             // add / remount node
109             try {
110                 me.expandPath(parentPath, '', function (sucess, parentNode) {
111                     var childNode = parentNode.findChild('name', data.name);
112                     if (!childNode) {
113                         parentNode.appendChild(node || me.loader.createNode(data));
114                     } else if (childNode != node) {
115                         // node got duplicated by expand load
116                         try {
117                             node.cancelExpand();
118                             node.remove(true);
119                         } catch (e) {
120                         }
121                     }
122                 });
123             } catch (e) {}
124         }
125     },
126
127     /**
128      * autosort new nodes
129      *
130      * @param tree
131      * @param parent
132      * @param appendedNode
133      * @param idx
134      */
135     onAppendNode: function(tree, parent, appendedNode, idx) {
136         if (parent.getDepth() > 0) {
137             parent.sort(function (n1, n2) {
138                 return n1.text.localeCompare(n2.text);
139             });
140         }
141     },
142
143     /**
144      * An empty function by default, but provided so that you can perform a custom action before the initial
145      * drag event begins and optionally cancel it.
146      * @param {Object} data An object containing arbitrary data to be shared with drop targets
147      * @param {Event} e The event object
148      * @return {Boolean} isValid True if the drag event is valid, else false to cancel
149      */
150     onBeforeDrag : function(data, e) {
151         var _ = window.lodash,
152             requiredGrant = e.ctrlKey || e.altKey ? 'readGrant' : 'editGrant';
153
154         // @TODO: rethink: do I need delte on the record or parent?
155         return !! _.get(data, 'node.attributes.nodeRecord.data.account_grants.' + requiredGrant);
156     },
157
158     onNodeOver : function(n, dd, e, data) {
159         var action = e.ctrlKey || e.altKey ? 'copy' : 'move',
160             cls = Ext.tree.TreeDropZone.prototype.onNodeOver.apply(this.dropZone, arguments);
161
162         return cls != this.dropZone.dropNotAllowed ?
163             'tinebase-dd-drop-ok-' + action :
164             this.dropZone.dropNotAllowed;
165     },
166
167     /**
168      * @param {Object} dragOverEvent
169      *
170      * tree - The TreePanel
171      * target - The node being targeted for the drop
172      * data - The drag data from the drag source
173      * point - The point of the drop - append, above or below
174      * source - The drag source
175      * rawEvent - Raw mouse event
176      * dropNode - Drop node(s) provided by the source.
177      * cancel - Set this to true to signal drop not allowed.
178      */
179     onNodeDragOver: function(dragOverEvent) {
180         var _ = window.lodash,
181             cancel = this.readOnly
182                 || ! dragOverEvent.target.expanded
183                 || dragOverEvent.target == dragOverEvent.source.dragData.node
184                 || ! _.get(dragOverEvent, 'target.attributes.nodeRecord.data.account_grants.addGrant');
185
186         dragOverEvent.cancel = cancel;
187     },
188
189     /**
190      * files/folder got dropped on node
191      *
192      * @param {Object} dropEvent
193      * @private
194      */
195     onBeforeNodeDrop: function(dropEvent) {
196         var nodes, target = dropEvent.target;
197
198         if(dropEvent.data.selections) {
199             nodes = dropEvent.data.grid.selModel.selections.items;
200         }
201
202         if(!nodes && dropEvent.data.node) {
203             nodes = [dropEvent.data.node];
204         }
205
206         Tine.Filemanager.fileRecordBackend.copyNodes(nodes, target, !(dropEvent.rawEvent.ctrlKey  || dropEvent.rawEvent.altKey));
207
208         dropEvent.dropStatus = true;
209         return true;
210     },
211
212     /**
213      * load everything from server
214      * @returns {Object} root node definition
215      */
216     getRoot: function() {
217         return {
218             path: '/',
219             cls: 'tinebase-tree-hide-collapsetool'
220         };
221     },
222
223     /**
224      * Tine.widgets.tree.FilterPlugin
225      * returns a filter plugin to be used in a grid
226      */
227     getFilterPlugin: function() {
228         if (!this.filterPlugin) {
229             this.filterPlugin = new Tine.Filemanager.PathFilterPlugin({
230                 treePanel: this,
231                 field: 'path',
232                 nodeAttributeField: 'path'
233             });
234         }
235
236         return this.filterPlugin;
237     },
238
239     /**
240      * returns params for async request
241      *
242      * @param {Ext.tree.TreeNode} node
243      * @return {Object}
244      */
245     onBeforeLoad: function(node) {
246         var path = node.attributes.path;
247         var type = Tine.Tinebase.container.path2type(path);
248         var owner = Tine.Tinebase.container.pathIsPersonalNode(path);
249         var loginName = Tine.Tinebase.registry.get('currentAccount').accountLoginName;
250
251         if (type === 'personal' && owner != loginName) {
252             type = 'otherUsers';
253         }
254
255         var newPath = path;
256
257         if (type === 'personal' && owner) {
258             var pathParts = path.toString().split('/');
259             newPath = '/' + pathParts[1] + '/' + loginName;
260             if(pathParts[3]) {
261                 newPath += '/' + pathParts[3];
262             }
263         }
264
265         var params = {
266             method: this.recordClass.getMeta('appName') + '.searchNodes',
267             application: this.app.appName,
268             owner: owner,
269             filter: [
270                      {field: 'path', operator:'equals', value: newPath},
271                      {field: 'type', operator:'equals', value: 'folder'}
272                      ],
273             paging: {dir: 'ASC', sort: 'name'}
274         };
275
276         return params;
277     },
278
279     onBeforeCreateNode: function(attr) {
280         Tine.Filemanager.NodeTreePanel.superclass.onBeforeCreateNode.apply(this, arguments);
281
282         attr.leaf = false;
283
284         if(attr.name && typeof attr.name == 'object') {
285             Ext.apply(attr, {
286                 text: Ext.util.Format.htmlEncode(attr.name.name),
287                 qtip: Tine.Tinebase.common.doubleEncode(attr.name.name)
288             });
289         }
290
291         // copy 'real' data to a node record NOTE: not a full record as we have no record reader here
292         var nodeData = Ext.copyTo({}, attr, this.recordClass.getFieldNames());
293         attr.nodeRecord = new this.recordClass(nodeData);
294     },
295
296     /**
297      * initiates tree context menus
298      *
299      * @private
300      */
301     initContextMenu: function() {
302         this.ctxMenu = Tine.Filemanager.nodeContextMenu.getMenu({
303             actionMgr: Tine.Filemanager.nodeActionsMgr,
304             nodeName: this.recordClass.getContainerName(),
305             actions: ['reload', 'createFolder', 'delete', 'rename', 'move', 'edit', 'publish', 'systemLink'],
306             scope: this,
307             backend: 'Filemanager',
308             backendModel: 'Node'
309         });
310
311         this.actionUpdater = new Tine.widgets.ActionUpdater({
312             actions: this.ctxMenu.items
313         });
314     },
315
316     /**
317      * show context menu
318      *
319      * @param {Ext.tree.TreeNode} node
320      * @param {Ext.EventObject} event
321      */
322     onContextMenu: function(node, event) {
323         event.stopEvent();
324         Tine.log.debug(node);
325
326         // legacy for reload action
327         this.ctxNode = node;
328
329         //@TODO implement selection vs ctxNode if multiselect is allowed
330         var record = new this.recordClass(node.attributes.nodeRecord.data);
331         this.actionUpdater.updateActions([record]);
332
333         this.ctxMenu.showAt(event.getXY());
334     },
335
336     /**
337      * called when tree selection changes
338      *
339      * @param {} sm     SelectionModel
340      * @param {Ext.tree.TreeNode} node
341      */
342     onSelectionChange: function(sm, node) {
343         // this.updateActions(sm, node);
344         var grid = this.app.getMainScreen().getCenterPanel(),
345             gridSelectionModel = grid.selectionModel,
346             actionUpdater = grid.actionUpdater,
347             record = node ? new this.recordClass(window.lodash.get(node, 'attributes.nodeRecord.data')) : null,
348             selection = record ? [record] : [];
349
350         grid.currentFolderNode = node;
351
352         if (gridSelectionModel) {
353             gridSelectionModel.clearSelections();
354         }
355
356         if (actionUpdater) {
357             actionUpdater.updateActions(selection);
358         }
359
360         Tine.Filemanager.NodeTreePanel.superclass.onSelectionChange.call(this, sm, node);
361     },
362
363     /**
364      * convert filesystem path to treePath
365      *
366      * NOTE: only the first depth gets converted!
367      *       fs pathes of not yet loaded tree nodes can't be converted!
368      *
369      * @param {String} containerPath
370      * @return {String} tree path
371      */
372     getTreePath: function(containerPath) {
373         var _ = window.lodash,
374             treePath = '/' + this.getRootNode().id + containerPath
375             .replace(new RegExp('^' + _.escapeRegExp(Tine.Tinebase.container.getMyFileNodePath())), '/myUser')
376             .replace(/^\/personal/, '/otherUsers')
377             .replace(/\/$/, '');
378
379         return treePath;
380     },
381
382     /**
383      * Expands a specified path in this TreePanel. A path can be retrieved from a node with {@link Ext.data.Node#getPath}
384      *
385      * NOTE: path does not consist of id's starting from the second depth
386      *
387      * @param {String} path
388      * @param {String} attr (optional) The attribute used in the path (see {@link Ext.data.Node#getPath} for more info)
389      * @param {Function} callback (optional) The callback to call when the expand is complete. The callback will be called with
390      * (bSuccess, oLastNode) where bSuccess is if the expand was successful and oLastNode is the last node that was expanded.
391      */
392     expandPath : function(path, attr, callback){
393         if (! path.match(/^\/xnode-/)) {
394             path = this.getTreePath(path);
395         }
396
397         var keys = path.split(this.pathSeparator);
398         var curNode = this.root;
399         var curPath = curNode.attributes.path;
400         var index = 1;
401         var f = function(){
402             if(++index == keys.length){
403                 if(callback){
404                     callback(true, curNode);
405                 }
406                 return;
407             }
408
409             if (index > 2) {
410                 var c = curNode.findChild('path', curPath + '/' + keys[index]);
411             } else {
412                 var c = curNode.findChild('id', keys[index]);
413             }
414             if(!c){
415                 if(callback){
416                     callback(false, curNode);
417                 }
418                 return;
419             }
420             curNode = c;
421             curPath = c.attributes.path;
422             c.expand(false, false, f);
423         };
424         curNode.expand(false, false, f);
425     },
426
427     /**
428      * Selects the node in this tree at the specified path. A path can be retrieved from a node with {@link Ext.data.Node#getPath}
429      * @param {String} path
430      * @param {String} attr (optional) The attribute used in the path (see {@link Ext.data.Node#getPath} for more info)
431      * @param {Function} callback (optional) The callback to call when the selection is complete. The callback will be called with
432      * (bSuccess, oSelNode) where bSuccess is if the selection was successful and oSelNode is the selected node.
433      */
434     selectPath : function(path, attr, callback) {
435         var node = this.expandPath(path, attr, function(bSuccess, oLastNode){
436             if (oLastNode) {
437                 oLastNode.select();
438                 if (Ext.isFunction(callback)) {
439                     callback.call(true, oLastNode);
440                 }
441             }
442         }.createDelegate(this));
443     },
444
445     /**
446      * clone a tree node / create a node from grid node
447      *
448      * @param node
449      * @returns {Ext.tree.AsyncTreeNode}
450      */
451     cloneTreeNode: function(node, target) {
452         var targetPath = target.attributes.path,
453             newPath = '',
454             copy;
455
456         if(node.attributes) {
457             var nodeName = node.attributes.name;
458             if(typeof nodeName == 'object') {
459                 nodeName = nodeName.name;
460             }
461             newPath = targetPath + '/' + nodeName;
462
463             copy = new Ext.tree.AsyncTreeNode({text: node.text, path: newPath, name: node.attributes.name
464                 , nodeRecord: node.attributes.nodeRecord, account_grants: node.attributes.account_grants});
465         }
466         else {
467             var nodeName = node.data.name;
468             if(typeof nodeName == 'object') {
469                 nodeName = nodeName.name;
470             }
471
472             var nodeData = Ext.copyTo({}, node.data, this.recordClass.getFieldNames());
473             var newNodeRecord = new this.recordClass(nodeData);
474
475             newPath = targetPath + '/' + nodeName;
476             copy = new Ext.tree.AsyncTreeNode({text: nodeName, path: newPath, name: node.data.name
477                 , nodeRecord: newNodeRecord, account_grants: node.data.account_grants});
478         }
479
480         copy.attributes.nodeRecord.beginEdit();
481         copy.attributes.nodeRecord.set('path', newPath);
482         copy.attributes.nodeRecord.endEdit();
483
484         copy.parentNode = target;
485         return copy;
486     },
487
488     /**
489      * handels tree drop of object from outside the browser
490      *
491      * @param fileSelector
492      * @param targetNodeId
493      */
494     dropIntoTree: function(fileSelector, event) {
495
496         var treePanel = fileSelector.component,
497             app = treePanel.app,
498             grid = app.getMainScreen().getCenterPanel(),
499             targetNode,
500             targetNodePath;
501
502
503         var targetNodeId;
504         var treeNodeAttribute = event.getTarget('div').attributes['ext:tree-node-id'];
505         if(treeNodeAttribute) {
506             targetNodeId = treeNodeAttribute.nodeValue;
507             targetNode = treePanel.getNodeById(targetNodeId);
508             targetNodePath = targetNode.attributes.path;
509
510         }
511
512         if(!targetNode.attributes.nodeRecord.isDropFilesAllowed()) {
513             Ext.MessageBox.alert(
514                     i18n._('Upload Failed'),
515                     app.i18n._('Putting files in this folder is not allowed!')
516                 ).setIcon(Ext.MessageBox.ERROR);
517
518             return;
519         }
520
521         var files = fileSelector.getFileList(),
522             filePathsArray = [],
523             uploadKeyArray = [],
524             addToGridStore = false;
525
526         Ext.each(files, function (file) {
527             if ("" === file.type) {
528                 return true;
529             }
530
531             var fileName = file.name || file.fileName,
532                 filePath = targetNodePath + '/' + fileName;
533
534             var upload = new Ext.ux.file.Upload({
535                 fmDirector: treePanel,
536                 file: file,
537                 fileSelector: fileSelector,
538                 id: filePath
539             });
540
541             var uploadKey = Tine.Tinebase.uploadManager.queueUpload(upload);
542
543             filePathsArray.push(filePath);
544             uploadKeyArray.push(uploadKey);
545
546             addToGridStore = grid.currentFolderNode.id === targetNodeId;
547
548         }, this);
549
550         if (0 === uploadKeyArray.length) {
551             return;
552         }
553
554         var params = {
555                 filenames: filePathsArray,
556                 type: "file",
557                 tempFileIds: [],
558                 forceOverwrite: false
559         };
560         Tine.Filemanager.fileRecordBackend.createNodes(params, uploadKeyArray, addToGridStore);
561     }
562 });