/*
* Copyright 2010 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @private
* This tool displays the JCR repository nodes in a tree.
*
* FIXME Bug when the tool has never been focused (its tab is inactive) and tries to select a node (to reflect another selection).
* See https://www.sencha.com/forum/showthread.php?302708-Select-a-node-of-a-not-rendered-tree
*/
Ext.define('Ametys.repository.tool.JcrViewTool',
{
extend: 'Ametys.repository.tool.RepositoryTool',
config:
{
/**
* @cfg {String} [defaultWorkspaceName='default'] the default repository workspace name.
*/
defaultWorkspaceName: 'default'
},
/**
* @property {String} pendingChangesCls The CSS class used when a node has pending changes
*/
pendingChangesCls: "pending-changes",
/**
* @property {String} [messageTargetType=Ametys.message.MessageTarget.REPOSITORY_NODE] The type of message target for nodes holding by the main tree panel
*/
messageTargetType: null,
/**
* @private
* @property {Ext.tree.Panel} _tree The main tree panel
*/
_tree: null,
/**
* @private
* @property {Ametys.repository.tree.SearchTreePanel} _searchTree The tree panel for search results.
*/
_searchTree: null,
/**
* @property {String[]} _nodesWithPendingChanges An array of the path of the nodes with pending changes.
*/
_nodesWithPendingChanges: [],
constructor: function(config)
{
this.callParent(arguments);
// Bus messages listeners
Ametys.message.MessageBus.on(Ametys.message.Message.SELECTION_CHANGED, this._onMessageSelectionChanged, this);
Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onMessageCreated, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MOVED, this._onMessageMoved, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onMessageModified, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFYING, this._onMessageModifying, this);
Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onMessageDeleted, this);
Ametys.message.MessageBus.on(Ametys.message.Message.SORTED, this._onMessageSorted, this);
Ametys.message.MessageBus.on(Ametys.message.Message.REVERTED, this._onMessageReverted, this);
this.messageTargetType = Ametys.message.MessageTarget.REPOSITORY_NODE;
},
createPanel: function()
{
// Create the main tree
this._tree = this._createTree();
this._tree.getSelectionModel().on('selectionchange', this.sendCurrentSelection, this);
this._tree.on('itemdblclick', this.openNode, this);
// Create the tree for search result
this._searchTree = Ext.create('Ametys.repository.tree.SearchTreePanel', {
maxHeight: 200,
split: true
});
this._searchTree.getSelectionModel().on('select', this.onSelectSearchNode, this);
this._searchTree.on('itemdblclick', this.openNode, this);
return {
xtype: 'panel',
layout: {
type: 'vbox',
align: 'stretch'
},
cls: 'ametys-repository-jcr-tool',
scrollable: false,
border: false,
items: [
this._tree,
this._searchTree
]
};
},
/**
* @protected
* Creates the tree panel
* @return {Ext.tree.Panel} the tree
*/
_createTree: function ()
{
return Ext.create('Ametys.repository.tree.JcrTreePanel', {
flex: 1,
split: true,
defaultWorkspaceName: this.getConfig('defaultWorkspaceName'),
viewConfig: {
plugins: [{
ptype: 'ametystreeviewdragdrop',
containerScroll: true,
appendOnly: false,
sortOnDrop: false,
expandDelay: 500,
allowContainerDrops: false,
setAmetysDragInfos: Ext.bind(this.getDragInfo, this),
setAmetysDropZoneInfos: Ext.bind(this.getDropInfo, this)
}]
},
listeners: {
'load': {fn: this._onLoad, scope: this}
}
});
},
/**
* @private
* This event is thrown by the getDragData to add the 'source' of the drag.
* @param {Object} item The default drag data that will be transmitted. You have to add a 'source' item in it:
* @param {Ametys.relation.RelationPoint} item.source The source (in the relation way) of the drag operation.
*/
getDragInfo: function(item)
{
var targets = [];
for (var i = 0; i < item.records.length; i++)
{
var cfg = this.getMessageTargetConfiguration(item.records[i]);
if (cfg != null)
{
targets.push(cfg);
}
}
if (targets.length > 0)
{
item.source = {
relationTypes: [Ametys.relation.Relation.MOVE, Ametys.relation.Relation.COPY, Ametys.relation.Relation.REFERENCE],
targets: targets
};
}
},
/**
* @private
* This event is thrown before the beforeDrop event and create the target of the drop operation relation.
* @param {Ext.data.Model[]} targetRecords The target records of the drop operation.
* @param {Object} item The default drag data that will be transmitted. You have to add a 'target' item in it:
* @param {Object} item.target The target (in the relation way) of the drop operation. A Ametys.relation.RelationPoint config.
* @param {"append"/"before"/"after"} dropPosition The drop mode
*/
getDropInfo: function(targetRecords, item, dropPosition)
{
var targets = [];
var positionInTargets = -1
for (var i = 0; i < targetRecords.length; i++)
{
var node = targetRecords[i];
if (node.isRoot() || dropPosition == 'append')
{
var cfg = this.getMessageTargetConfiguration(node);
if (cfg != null)
{
targets.push(cfg);
}
}
else if (node.parentNode != null) // dropPosition == 'before' or 'after'
{
// Get node position
for (var i=0; i < node.parentNode.childNodes.length; i++)
{
if (node.parentNode.childNodes[i].getId() == node.getId())
{
positionInTargets = i + (dropPosition == 'after' ? + 1 : 0);
break;
}
}
var cfg = this.getMessageTargetConfiguration(node.parentNode);
if (cfg != null)
{
targets.push(cfg);
}
}
}
if (targets.length > 0)
{
item.target = {
relationTypes: [Ametys.relation.Relation.MOVE],
targets: targets,
positionInTargets: positionInTargets
};
}
},
/**
* @protected
* Creates the tree panel for search results
* @return {Ext.tree.Panel} the search tree
*/
_createSearchTree: function ()
{
return Ext.create('Ametys.repository.tree.SearchTreePanel', {
maxHeight: 200,
split: true
});
},
getMBSelectionInteraction: function()
{
return Ametys.tool.Tool.MB_TYPE_ACTIVE;
},
getType: function()
{
return Ametys.tool.Tool.TYPE_REPOSITORY;
},
sendCurrentSelection: function()
{
var selection = this._tree.getSelectionModel().getSelection();
var targets = [];
if (selection.length > 0)
{
var node = selection[0];
targets.push(this.getMessageTargetConfiguration(node));
}
Ext.create('Ametys.message.Message', {
type: Ametys.message.Message.SELECTION_CHANGED,
targets: targets
});
},
/**
* @private
* Get the target configuration object for given record
* @param {Ext.data.Model} record The tree record to convert to its Ametys.message.MessageTarget configuration
* @return {Object} The configuration to create a Ametys.message.MessageTarget. Can be null, if the record is null or not relevant to be a messagetarget.
*/
getMessageTargetConfiguration: function (record)
{
if (record == null)
{
// Empty selection
return null;
}
else
{
return {
id: this.messageTargetType,
parameters: {
paths: [record.get('path')],
workspaceName: this._tree.getCurrentWorkspaceName()
}
};
}
},
setParams: function (params)
{
this.callParent(arguments);
var workspaceName = params.workspaceName || 'default';
var query = params.query;
var queryLanguage = params.queryLanguage || 'xpath';
var path = params.path;
if (!Ext.isEmpty(query))
{
this._executeSearchQuery(query, queryLanguage, workspaceName);
}
if (!Ext.isEmpty(path))
{
this._tree.selectNodeByPath(path);
}
},
/**
* Get the JCR node tree.
* @return {Ametys.repository.tree.JcrTreePanel} The JCR node tree.
*/
getTree: function()
{
return this._tree;
},
/**
* @private
* Function invoked after loading a node.
* Stores the path of nodes with pending changes.
* @param {Ext.data.TreeStore} store The tree store
* @param {Ext.data.Model[]} records The loaded records
* @param {Boolean} successful true if success
* @param {Ext.data.operation.Read} operation The data operation
*/
_onLoad: function (store, records, successful, operation)
{
if (operation.node.isRoot())
{
// Reset pending changes
this._nodesWithPendingChanges = [];
}
var me = this;
Ext.Array.each(records, function (record) {
if (record.get('isNew') || record.get('isModified'))
{
me._nodesWithPendingChanges.push(record.get('path'));
}
});
},
/**
* @private
* This listener is invoked when the selection has changed
* @param {Ametys.message.Message} message the selection message
*/
_onMessageSelectionChanged: function(message)
{
var target = message.getTarget(this.messageTargetType);
if (target != null)
{
var selection = this._tree.getSelectionModel().getSelection();
var node = this._tree.getStore().getNodeById(target.getParameters().id);
if (node != null && selection.length > 0 && node == selection[0])
{
// same selection
return;
}
if (target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
{
this._tree.selectNodeByPath(target.getParameters().pathWithGroups || target.getParameters().path);
}
}
},
/**
* @private
* This listener is invoked when a node has been created
* @param {Ametys.message.Message} message The bus message.
*/
_onMessageCreated: function(message)
{
var target = message.getTarget(this.messageTargetType);
if (target != null && target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
{
var path = target.getParameters().path;
var parentPath = path.substring(0, path.lastIndexOf('/'));
var parentNode = this._tree.getNodeByPath(parentPath);
if (parentNode != null)
{
this._tree.getStore().load({node: parentNode});
this.setInDirtyState(parentNode);
}
}
},
/**
* @private
* This listener is invoked when a node has been created
* @param {Ametys.message.Message} message The bus message.
*/
_onMessageMoved: function(message)
{
var me= this;
var target = message.getTarget(this.messageTargetType);
if (target != null && target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
{
var path = target.getParameters().path;
var parentPath = path.substring(0, path.lastIndexOf('/'));
var parentNode = me._tree.getNodeByPath(parentPath);
if (parentNode != null)
{
me._tree.getStore().load({node: parentNode, callback: function() {me._tree.getStore().load({node: parentNode})}}); // For some reason, the first load does nothing in some cases (sub node)
me.setInDirtyState(parentNode);
}
}
},
/**
* @private
* This listener is invoked when a node has on-going changes
* @param {Ametys.message.Message} message The bus message.
*/
_onMessageModifying: function(message)
{
var target = message.getTarget(this.messageTargetType);
if (target != null && target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
{
var path = target.getParameters().path;
var node = this._tree.getNodeByPath(path);
if (node != null)
{
this.setInDirtyState(node);
}
}
},
/**
* @private
* This listener is invoked when a node has been deleted
* @param {Ametys.message.Message} message The bus message.
*/
_onMessageDeleted: function(message)
{
var target = message.getTarget(this.messageTargetType);
if (target != null && target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
{
var path = target.getParameters().path;
var node = this._tree.getNodeByPath(path);
if (node != null)
{
var parentNode = node.parentNode;
this._tree.getSelectionModel().select([parentNode]);
node.remove();
this.setInDirtyState(parentNode);
}
}
},
/**
* @protected
* Set the node in a dirty state
* @param {Ext.data.NodeInterface} node The node
*/
setInDirtyState: function (node)
{
this._nodesWithPendingChanges.push(node.get('path'));
node.set('iconCls', 'a-tree-glyph ametysicon-document112 decorator-ametysicon-clock56', {convert: false});
this._tree.getView().addRowCls(node, this.pendingChangesCls);
node.commit();
},
/**
* @protected
* Remove the node dirty state
* @param {Ext.data.NodeInterface} node The node
*/
removeDirtyState: function (node)
{
node.set('iconCls', 'a-tree-glyph ametysicon-document112', {convert: false});
this._tree.getView().removeRowCls(node, this.pendingChangesCls);
node.commit();
},
/**
* @private
* This listener is invoked when the repository session has been saved.
* @param {Ametys.message.Message} message The bus message.
*/
_onMessageModified: function(message)
{
var target = message.getTarget(Ametys.message.MessageTarget.REPOSITORY_SESSION);
var currentWorkspace = Ametys.repository.RepositoryApp.getCurrentWorkspace();
if (target != null && target.getParameters().workspaceName == currentWorkspace)
{
this._refreshTreeChanges();
}
},
/**
* @private
* This listener is invoked when the repository session has been rolled back.
* @param {Ametys.message.Message} message The bus message.
*/
_onMessageReverted: function(message)
{
var target = message.getTarget(Ametys.message.MessageTarget.REPOSITORY_SESSION);
var currentWorkspace = Ametys.repository.RepositoryApp.getCurrentWorkspace();
if (target != null && target.getParameters().workspaceName == currentWorkspace)
{
this._refreshTreeChanges();
}
},
/**
* Triggered when a node is sorted.
* @param {Ametys.message.Message} message The bus message.
*/
_onMessageSorted: function(message)
{
var target = message.getTarget(this.messageTargetType);
if (target != null && target.getParameters().workspaceName == this._tree.getCurrentWorkspaceName())
{
var path = target.getParameters().path;
var workspaceName = target.getParameters().workspaceName;
var newOrder = message.getParameters().order;
var node = this._tree.getNodeByPath(path);
if (node != null)
{
// Get the first parent which is a node (not a group).
var jcrNode = node;
while (!jcrNode.isNode())
{
jcrNode = jcrNode.parentNode;
}
if (jcrNode.get('order') == null || jcrNode.get('order') == '')
{
jcrNode.set('order', Ametys.repository.RepositoryApp.getDefaultSort());
}
// If the order was changed, refresh the node and change the tooltip.
if (jcrNode.get('order') != newOrder)
{
jcrNode.set('order', newOrder);
this._tree.reloadAndSelect(jcrNode);
}
jcrNode.commit();
}
}
},
/**
* @private
* Execute a search query
* @param {String} query The string query
* @param {String} language The query language (XPath, SQL, JCR-SQL2)
* @param {String} workspaceName the workspace name. The query is not executed if the workspace is not the same as current tree workspace
*/
_executeSearchQuery: function(query, language, workspaceName)
{
if (workspaceName == this._tree.getCurrentWorkspaceName())
{
this._searchTree.setQuery(query, language, workspaceName);
this._searchTree.getStore().reload();
}
},
/**
* Refresh the tree changes to reflect the current
* @protected
*/
_refreshTreeChanges: function()
{
var me = this;
var store = me._tree.getStore();
Ext.Array.each(me._nodesWithPendingChanges, function (path) {
var nodes = store.query('path', path, false, false, true);
nodes.each (function (node) {
// Refresh the node and update UI
store.load({node: node});
me.removeDirtyState(node);
});
});
me._nodesWithPendingChanges = [];
},
/**
* Determines if the tree contains nodes with pending changes
* @return true if there is unsaved nodes.
*/
hasPendingChanges: function()
{
return this._nodesWithPendingChanges != null && this._nodesWithPendingChanges.length > 0;
},
/**
* Listens before a node selection to prevent selecting the root node.
* @param {Ext.selection.TreeModel} sm The tree selection model.
* @param {Ext.data.NodeInterface} record The selected node.
* @param {Number} index The row index of the record
* @param {Object} eOpts Options added to the addListener
* @private
*/
beforeSelectSearchNode: function(sm, record, index, eOpts)
{
// Prevent selecting the root node.
if (record == null || record.isRoot())
{
return false;
}
},
/**
* Listens for search node selection to select the corresponding node in the JCR tree.
* @param {Ext.selection.TreeModel} sm The tree selection model.
* @param {Ext.data.NodeInterface} record The selected node.
* @param {Number} index The row index of the record
* @param {Object} eOpts Options added to the addListener
* @private
*/
onSelectSearchNode: function(sm, record, index, eOpts)
{
if (record != null)
{
var path = record.get('pathWithGroups') || record.get('path') || record.get('text');
this._tree.selectNodeByPath(path);
}
},
/**
* Called when a node is double-clicked in the tree.
* @param {Ext.tree.View} view the tree view.
* @param {Ext.data.Model} node the double-clicked node
* @protected
*/
openNode: function(view, node)
{
var workspaceName = Ametys.repository.RepositoryApp.getCurrentWorkspace();
Ametys.repository.tool.NodePropertiesTool.open(node.get('path'), workspaceName);
}
});