f57916c39aac4cb7864c7b583b4b68c47e61fc86
[tine20] / tine20 / Tinebase / js / widgets / container / TreePanel.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2007-2011 Metaways Infosystems GmbH (http://www.metaways.de)
7  */
8 Ext.ns('Tine.widgets', 'Tine.widgets.container');
9
10 /**
11  * @namespace   Tine.widgets.container
12  * @class       Tine.widgets.container.TreePanel
13  * @extends     Ext.tree.TreePanel
14  * @author      Cornelius Weiss <c.weiss@metaways.de>
15  * @param       {Object} config Configuration options
16  * @description
17  * <p>Utility class for generating container trees as used in the apps tree panel</p>
18  * <p>This widget handles all container related actions like add/rename/delte and manager permissions<p>
19  *<p>Example usage:</p>
20 <pre><code>
21 var taskPanel =  new Tine.containerTreePanel({
22     app: Tine.Tinebase.appMgr.get('Tasks'),
23     recordClass: Tine.Tasks.Model.Task
24 });
25 </code></pre>
26  */
27 Tine.widgets.container.TreePanel = function(config) {
28     Ext.apply(this, config);
29
30     this.addEvents(
31         /**
32          * @event containeradded
33          * Fires when a container was added
34          * @param {container} the new container
35          */
36         'containeradd',
37         /**
38          * @event containerdelete
39          * Fires when a container got deleted
40          * @param {container} the deleted container
41          */
42         'containerdelete',
43         /**
44          * @event containerrename
45          * Fires when a container got renamed
46          * @param {container} the renamed container
47          */
48         'containerrename',
49         /**
50          * @event containerpermissionchange
51          * Fires when a container got renamed
52          * @param {container} the container whose permissions where changed
53          */
54         'containerpermissionchange',
55         /**
56          * @event containercolorset
57          * Fires when a container color got changed
58          * @param {container} the container whose color where changed
59          */
60         'containercolorset'
61     );
62
63     Tine.widgets.container.TreePanel.superclass.constructor.call(this);
64 };
65
66 Ext.extend(Tine.widgets.container.TreePanel, Ext.tree.TreePanel, {
67     /**
68      * @cfg {Tine.Tinebase.Application} app
69      */
70     app: null,
71     /**
72      * @cfg {Boolean} allowMultiSelection (defaults to true)
73      */
74     allowMultiSelection: true,
75     /**
76      * @cfg {String} defaultContainerPath
77      */
78     defaultContainerPath: null,
79     /**
80      * @cfg {array} extraItems additional items to display under all
81      */
82     extraItems: null,
83     /**
84      * @cfg {String} filterMode one of:
85      *   - gridFilter: hooks into the grids.store
86      *   - filterToolbar: hooks into the filterToolbar (container filterModel required)
87      */
88     filterMode: 'gridFilter',
89     /**
90      * @cfg {Tine.data.Record} recordClass
91      */
92     recordClass: null,
93     /**
94      * @cfg {Array} requiredGrants
95      * grants which are required to select leaf node(s)
96      */
97     requiredGrants: null,
98
99     /**
100      * @cfg {Boolean} useContainerColor
101      * use container colors
102      */
103     useContainerColor: false,
104     /**
105      * @cfg {Boolean} useProperties
106      * use container properties
107      */
108     useProperties: true,
109
110     /**
111      * @property {Object}
112      * modelConfiguration of recordClass (if available)
113      */
114     modelConfiguration: null,
115
116     /**
117      * @cfg {String}
118      * canonical name
119      */
120     canonicalName: 'ContainerTree',
121
122     /**
123      * Referenced grid panel
124      */
125     gridPanel: null,
126
127     useArrows: true,
128     border: false,
129     autoScroll: true,
130     enableDrop: true,
131     ddGroup: 'containerDDGroup',
132     hasPersonalContainer: true,
133     hasContextMenu: true,
134
135     /**
136      * @fixme not needed => all events hand their events over!!!
137      *
138      * @property ctxNode holds treenode which got a contextmenu
139      * @type Ext.tree.TreeNode
140      */
141     ctxNode: null,
142
143     /**
144      * No user interactions, menus etc. allowed except for browsing
145      */
146     readOnly: false,
147
148     /**
149      * init this treePanel
150      */
151     initComponent: function() {
152         if (! this.appName && this.recordClass) {
153             this.appName = this.recordClass.getMeta('appName');
154         }
155         if (! this.app) {
156             this.app = Tine.Tinebase.appMgr.get(this.appName);
157         }
158
159         if (this.allowMultiSelection) {
160             this.selModel = new Ext.tree.MultiSelectionModel({});
161         }
162
163         if (this.recordClass) {
164             this.modelConfiguration = this.recordClass.getModelConfiguration();
165         }
166
167         if (this.modelConfiguration) {
168             this.hasPersonalContainer = this.modelConfiguration.hasPersonalContainer !== false;
169         }
170
171         var containerName = this.recordClass ? this.recordClass.getContainerName() : 'container';
172         var containersName = this.recordClass ? this.recordClass.getContainersName() : 'containers';
173
174         //ngettext('container', 'containers', n);
175         this.containerName = this.containerName || this.app.i18n.n_hidden(containerName, containersName, 1);
176         this.containersName = this.containersName || this.app.i18n._hidden(containersName);
177
178         this.loader = this.loader || new Tine.widgets.tree.Loader({
179             getParams: this.onBeforeLoad.createDelegate(this),
180             inspectCreateNode: this.onBeforeCreateNode.createDelegate(this)
181         });
182
183         this.loader.on('virtualNodesSelected', this.onVirtualNodesSelected.createDelegate(this));
184
185         var extraItems = this.getExtraItems();
186         this.root = this.getRoot(extraItems);
187         if (!this.hasPersonalContainer && ! extraItems.length) {
188             this.rootVisible = false;
189         }
190
191         if (!this.readOnly && !this.dropConfig) {
192             // init drop zone
193             this.dropConfig = {
194                 ddGroup: this.ddGroup || 'TreeDD',
195                 appendOnly: this.ddAppendOnly === true,
196                 /**
197                  * @todo check acl!
198                  */
199                 onNodeOver: function (n, dd, e, data) {
200                     var node = n.node;
201
202                     // auto node expand check
203                     if (node.hasChildNodes() && !node.isExpanded()) {
204                         this.queueExpand(node);
205                     }
206                     return node.attributes.allowDrop ? 'tinebase-tree-drop-move' : false;
207                 },
208                 isValidDropPoint: function (n, dd, e, data) {
209                     return n.node.attributes.allowDrop;
210                 },
211                 completeDrop: Ext.emptyFn
212             };
213         }
214
215         if (this.hasContextMenu) {
216             this.initContextMenu();
217         }
218
219         this.getSelectionModel().on('beforeselect', this.onBeforeSelect, this);
220         this.getSelectionModel().on('selectionchange', this.onSelectionChange, this);
221
222         this.on('click', this.onClick, this);
223         if (this.hasContextMenu) {
224             this.on('contextmenu', this.onContextMenu, this);
225         }
226
227         if (!this.readOnly) {
228             this.on('beforenodedrop', this.onBeforeNodeDrop, this);
229             this.on('append', this.onAppendNode, this);
230             this.on('beforecollapsenode', this.onBeforeCollapse, this);
231         }
232
233         Tine.widgets.container.TreePanel.superclass.initComponent.call(this);
234     },
235
236     /**
237      * @param nodes
238      */
239     onVirtualNodesSelected: function (nodes) {
240         this.suspendEvents();
241
242         if (0 === nodes.length) {
243             return;
244         }
245
246
247         var sm = this.getSelectionModel();
248
249         sm.suspendEvents();
250         sm.clearSelections(true);
251
252         for (var i = 0; i < nodes.length; i++) {
253             var node = nodes[i];
254
255             if (sm.isSelected(node)) {
256                 sm.lastSelNode = node;
257                 continue;
258             }
259
260             sm.selNodes.push(node);
261             sm.selMap[node.id] = node;
262             sm.lastSelNode = node;
263             node.ui.onSelectedChange(true);
264         }
265
266         this.onFilterChange();
267         this.resumeEvents();
268     },
269
270     /**
271      * returns canonical path part
272      * @returns {string}
273      */
274     getCanonicalPathSegment: function () {
275         if (this.recordClass) {
276             return [
277                 this.recordClass.getMeta('modelName'),
278                 this.canonicalName,
279             ].join(Tine.Tinebase.CanonicalPath.separator);
280         }
281     },
282
283     getRoot: function(extraItems)
284     {
285         return {
286             path: '/',
287             cls: 'tinebase-tree-hide-collapsetool',
288             expanded: true,
289             children: [{
290                 path: Tine.Tinebase.container.getMyNodePath(),
291                 id: 'personal',
292                 hidden: !this.hasPersonalContainer
293             }, {
294                 path: '/shared',
295                 id: 'shared'
296             }, {
297                 path: '/personal',
298                 id: 'otherUsers',
299                 hidden: !this.hasPersonalContainer
300             }].concat(extraItems)
301         };
302     },
303
304     /**
305      * template fn for subclasses to set default path
306      *
307      * @return {String}
308      */
309     getDefaultContainerPath: function() {
310         return this.defaultContainerPath || '/';
311     },
312
313     /**
314      * template fn for subclasses to append extra items
315      *
316      * @return {Array}
317      */
318     getExtraItems: function() {
319         return this.extraItems || [];
320     },
321
322     /**
323      * returns a filter plugin to be used in a grid
324      */
325     getFilterPlugin: function() {
326         if (!this.filterPlugin) {
327             this.filterPlugin = new Tine.widgets.tree.FilterPlugin({
328                 treePanel: this
329             });
330         }
331
332         return this.filterPlugin;
333     },
334
335     /**
336      * returns object of selected container/filter or null/default
337      *
338      * @param {Array} [requiredGrants]
339      * @param {Tine.Tinebase.Model.Container} [defaultContainer]
340      * @param {Boolean} onlySingle use default if more than one container in selection
341      * @return {Tine.Tinebase.Model.Container}
342      */
343     getSelectedContainer: function(requiredGrants, defaultContainer, onlySingle) {
344         var container = defaultContainer,
345             sm = this.getSelectionModel(),
346             selection = typeof sm.getSelectedNodes == 'function' ? sm.getSelectedNodes() : [sm.getSelectedNode()];
347
348         if (Ext.isArray(selection) && selection.length > 0 && (! onlySingle || selection.length === 1 || ! container)) {
349             container = this.getContainerFromSelection(selection, requiredGrants) || container;
350         }
351         // postpone this as we don't get the whole container record here
352 //        else if (this.filterMode == 'filterToolbar' && this.filterPlugin) {
353 //            container = this.getContainerFromFilter() || container;
354 //        }
355
356         return container;
357     },
358
359     /**
360      * get container from selection
361      *
362      * @param {Array} selection
363      * @param {Array} requiredGrants
364      * @return {Tine.Tinebase.Model.Container}
365      */
366     getContainerFromSelection: function(selection, requiredGrants) {
367         var result = null;
368
369         Ext.each(selection, function(node) {
370             if (node && Tine.Tinebase.container.pathIsContainer(node.attributes.container.path)) {
371                 if (! requiredGrants || this.hasGrant(node, requiredGrants)) {
372                     result = node.attributes.container;
373                     // take the first one
374                     return false;
375                 }
376             }
377         }, this);
378
379         return result;
380     },
381
382     /**
383      * get container from filter toolbar
384      *
385      * @param {Array} requiredGrants
386      * @return {Tine.Tinebase.Model.Container}
387      *
388      * TODO make this work -> atm we don't get the account grants here (why?)
389      */
390     getContainerFromFilter: function(requiredGrants) {
391         var result = null;
392
393         // check if single container is selected in filter toolbar 
394         var ftb = this.filterPlugin.getGridPanel().filterToolbar,
395             filterValue = null;
396
397         ftb.filterStore.each(function(filter) {
398             if (filter.get('field') == this.recordClass.getMeta('containerProperty')) {
399                 filterValue = filter.get('value');
400                 if (filter.get('operator') == 'equals') {
401                     result = filterValue;
402                 } else if (filter.get('operator') == 'in' && filterValue.length == 1){
403                     result = filterValue[0];
404                 }
405                 // take the first one
406                 return false;
407             }
408         }, this);
409
410         return result;
411     },
412
413     /**
414      * convert containerPath to treePath
415      *
416      * @param {String}  containerPath
417      * @return {String} treePath
418      */
419     getTreePath: function(containerPath) {
420         var treePath = '/' + this.getRootNode().id + (containerPath !== '/' ? containerPath : '');
421
422         // replace personal with otherUsers if personal && ! personal/myaccountid
423         var matches = containerPath.match(/^\/personal\/{0,1}([0-9a-z_\-]*)\/{0,1}/i);
424         if (matches) {
425             if (matches[1] != Tine.Tinebase.registry.get('currentAccount').accountId) {
426                 treePath = treePath.replace('personal', 'otherUsers');
427             } else {
428                 treePath = treePath.replace('personal/'  + Tine.Tinebase.registry.get('currentAccount').accountId, 'personal');
429             }
430         }
431
432         return treePath;
433     },
434
435     /**
436      * checkes if user has requested grant for given container represented by a tree node
437      *
438      * @param {Ext.tree.TreeNode} node
439      * @param {Array} grant
440      * @return {}
441      */
442     hasGrant: function(node, grants) {
443         var attr = node.attributes,
444             condition = false;
445
446         if(attr && attr.leaf) {
447             condition = true;
448             Ext.each(grants, function(grant) {
449                 condition = condition && attr.container.account_grants[grant];
450             }, this);
451         }
452
453         return condition;
454     },
455
456     /**
457      * @private
458      * - select default path
459      */
460     afterRender: function() {
461         Tine.widgets.container.TreePanel.superclass.afterRender.call(this);
462
463         var defaultContainerPath = this.getDefaultContainerPath();
464
465         if (defaultContainerPath && defaultContainerPath != '/') {
466             var root = '/' + this.getRootNode().id;
467
468             this.expand();
469
470             // @TODO use getTreePath() when filemanager is fixed
471             (function() {
472                 // no initial load triggering here
473                 this.getSelectionModel().suspendEvents();
474                 this.selectPath(root + defaultContainerPath);
475                 this.getSelectionModel().resumeEvents();
476             }).defer(100, this);
477         }
478     },
479
480     /**
481      * @private
482      */
483     initContextMenu: function() {
484
485         this.contextMenuUserFolder = Tine.widgets.tree.ContextMenu.getMenu({
486             nodeName: this.containerName,
487             actions: ['add'],
488             scope: this,
489             backend: 'Tinebase_Container',
490             backendModel: 'Container'
491         });
492
493         this.contextMenuSingleContainer = Tine.widgets.tree.ContextMenu.getMenu({
494             nodeName: this.containerName,
495             actions: ['delete', 'rename', 'grants'].concat(
496                 this.useProperties ? ['properties'] : []
497             ).concat(
498                 this.useContainerColor ? ['changecolor'] : []
499             ),
500             scope: this,
501             backend: 'Tinebase_Container',
502             backendModel: 'Container'
503         });
504
505         this.contextMenuSingleContainerProperties = Tine.widgets.tree.ContextMenu.getMenu({
506             nodeName: this.containerName,
507             actions: ['properties'],
508             scope: this,
509             backend: 'Tinebase_Container',
510             backendModel: 'Container'
511         });
512     },
513
514     /**
515      * called when node is appended to this tree
516      */
517     onAppendNode: function(tree, parent, appendedNode, idx) {
518         if (appendedNode.leaf && this.hasGrant(appendedNode, this.requiredGrants)) {
519             if (this.useContainerColor) {
520                 appendedNode.ui.render = appendedNode.ui.render.createSequence(function () {
521                     this.colorNode = Ext.DomHelper.insertAfter(this.iconNode, {
522                         tag: 'span',
523                         html: '&nbsp;&#9673;&nbsp',
524                         style: {color: appendedNode.attributes.container.color || '#808080'}
525                     }, true);
526                 }, appendedNode.ui);
527             }
528         }
529     },
530
531     /**
532      * expand automatically on node click
533      *
534      * @param {} node
535      * @param {} e
536      */
537     onClick: function(node, e) {
538         var sm = this.getSelectionModel(),
539             selectedNode = sm.getSelectedNode();
540
541         // NOTE: in single select mode, a node click on a selected node does not trigger 
542         //       a selection change. We need to do this by hand here
543         if (! this.allowMultiSelection && node == selectedNode) {
544             this.onSelectionChange(sm, node);
545         }
546
547         node.expand();
548     },
549
550     /**
551      * show context menu
552      *
553      * @param {} node
554      * @param {} event
555      */
556     onContextMenu: function(node, event) {
557         this.ctxNode = node;
558         var container = node.attributes.container,
559             path = container.path,
560             owner;
561
562         if (! Ext.isString(path)) {
563             return;
564         }
565
566         event.stopPropagation();
567         event.preventDefault();
568
569         if (Tine.Tinebase.container.pathIsContainer(path)) {
570             if (container.account_grants && container.account_grants.adminGrant) {
571                 this.contextMenuSingleContainer.showAt(event.getXY());
572             } else {
573                 this.contextMenuSingleContainerProperties.showAt(event.getXY());
574             }
575         } else if (path.match(/^\/shared$/) && (Tine.Tinebase.common.hasRight('admin', this.app.appName) || Tine.Tinebase.common.hasRight('manage_shared_folders', this.app.appName))){
576             this.contextMenuUserFolder.showAt(event.getXY());
577         } else if (Tine.Tinebase.registry.get('currentAccount').accountId == Tine.Tinebase.container.pathIsPersonalNode(path)){
578             this.contextMenuUserFolder.showAt(event.getXY());
579         }
580     },
581
582     /**
583      * adopt attr
584      *
585      * @param {Object} attr
586      */
587     onBeforeCreateNode: function(attr) {
588         if (attr.accountDisplayName) {
589             attr.name = attr.accountDisplayName;
590             attr.path = '/personal/' + attr.accountId;
591             attr.id = attr.accountId;
592         }
593
594         if (! attr.name && attr.path) {
595             attr.name = Tine.Tinebase.container.path2name(attr.path, this.containerName, this.containersName);
596         }
597
598         Ext.applyIf(attr, {
599             text: Ext.util.Format.htmlEncode(attr.name),
600             qtip: Tine.Tinebase.common.doubleEncode(attr.name),
601             leaf: !!attr.account_grants,
602             allowDrop: !!attr.account_grants && attr.account_grants.addGrant
603         });
604
605         // copy 'real' data to container space
606         attr.container = Ext.copyTo({}, attr, Tine.Tinebase.Model.Container.getFieldNames());
607     },
608
609     /**
610      * returns params for async request
611      *
612      * @param {Ext.tree.TreeNode} node
613      * @return {Object}
614      */
615     onBeforeLoad: function(node) {
616         var path = node.attributes.path;
617         var type = Tine.Tinebase.container.path2type(path);
618         var owner = Tine.Tinebase.container.pathIsPersonalNode(path);
619
620         if (type === 'personal' && ! owner) {
621             type = 'otherUsers';
622         }
623
624         var params = {
625             method: 'Tinebase_Container.getContainer',
626             application: this.app.appName,
627             containerType: type,
628             requiredGrants: this.requiredGrants,
629             owner: owner
630         };
631
632         return params;
633     },
634
635     /**
636      * permit selection of nodes with missing required grant
637      *
638      * @param {} sm
639      * @param {} newSelection
640      * @param {} oldSelection
641      * @return {Boolean}
642      */
643     onBeforeSelect: function(sm, newSelection, oldSelection) {
644
645         if (this.requiredGrant && newSelection.isLeaf()) {
646             var accountGrants =  newSelection.attributes.container.account_grants || {};
647             if (! accountGrants[this.requiredGrant]) {
648                 var message = '<b>' +String.format(i18n._("You are not allowed to select the {0} '{1}':"), this.containerName, newSelection.attributes.text) + '</b><br />' +
649                               String.format(i18n._("{0} grant is required for desired action"), this.requiredGrant);
650                 Ext.Msg.alert(i18n._('Insufficient Grants'), message);
651                 return false;
652             }
653         }
654     },
655
656     /**
657      * record got dropped on container node
658      *
659      * @param {Object} dropEvent
660      * @private
661      *
662      * TODO use Ext.Direct
663      */
664     onBeforeNodeDrop: function(dropEvent) {
665         var targetContainerId = dropEvent.target.id;
666
667         // get selection filter from grid
668         var sm = this.app.getMainScreen().getCenterPanel().getGrid().getSelectionModel();
669         if (sm.getCount() === 0) {
670             return false;
671         }
672         var filter = sm.getSelectionFilter();
673
674         // move messages to folder
675         Ext.Ajax.request({
676             params: {
677                 method: 'Tinebase_Container.moveRecordsToContainer',
678                 targetContainerId: targetContainerId,
679                 filterData: filter,
680                 model: this.recordClass.getMeta('modelName'),
681                 applicationName: this.recordClass.getMeta('appName')
682             },
683             scope: this,
684             success: function(result, request){
685                 // update grid
686                 this.app.getMainScreen().getCenterPanel().loadGridData();
687             }
688         });
689
690         // prevent repair actions
691         dropEvent.dropStatus = true;
692         return true;
693     },
694
695     /**
696      * require reload when node is collapsed
697      */
698     onBeforeCollapse: function(node) {
699         node.removeAll();
700         node.loaded = false;
701     },
702
703     onFilterChange: function() {
704         // get filterToolbar
705         var ftb = this.filterPlugin.getGridPanel().filterToolbar;
706
707         // in case of filterPanel
708         ftb = ftb.activeFilterPanel ? ftb.activeFilterPanel : ftb;
709
710         // remove all ftb container and /toberemoved/ filters
711         ftb.supressEvents = true;
712         ftb.filterStore.each(function(filter) {
713             var field = filter.get('field');
714             // @todo find criteria what to remove
715             if (field === 'container_id' || field === 'attender' || field === 'path') {
716                 ftb.deleteFilter(filter);
717             }
718         }, this);
719         ftb.supressEvents = false;
720
721         // set ftb filters according to tree selection
722         var containerFilter = this.getFilterPlugin().getFilter();
723         ftb.addFilter(new ftb.record(containerFilter));
724
725         ftb.onFiltertrigger();
726     },
727
728     /**
729      * called when tree selection changes
730      *
731      * @param {} sm
732      * @param {} nodes
733      */
734     onSelectionChange: function(sm, nodes) {
735
736         if (this.filterMode == 'gridFilter' && this.filterPlugin) {
737             this.filterPlugin.onFilterChange();
738         }
739         if (this.filterMode == 'filterToolbar' && this.filterPlugin) {
740
741             this.onFilterChange();
742
743             // finally select the selected node, as filtertrigger clears all selections
744             sm.suspendEvents();
745             Ext.each(nodes, function(node) {
746                 sm.select(node, Ext.EventObject, true);
747             }, this);
748             sm.resumeEvents();
749         }
750     },
751
752     /**
753      * selects path by container Path
754      *
755      * @param {String} containerPath
756      * @param {String} [attr]
757      * @param {Function} [callback]
758      */
759     selectContainerPath: function(containerPath, attr, callback) {
760         return this.selectPath(this.getTreePath(containerPath), attr, callback);
761     },
762
763     /**
764      * get default container for new records
765      *
766      * @param {String} default container registry key
767      * @return {Tine.Tinebase.Model.Container}
768      */
769     getDefaultContainer: function(registryKey) {
770         if (! registryKey) {
771             registryKey = 'defaultContainer';
772         }
773
774         var container = Tine[this.appName].registry.get(registryKey);
775
776         return this.getSelectedContainer('addGrant', container, true);
777     }
778 });