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)
8 Ext.ns('Tine.widgets', 'Tine.widgets.container');
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
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>
21 var taskPanel = new Tine.containerTreePanel({
22 app: Tine.Tinebase.appMgr.get('Tasks'),
23 recordClass: Tine.Tasks.Model.Task
27 Tine.widgets.container.TreePanel = function(config) {
28 Ext.apply(this, config);
32 * @event containeradded
33 * Fires when a container was added
34 * @param {container} the new container
38 * @event containerdelete
39 * Fires when a container got deleted
40 * @param {container} the deleted container
44 * @event containerrename
45 * Fires when a container got renamed
46 * @param {container} the renamed container
50 * @event containerpermissionchange
51 * Fires when a container got renamed
52 * @param {container} the container whose permissions where changed
54 'containerpermissionchange',
56 * @event containercolorset
57 * Fires when a container color got changed
58 * @param {container} the container whose color where changed
63 Tine.widgets.container.TreePanel.superclass.constructor.call(this);
66 Ext.extend(Tine.widgets.container.TreePanel, Ext.tree.TreePanel, {
68 * @cfg {Tine.Tinebase.Application} app
72 * @cfg {Boolean} allowMultiSelection (defaults to true)
74 allowMultiSelection: true,
76 * @cfg {String} defaultContainerPath
78 defaultContainerPath: null,
80 * @cfg {array} extraItems additional items to display under all
84 * @cfg {String} filterMode one of:
85 * - gridFilter: hooks into the grids.store
86 * - filterToolbar: hooks into the filterToolbar (container filterModel required)
88 filterMode: 'gridFilter',
90 * @cfg {Tine.data.Record} recordClass
94 * @cfg {Array} requiredGrants
95 * grants which are required to select leaf node(s)
100 * @cfg {Boolean} useContainerColor
101 * use container colors
103 useContainerColor: false,
105 * @cfg {Boolean} useProperties
106 * use container properties
112 * modelConfiguration of recordClass (if available)
114 modelConfiguration: null,
120 canonicalName: 'ContainerTree',
123 * Referenced grid panel
131 ddGroup: 'containerDDGroup',
132 hasPersonalContainer: true,
133 hasContextMenu: true,
136 * @fixme not needed => all events hand their events over!!!
138 * @property ctxNode holds treenode which got a contextmenu
139 * @type Ext.tree.TreeNode
144 * No user interactions, menus etc. allowed except for browsing
149 * init this treePanel
151 initComponent: function() {
152 if (! this.appName && this.recordClass) {
153 this.appName = this.recordClass.getMeta('appName');
156 this.app = Tine.Tinebase.appMgr.get(this.appName);
159 if (this.allowMultiSelection) {
160 this.selModel = new Ext.tree.MultiSelectionModel({});
163 if (this.recordClass) {
164 this.modelConfiguration = this.recordClass.getModelConfiguration();
167 if (this.modelConfiguration) {
168 this.hasPersonalContainer = this.modelConfiguration.hasPersonalContainer !== false;
171 var containerName = this.recordClass ? this.recordClass.getContainerName() : 'container';
172 var containersName = this.recordClass ? this.recordClass.getContainersName() : 'containers';
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);
178 this.loader = this.loader || new Tine.widgets.tree.Loader({
179 getParams: this.onBeforeLoad.createDelegate(this),
180 inspectCreateNode: this.onBeforeCreateNode.createDelegate(this)
183 this.loader.on('virtualNodesSelected', this.onVirtualNodesSelected.createDelegate(this));
185 var extraItems = this.getExtraItems();
186 this.root = this.getRoot(extraItems);
187 if (!this.hasPersonalContainer && ! extraItems.length) {
188 this.rootVisible = false;
191 if (!this.readOnly && !this.dropConfig) {
194 ddGroup: this.ddGroup || 'TreeDD',
195 appendOnly: this.ddAppendOnly === true,
199 onNodeOver: function (n, dd, e, data) {
202 // auto node expand check
203 if (node.hasChildNodes() && !node.isExpanded()) {
204 this.queueExpand(node);
206 return node.attributes.allowDrop ? 'tinebase-tree-drop-move' : false;
208 isValidDropPoint: function (n, dd, e, data) {
209 return n.node.attributes.allowDrop;
211 completeDrop: Ext.emptyFn
215 if (this.hasContextMenu) {
216 this.initContextMenu();
219 this.getSelectionModel().on('beforeselect', this.onBeforeSelect, this);
220 this.getSelectionModel().on('selectionchange', this.onSelectionChange, this);
222 this.on('click', this.onClick, this);
223 if (this.hasContextMenu) {
224 this.on('contextmenu', this.onContextMenu, this);
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);
233 Tine.widgets.container.TreePanel.superclass.initComponent.call(this);
239 onVirtualNodesSelected: function (nodes) {
240 this.suspendEvents();
242 if (0 === nodes.length) {
247 var sm = this.getSelectionModel();
250 sm.clearSelections(true);
252 for (var i = 0; i < nodes.length; i++) {
255 if (sm.isSelected(node)) {
256 sm.lastSelNode = node;
260 sm.selNodes.push(node);
261 sm.selMap[node.id] = node;
262 sm.lastSelNode = node;
263 node.ui.onSelectedChange(true);
266 this.onFilterChange();
271 * returns canonical path part
274 getCanonicalPathSegment: function () {
275 if (this.recordClass) {
277 this.recordClass.getMeta('modelName'),
279 ].join(Tine.Tinebase.CanonicalPath.separator);
283 getRoot: function(extraItems)
287 cls: 'tinebase-tree-hide-collapsetool',
290 path: Tine.Tinebase.container.getMyNodePath(),
292 hidden: !this.hasPersonalContainer
299 hidden: !this.hasPersonalContainer
300 }].concat(extraItems)
305 * template fn for subclasses to set default path
309 getDefaultContainerPath: function() {
310 return this.defaultContainerPath || '/';
314 * template fn for subclasses to append extra items
318 getExtraItems: function() {
319 return this.extraItems || [];
323 * returns a filter plugin to be used in a grid
325 getFilterPlugin: function() {
326 if (!this.filterPlugin) {
327 this.filterPlugin = new Tine.widgets.tree.FilterPlugin({
332 return this.filterPlugin;
336 * returns object of selected container/filter or null/default
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}
343 getSelectedContainer: function(requiredGrants, defaultContainer, onlySingle) {
344 var container = defaultContainer,
345 sm = this.getSelectionModel(),
346 selection = typeof sm.getSelectedNodes == 'function' ? sm.getSelectedNodes() : [sm.getSelectedNode()];
348 if (Ext.isArray(selection) && selection.length > 0 && (! onlySingle || selection.length === 1 || ! container)) {
349 container = this.getContainerFromSelection(selection, requiredGrants) || container;
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;
360 * get container from selection
362 * @param {Array} selection
363 * @param {Array} requiredGrants
364 * @return {Tine.Tinebase.Model.Container}
366 getContainerFromSelection: function(selection, requiredGrants) {
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
383 * get container from filter toolbar
385 * @param {Array} requiredGrants
386 * @return {Tine.Tinebase.Model.Container}
388 * TODO make this work -> atm we don't get the account grants here (why?)
390 getContainerFromFilter: function(requiredGrants) {
393 // check if single container is selected in filter toolbar
394 var ftb = this.filterPlugin.getGridPanel().filterToolbar,
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];
405 // take the first one
414 * convert containerPath to treePath
416 * @param {String} containerPath
417 * @return {String} treePath
419 getTreePath: function(containerPath) {
420 var treePath = '/' + this.getRootNode().id + (containerPath !== '/' ? containerPath : '');
422 // replace personal with otherUsers if personal && ! personal/myaccountid
423 var matches = containerPath.match(/^\/personal\/{0,1}([0-9a-z_\-]*)\/{0,1}/i);
425 if (matches[1] != Tine.Tinebase.registry.get('currentAccount').accountId) {
426 treePath = treePath.replace('personal', 'otherUsers');
428 treePath = treePath.replace('personal/' + Tine.Tinebase.registry.get('currentAccount').accountId, 'personal');
436 * checkes if user has requested grant for given container represented by a tree node
438 * @param {Ext.tree.TreeNode} node
439 * @param {Array} grant
442 hasGrant: function(node, grants) {
443 var attr = node.attributes,
446 if(attr && attr.leaf) {
448 Ext.each(grants, function(grant) {
449 condition = condition && attr.container.account_grants[grant];
458 * - select default path
460 afterRender: function() {
461 Tine.widgets.container.TreePanel.superclass.afterRender.call(this);
463 var defaultContainerPath = this.getDefaultContainerPath();
465 if (defaultContainerPath && defaultContainerPath != '/') {
466 var root = '/' + this.getRootNode().id;
470 // @TODO use getTreePath() when filemanager is fixed
472 // no initial load triggering here
473 this.getSelectionModel().suspendEvents();
474 this.selectPath(root + defaultContainerPath);
475 this.getSelectionModel().resumeEvents();
483 initContextMenu: function() {
485 this.contextMenuUserFolder = Tine.widgets.tree.ContextMenu.getMenu({
486 nodeName: this.containerName,
489 backend: 'Tinebase_Container',
490 backendModel: 'Container'
493 this.contextMenuSingleContainer = Tine.widgets.tree.ContextMenu.getMenu({
494 nodeName: this.containerName,
495 actions: ['delete', 'rename', 'grants'].concat(
496 this.useProperties ? ['properties'] : []
498 this.useContainerColor ? ['changecolor'] : []
501 backend: 'Tinebase_Container',
502 backendModel: 'Container'
505 this.contextMenuSingleContainerProperties = Tine.widgets.tree.ContextMenu.getMenu({
506 nodeName: this.containerName,
507 actions: ['properties'],
509 backend: 'Tinebase_Container',
510 backendModel: 'Container'
515 * called when node is appended to this tree
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, {
523 html: ' ◉ ',
524 style: {color: appendedNode.attributes.container.color || '#808080'}
532 * expand automatically on node click
537 onClick: function(node, e) {
538 var sm = this.getSelectionModel(),
539 selectedNode = sm.getSelectedNode();
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);
556 onContextMenu: function(node, event) {
558 var container = node.attributes.container,
559 path = container.path,
562 if (! Ext.isString(path)) {
566 event.stopPropagation();
567 event.preventDefault();
569 if (Tine.Tinebase.container.pathIsContainer(path)) {
570 if (container.account_grants && container.account_grants.adminGrant) {
571 this.contextMenuSingleContainer.showAt(event.getXY());
573 this.contextMenuSingleContainerProperties.showAt(event.getXY());
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());
585 * @param {Object} attr
587 onBeforeCreateNode: function(attr) {
588 if (attr.accountDisplayName) {
589 attr.name = attr.accountDisplayName;
590 attr.path = '/personal/' + attr.accountId;
591 attr.id = attr.accountId;
594 if (! attr.name && attr.path) {
595 attr.name = Tine.Tinebase.container.path2name(attr.path, this.containerName, this.containersName);
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
605 // copy 'real' data to container space
606 attr.container = Ext.copyTo({}, attr, Tine.Tinebase.Model.Container.getFieldNames());
610 * returns params for async request
612 * @param {Ext.tree.TreeNode} node
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);
620 if (type === 'personal' && ! owner) {
625 method: 'Tinebase_Container.getContainer',
626 application: this.app.appName,
628 requiredGrants: this.requiredGrants,
636 * permit selection of nodes with missing required grant
639 * @param {} newSelection
640 * @param {} oldSelection
643 onBeforeSelect: function(sm, newSelection, oldSelection) {
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);
657 * record got dropped on container node
659 * @param {Object} dropEvent
662 * TODO use Ext.Direct
664 onBeforeNodeDrop: function(dropEvent) {
665 var targetContainerId = dropEvent.target.id;
667 // get selection filter from grid
668 var sm = this.app.getMainScreen().getCenterPanel().getGrid().getSelectionModel();
669 if (sm.getCount() === 0) {
672 var filter = sm.getSelectionFilter();
674 // move messages to folder
677 method: 'Tinebase_Container.moveRecordsToContainer',
678 targetContainerId: targetContainerId,
680 model: this.recordClass.getMeta('modelName'),
681 applicationName: this.recordClass.getMeta('appName')
684 success: function(result, request){
686 this.app.getMainScreen().getCenterPanel().loadGridData();
690 // prevent repair actions
691 dropEvent.dropStatus = true;
696 * require reload when node is collapsed
698 onBeforeCollapse: function(node) {
703 onFilterChange: function() {
705 var ftb = this.filterPlugin.getGridPanel().filterToolbar;
707 // in case of filterPanel
708 ftb = ftb.activeFilterPanel ? ftb.activeFilterPanel : ftb;
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);
719 ftb.supressEvents = false;
721 // set ftb filters according to tree selection
722 var containerFilter = this.getFilterPlugin().getFilter();
723 ftb.addFilter(new ftb.record(containerFilter));
725 ftb.onFiltertrigger();
729 * called when tree selection changes
734 onSelectionChange: function(sm, nodes) {
736 if (this.filterMode == 'gridFilter' && this.filterPlugin) {
737 this.filterPlugin.onFilterChange();
739 if (this.filterMode == 'filterToolbar' && this.filterPlugin) {
741 this.onFilterChange();
743 // finally select the selected node, as filtertrigger clears all selections
745 Ext.each(nodes, function(node) {
746 sm.select(node, Ext.EventObject, true);
753 * selects path by container Path
755 * @param {String} containerPath
756 * @param {String} [attr]
757 * @param {Function} [callback]
759 selectContainerPath: function(containerPath, attr, callback) {
760 return this.selectPath(this.getTreePath(containerPath), attr, callback);
764 * get default container for new records
766 * @param {String} default container registry key
767 * @return {Tine.Tinebase.Model.Container}
769 getDefaultContainer: function(registryKey) {
771 registryKey = 'defaultContainer';
774 var container = Tine[this.appName].registry.get(registryKey);
776 return this.getSelectedContainer('addGrant', container, true);