0012932: file picker dialog in fileuploadgrid
[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         var extraItems = this.getExtraItems();
184         this.root = this.getRoot(extraItems);
185         if (!this.hasPersonalContainer && ! extraItems.length) {
186             this.rootVisible = false;
187         }
188
189         if (!this.readOnly) {
190             // init drop zone
191             this.dropConfig = {
192                 ddGroup: this.ddGroup || 'TreeDD',
193                 appendOnly: this.ddAppendOnly === true,
194                 /**
195                  * @todo check acl!
196                  */
197                 onNodeOver: function (n, dd, e, data) {
198                     var node = n.node;
199
200                     // auto node expand check
201                     if (node.hasChildNodes() && !node.isExpanded()) {
202                         this.queueExpand(node);
203                     }
204                     return node.attributes.allowDrop ? 'tinebase-tree-drop-move' : false;
205                 },
206                 isValidDropPoint: function (n, dd, e, data) {
207                     return n.node.attributes.allowDrop;
208                 },
209                 completeDrop: Ext.emptyFn
210             };
211         }
212
213         if (this.hasContextMenu) {
214             this.initContextMenu();
215         }
216
217         this.getSelectionModel().on('beforeselect', this.onBeforeSelect, this);
218         this.getSelectionModel().on('selectionchange', this.onSelectionChange, this);
219         this.on('click', this.onClick, this);
220         if (this.hasContextMenu) {
221             this.on('contextmenu', this.onContextMenu, this);
222         }
223
224         if (!this.readOnly) {
225             this.on('beforenodedrop', this.onBeforeNodeDrop, this);
226             this.on('append', this.onAppendNode, this);
227             this.on('beforecollapsenode', this.onBeforeCollapse, this);
228         }
229
230         Tine.widgets.container.TreePanel.superclass.initComponent.call(this);
231     },
232
233     /**
234      * returns canonical path part
235      * @returns {string}
236      */
237     getCanonicalPathSegment: function () {
238         if (this.recordClass) {
239             return [
240                 this.recordClass.getMeta('modelName'),
241                 this.canonicalName,
242             ].join(Tine.Tinebase.CanonicalPath.separator);
243         }
244     },
245
246     getRoot: function(extraItems)
247     {
248         return {
249             path: '/',
250             cls: 'tinebase-tree-hide-collapsetool',
251             expanded: true,
252             children: [{
253                 path: Tine.Tinebase.container.getMyNodePath(),
254                 id: 'personal',
255                 hidden: !this.hasPersonalContainer
256             }, {
257                 path: '/shared',
258                 id: 'shared'
259             }, {
260                 path: '/personal',
261                 id: 'otherUsers',
262                 hidden: !this.hasPersonalContainer
263             }].concat(extraItems)
264         };
265     },
266
267     /**
268      * template fn for subclasses to set default path
269      *
270      * @return {String}
271      */
272     getDefaultContainerPath: function() {
273         return this.defaultContainerPath || '/';
274     },
275
276     /**
277      * template fn for subclasses to append extra items
278      *
279      * @return {Array}
280      */
281     getExtraItems: function() {
282         return this.extraItems || [];
283     },
284
285     /**
286      * returns a filter plugin to be used in a grid
287      */
288     getFilterPlugin: function() {
289         if (!this.filterPlugin) {
290             this.filterPlugin = new Tine.widgets.tree.FilterPlugin({
291                 treePanel: this
292             });
293         }
294
295         return this.filterPlugin;
296     },
297
298     /**
299      * returns object of selected container/filter or null/default
300      *
301      * @param {Array} [requiredGrants]
302      * @param {Tine.Tinebase.Model.Container} [defaultContainer]
303      * @param {Boolean} onlySingle use default if more than one container in selection
304      * @return {Tine.Tinebase.Model.Container}
305      */
306     getSelectedContainer: function(requiredGrants, defaultContainer, onlySingle) {
307         var container = defaultContainer,
308             sm = this.getSelectionModel(),
309             selection = typeof sm.getSelectedNodes == 'function' ? sm.getSelectedNodes() : [sm.getSelectedNode()];
310
311         if (Ext.isArray(selection) && selection.length > 0 && (! onlySingle || selection.length === 1 || ! container)) {
312             container = this.getContainerFromSelection(selection, requiredGrants) || container;
313         }
314         // postpone this as we don't get the whole container record here
315 //        else if (this.filterMode == 'filterToolbar' && this.filterPlugin) {
316 //            container = this.getContainerFromFilter() || container;
317 //        }
318
319         return container;
320     },
321
322     /**
323      * get container from selection
324      *
325      * @param {Array} selection
326      * @param {Array} requiredGrants
327      * @return {Tine.Tinebase.Model.Container}
328      */
329     getContainerFromSelection: function(selection, requiredGrants) {
330         var result = null;
331
332         Ext.each(selection, function(node) {
333             if (node && Tine.Tinebase.container.pathIsContainer(node.attributes.container.path)) {
334                 if (! requiredGrants || this.hasGrant(node, requiredGrants)) {
335                     result = node.attributes.container;
336                     // take the first one
337                     return false;
338                 }
339             }
340         }, this);
341
342         return result;
343     },
344
345     /**
346      * get container from filter toolbar
347      *
348      * @param {Array} requiredGrants
349      * @return {Tine.Tinebase.Model.Container}
350      *
351      * TODO make this work -> atm we don't get the account grants here (why?)
352      */
353     getContainerFromFilter: function(requiredGrants) {
354         var result = null;
355
356         // check if single container is selected in filter toolbar 
357         var ftb = this.filterPlugin.getGridPanel().filterToolbar,
358             filterValue = null;
359
360         ftb.filterStore.each(function(filter) {
361             if (filter.get('field') == this.recordClass.getMeta('containerProperty')) {
362                 filterValue = filter.get('value');
363                 if (filter.get('operator') == 'equals') {
364                     result = filterValue;
365                 } else if (filter.get('operator') == 'in' && filterValue.length == 1){
366                     result = filterValue[0];
367                 }
368                 // take the first one
369                 return false;
370             }
371         }, this);
372
373         return result;
374     },
375
376     /**
377      * convert containerPath to treePath
378      *
379      * @param {String}  containerPath
380      * @return {String} treePath
381      */
382     getTreePath: function(containerPath) {
383         var treePath = '/' + this.getRootNode().id + (containerPath !== '/' ? containerPath : '');
384
385         // replace personal with otherUsers if personal && ! personal/myaccountid
386         var matches = containerPath.match(/^\/personal\/{0,1}([0-9a-z_\-]*)\/{0,1}/i);
387         if (matches) {
388             if (matches[1] != Tine.Tinebase.registry.get('currentAccount').accountId) {
389                 treePath = treePath.replace('personal', 'otherUsers');
390             } else {
391                 treePath = treePath.replace('personal/'  + Tine.Tinebase.registry.get('currentAccount').accountId, 'personal');
392             }
393         }
394
395         return treePath;
396     },
397
398     /**
399      * checkes if user has requested grant for given container represented by a tree node
400      *
401      * @param {Ext.tree.TreeNode} node
402      * @param {Array} grant
403      * @return {}
404      */
405     hasGrant: function(node, grants) {
406         var attr = node.attributes,
407             condition = false;
408
409         if(attr && attr.leaf) {
410             condition = true;
411             Ext.each(grants, function(grant) {
412                 condition = condition && attr.container.account_grants[grant];
413             }, this);
414         }
415
416         return condition;
417     },
418
419     /**
420      * @private
421      * - select default path
422      */
423     afterRender: function() {
424         Tine.widgets.container.TreePanel.superclass.afterRender.call(this);
425
426         var defaultContainerPath = this.getDefaultContainerPath();
427
428         if (defaultContainerPath && defaultContainerPath != '/') {
429             var root = '/' + this.getRootNode().id;
430
431             this.expand();
432             // @TODO use getTreePath() when filemanager is fixed
433             this.selectPath.defer(100, this, [root + defaultContainerPath]);
434         }
435
436         if (this.filterMode == 'filterToolbar' && this.filterPlugin && this.filterPlugin.getGridPanel()) {
437             this.filterPlugin.getGridPanel().filterToolbar.on('change', this.onFilterChange, this);
438         }
439     },
440
441     /**
442      * @private
443      */
444     initContextMenu: function() {
445
446         this.contextMenuUserFolder = Tine.widgets.tree.ContextMenu.getMenu({
447             nodeName: this.containerName,
448             actions: ['add'],
449             scope: this,
450             backend: 'Tinebase_Container',
451             backendModel: 'Container'
452         });
453
454         this.contextMenuSingleContainer = Tine.widgets.tree.ContextMenu.getMenu({
455             nodeName: this.containerName,
456             actions: ['delete', 'rename', 'grants'].concat(
457                 this.useProperties ? ['properties'] : []
458             ).concat(
459                 this.useContainerColor ? ['changecolor'] : []
460             ),
461             scope: this,
462             backend: 'Tinebase_Container',
463             backendModel: 'Container'
464         });
465
466         this.contextMenuSingleContainerProperties = Tine.widgets.tree.ContextMenu.getMenu({
467             nodeName: this.containerName,
468             actions: ['properties'],
469             scope: this,
470             backend: 'Tinebase_Container',
471             backendModel: 'Container'
472         });
473     },
474
475     /**
476      * called when node is appended to this tree
477      */
478     onAppendNode: function(tree, parent, appendedNode, idx) {
479         if (appendedNode.leaf && this.hasGrant(appendedNode, this.requiredGrants)) {
480             if (this.useContainerColor) {
481                 appendedNode.ui.render = appendedNode.ui.render.createSequence(function () {
482                     this.colorNode = Ext.DomHelper.insertAfter(this.iconNode, {
483                         tag: 'span',
484                         html: '&nbsp;&#9673;&nbsp',
485                         style: {color: appendedNode.attributes.container.color || '#808080'}
486                     }, true);
487                 }, appendedNode.ui);
488             }
489         }
490     },
491
492     /**
493      * expand automatically on node click
494      *
495      * @param {} node
496      * @param {} e
497      */
498     onClick: function(node, e) {
499         var sm = this.getSelectionModel(),
500             selectedNode = sm.getSelectedNode();
501
502         // NOTE: in single select mode, a node click on a selected node does not trigger 
503         //       a selection change. We need to do this by hand here
504         if (! this.allowMultiSelection && node == selectedNode) {
505             this.onSelectionChange(sm, node);
506         }
507
508         node.expand();
509     },
510
511     /**
512      * show context menu
513      *
514      * @param {} node
515      * @param {} event
516      */
517     onContextMenu: function(node, event) {
518         this.ctxNode = node;
519         var container = node.attributes.container,
520             path = container.path,
521             owner;
522
523         if (! Ext.isString(path)) {
524             return;
525         }
526
527         event.stopPropagation();
528         event.preventDefault();
529
530         if (Tine.Tinebase.container.pathIsContainer(path)) {
531             if (container.account_grants && container.account_grants.adminGrant) {
532                 this.contextMenuSingleContainer.showAt(event.getXY());
533             } else {
534                 this.contextMenuSingleContainerProperties.showAt(event.getXY());
535             }
536         } else if (path.match(/^\/shared$/) && (Tine.Tinebase.common.hasRight('admin', this.app.appName) || Tine.Tinebase.common.hasRight('manage_shared_folders', this.app.appName))){
537             this.contextMenuUserFolder.showAt(event.getXY());
538         } else if (Tine.Tinebase.registry.get('currentAccount').accountId == Tine.Tinebase.container.pathIsPersonalNode(path)){
539             this.contextMenuUserFolder.showAt(event.getXY());
540         }
541     },
542
543     /**
544      * adopt attr
545      *
546      * @param {Object} attr
547      */
548     onBeforeCreateNode: function(attr) {
549         if (attr.accountDisplayName) {
550             attr.name = attr.accountDisplayName;
551             attr.path = '/personal/' + attr.accountId;
552             attr.id = attr.accountId;
553         }
554
555         if (! attr.name && attr.path) {
556             attr.name = Tine.Tinebase.container.path2name(attr.path, this.containerName, this.containersName);
557         }
558
559         Ext.applyIf(attr, {
560             text: Ext.util.Format.htmlEncode(attr.name),
561             qtip: Tine.Tinebase.common.doubleEncode(attr.name),
562             leaf: !!attr.account_grants,
563             allowDrop: !!attr.account_grants && attr.account_grants.addGrant
564         });
565
566         // copy 'real' data to container space
567         attr.container = Ext.copyTo({}, attr, Tine.Tinebase.Model.Container.getFieldNames());
568     },
569
570     /**
571      * returns params for async request
572      *
573      * @param {Ext.tree.TreeNode} node
574      * @return {Object}
575      */
576     onBeforeLoad: function(node) {
577         var path = node.attributes.path;
578         var type = Tine.Tinebase.container.path2type(path);
579         var owner = Tine.Tinebase.container.pathIsPersonalNode(path);
580
581         if (type === 'personal' && ! owner) {
582             type = 'otherUsers';
583         }
584
585         var params = {
586             method: 'Tinebase_Container.getContainer',
587             application: this.app.appName,
588             containerType: type,
589             requiredGrants: this.requiredGrants,
590             owner: owner
591         };
592
593         return params;
594     },
595
596     /**
597      * permit selection of nodes with missing required grant
598      *
599      * @param {} sm
600      * @param {} newSelection
601      * @param {} oldSelection
602      * @return {Boolean}
603      */
604     onBeforeSelect: function(sm, newSelection, oldSelection) {
605
606         if (this.requiredGrant && newSelection.isLeaf()) {
607             var accountGrants =  newSelection.attributes.container.account_grants || {};
608             if (! accountGrants[this.requiredGrant]) {
609                 var message = '<b>' +String.format(i18n._("You are not allowed to select the {0} '{1}':"), this.containerName, newSelection.attributes.text) + '</b><br />' +
610                               String.format(i18n._("{0} grant is required for desired action"), this.requiredGrant);
611                 Ext.Msg.alert(i18n._('Insufficient Grants'), message);
612                 return false;
613             }
614         }
615     },
616
617     /**
618      * record got dropped on container node
619      *
620      * @param {Object} dropEvent
621      * @private
622      *
623      * TODO use Ext.Direct
624      */
625     onBeforeNodeDrop: function(dropEvent) {
626         var targetContainerId = dropEvent.target.id;
627
628         // get selection filter from grid
629         var sm = this.app.getMainScreen().getCenterPanel().getGrid().getSelectionModel();
630         if (sm.getCount() === 0) {
631             return false;
632         }
633         var filter = sm.getSelectionFilter();
634
635         // move messages to folder
636         Ext.Ajax.request({
637             params: {
638                 method: 'Tinebase_Container.moveRecordsToContainer',
639                 targetContainerId: targetContainerId,
640                 filterData: filter,
641                 model: this.recordClass.getMeta('modelName'),
642                 applicationName: this.recordClass.getMeta('appName')
643             },
644             scope: this,
645             success: function(result, request){
646                 // update grid
647                 this.app.getMainScreen().getCenterPanel().loadGridData();
648             }
649         });
650
651         // prevent repair actions
652         dropEvent.dropStatus = true;
653         return true;
654     },
655
656     /**
657      * called on filtertrigger of filter toolbar
658      * clears selection silently
659      */
660     onFilterChange: function() {
661         /* 2012-01-30 cweiss: i have no idea what this was for.
662          * if its needed please document here!
663         var sm = this.getSelectionModel();
664
665         sm.suspendEvents();
666         sm.clearSelections();
667         sm.resumeEvents();
668         */
669     },
670
671     /**
672      * If first node is collapsed, reload all data
673      */
674     onBeforeCollapse: function(node) {
675         if (node.attributes && node.attributes.path == "/") {
676             this.getLoader().load(this.root);
677         }
678     },
679
680
681     /**
682      * called when tree selection changes
683      *
684      * @param {} sm
685      * @param {} node
686      */
687     onSelectionChange: function(sm, nodes) {
688                 
689         if (this.filterMode == 'gridFilter' && this.filterPlugin) {
690             this.filterPlugin.onFilterChange();
691         }
692         if (this.filterMode == 'filterToolbar' && this.filterPlugin) {
693             // get filterToolbar
694             var ftb = this.filterPlugin.getGridPanel().filterToolbar;
695             
696             // in case of filterPanel
697             ftb = ftb.activeFilterPanel ? ftb.activeFilterPanel : ftb;
698
699             // remove all ftb container and /toberemoved/ filters
700             ftb.supressEvents = true;
701             ftb.filterStore.each(function(filter) {
702                 var field = filter.get('field');
703                 // @todo find criteria what to remove
704                 if (field === 'container_id' || field === 'attender' || field === 'path') {
705                     ftb.deleteFilter(filter);
706                 }
707             }, this);
708             ftb.supressEvents = false;
709
710             // set ftb filters according to tree selection
711             var containerFilter = this.getFilterPlugin().getFilter();
712             ftb.addFilter(new ftb.record(containerFilter));
713
714             ftb.onFiltertrigger();
715
716             // finally select the selected node, as filtertrigger clears all selections
717             sm.suspendEvents();
718             Ext.each(nodes, function(node) {
719                 sm.select(node, Ext.EventObject, true);
720             }, this);
721             sm.resumeEvents();
722         }
723     },
724
725     /**
726      * selects path by container Path
727      *
728      * @param {String} containerPath
729      * @param {String} [attr]
730      * @param {Function} [callback]
731      */
732     selectContainerPath: function(containerPath, attr, callback) {
733         return this.selectPath(this.getTreePath(containerPath), attr, callback);
734     },
735
736     /**
737      * get default container for new records
738      *
739      * @param {String} default container registry key
740      * @return {Tine.Tinebase.Model.Container}
741      */
742     getDefaultContainer: function(registryKey) {
743         if (! registryKey) {
744             registryKey = 'defaultContainer';
745         }
746
747         var container = Tine[this.appName].registry.get(registryKey);
748
749         return this.getSelectedContainer('addGrant', container, true);
750     }
751 });