/*
* Copyright 2017 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.
*/
/**
* Tool for displaying the hierarchical simple contents.
* @private
*/
Ext.define('Ametys.plugins.cms.content.tool.HierarchicalReferenceTablesTool', {
extend: "Ametys.tool.Tool",
/**
* @property {Boolean} true if this tool supports candidates.
*/
allowCandidates: false,
/**
* @private
* @property {Ametys.plugins.cms.content.tree.HierarchicalReferenceTablesTree} _tree The tree of hierarchical reference table
*/
/**
* @private
* @property {String[]} _supportedContentTypes The list of supported content types
*/
/**
* @private
* @property {Ext.data.NodeInterface[]} _lastOutOfDateNodes The nodes explaininng why the tree is out of date
*/
constructor: function(config)
{
this._lastOutOfDateNodes = [];
this._supportedContentTypes = [];
this.callParent(arguments);
Ametys.message.MessageBus.on(Ametys.message.Message.SELECTION_CHANGED, this._onSelectionChanged, this);
Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onMessageCreated, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onMessageModified, this);
Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onMessageDeleted, this);
},
createPanel: function()
{
var plugins = [];
if (Ametys.plugins.cms.content.actions.HierarchicalReferenceTablesActions)
{
// only add cell editing if action is available
plugins = {
ptype: 'cellediting',
clicksToEdit: 1,
editAfterSelect: true,
listeners: {
'beforeedit': this._onBeforeEdit,
'edit': this._onEdit,
scope: this
}
};
}
this._tree = Ext.create('Ametys.plugins.cms.content.tree.HierarchicalReferenceTablesTree', {
rootVisible: true,
rootCfg: {
expanded: false,
text: "" // empty text before retrieving information about leaf content type
},
listeners: {
'selectionchange': this.sendCurrentSelection,
'itemdblclick': this._onItemDoubleClick,
scope: this
},
plugins: plugins,
enableColumnResize: false,
hideHeaders: true,
columns: [{
xtype: 'treecolumn',
dataIndex: 'text',
flex: 1,
editor: {
xtype: 'textfield',
allowBlank: false,
selectOnFocus: true
}
}],
viewConfig: {
plugins: {
ptype: 'ametystreeviewdragdrop',
containerScroll: true,
appendOnly: true,
sortOnDrop: true,
expandDelay: 500,
allowContainerDrops: false,
setAmetysDragInfos: Ext.bind(this._getDragInfo, this),
setAmetysDropZoneInfos: Ext.bind(this._getDropInfo, this)
}
}
});
return this._tree;
},
getMBSelectionInteraction: function()
{
return Ametys.tool.Tool.MB_TYPE_ACTIVE;
},
/**
* @inheritdoc
* @param {Object} params The params to set
* @param {String} params.contentType The content type of the leaves of the tree
* @param {String} params.workflowEditActionId The id of the workflow action to edit a simple content
* @param {String} params.contentDefaultTitle The default title of newly created contents in the ReferenceTableEditionTool
* @param {String} params.contentLanguage The language of simple contents
* @param {String} params.startSearchAtOpening "true" to start the search at the opening of the ReferenceTableEditionTool
*/
setParams: function(params)
{
this.showRefreshing();
this._tree.setReferenceTableId(params.contentType);
this.serverCall("getReferenceTableProperties", [params.contentType], this._getReferenceTablePropertiesCb, {arguments: params});
},
/**
* @private
* Callback function invoked after getting properties of this hierarchical reference table.
* @param {Object} result The reference table properties:
* @param {String} result.rootContentType The id of the root content type
* @param {String[]} result.supportedContentTypes The list of supported content types
* @param {Boolean} result.allowCandidates True if this reference table supports candidates
* @param {Object} params The tool parameters
*/
_getReferenceTablePropertiesCb: function(result, params)
{
this._supportedContentTypes = result.supportedContentTypes;
this.allowCandidates = result.allowCandidates;
Ametys.plugins.cms.content.tool.HierarchicalReferenceTablesTool.superclass.setParams.call(this, params);
var leafCtype = Ametys.cms.content.ContentTypeDAO.getContentType(this.getParams().contentType);
var rootCtype = Ametys.cms.content.ContentTypeDAO.getContentType(result.rootContentType);
this.setTitle(leafCtype.getLabel());
this.setDescription(leafCtype.getDescription() || '');
var iconGlyph = leafCtype.getIconGlyph();
if (iconGlyph != null)
{
this.setGlyphIcon(iconGlyph);
this.setIconDecorator(leafCtype.getIconDecorator());
}
else
{
this.setSmallIcon(leafCtype.getIconSmall());
this.setMediumIcon(leafCtype.getIconMedium());
this.setLargeIcon(leafCtype.getIconLarge());
}
this._tree.initializeTree(this.getParams().contentType, this.allowCandidates);
this.showUpToDate();
this.showRefreshed();
},
refresh: function()
{
if (this._destroyed)
{
return;
}
if (this._lastOutOfDateNodes.length > 0)
{
this.showRefreshing();
this._reloadNodes(this._lastOutOfDateNodes);
}
this._lastOutOfDateNodes = [];
},
/**
* @private
* Reload nodes, one by one
* @param {Ext.data.NodeInterface[]} nodes The nodes to be refreshed
*/
_reloadNodes: function(nodes)
{
// only keep nodes still in store to avoid side-effects
var store = this._tree.getStore(),
data = store.getData(),
me = this;
nodes = nodes.filter(function(n) {return data.contains(n);});
if (nodes.length > 0)
{
// We can only load one node at once
store.load({
node: nodes[0],
callback: function(records) {
me._tree.expandNode (nodes[0], false, Ext.bind(me._reloadNodes, me, [Ext.Array.slice(nodes, 1)]));
}
});
}
else
{
this.showRefreshed();
this.showUpToDate();
}
},
sendCurrentSelection: function()
{
var selection = this._tree.getSelection();
var targets = this._getMessageTargetCfg(selection);
Ext.create('Ametys.message.Message', {
type: Ametys.message.Message.SELECTION_CHANGED,
targets: targets
});
},
/**
* @private
* Gets the message targets configuration from an array of records
* @param {Ext.data.Model[]} records The records
* @return {Object[]} The message targets configuration
*/
_getMessageTargetCfg: function(records)
{
var targets = [];
var contentIds = [];
var candidateIds = [];
Ext.Array.each(records, function (record){
var contentId = record.get('contentId');
if (record.get('isCandidate'))
{
candidateIds.push(record.get('contentId'));
}
else
{
contentIds.push(record.get('contentId'));
}
});
if (Ext.Array.contains(contentIds, "root"))
{
targets.push({
id: Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT_ROOT,
parameters: {
contentType: this.getParams().contentType
}
});
contentIds = Ext.Array.remove(contentIds, "root");
}
if (Ext.Array.contains(contentIds, "rootCandidate"))
{
targets.push({
id: Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT_ROOT_CANDIDATE,
parameters: {
contentType: this.getParams().contentType
}
});
contentIds = Ext.Array.remove(contentIds, "rootCandidate");
}
if (contentIds.length > 0)
{
targets.push({
id: Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT,
parameters: {
ids: contentIds
}
});
}
if (candidateIds.length > 0)
{
targets.push({
id: Ametys.message.MessageTarget.REFERENCE_TABLE_CANDIDATE,
parameters: {
ids: candidateIds
}
});
}
return targets;
},
/**
* @private
* Listener called when an item in the tree is double clicked
* @param {Ext.view.View} view The view
* @param {Ext.data.Model} record The record that belongs to the item
*/
_onItemDoubleClick: function(view, record)
{
if (record.isLeaf() || record.data.isCandidate || record.data.name == 'rootCandidate')
{
return;
}
if (!record.isRoot())
{
var contentId = record.get('contentId');
this.serverCall("getParentAttributePath", [contentId], Ext.bind(this._openSearchTool, this, [contentId], 1));
}
else
{
this.serverCall("getParentAttributePathForRoot", [record.data.contenttypesIds[0]], Ext.bind(this._openSearchTool, this, [""], 1));
}
},
/**
* @private
* Open the search tool
* @param {String} path The path of metadata holding the parent
* @param {String} contentId The content of double-clicked content
*/
_openSearchTool: function (path, contentId)
{
var toolParams = this.getParams(),
parentMetadataCriteria = "reference-" + path + "-eq";
values = {},
values[parentMetadataCriteria] = contentId;
var toolFactoryId = 'uitool-reference-table',
searchToolId = "reference-table-search-ui." + toolParams.contentType;
Ametys.tool.ToolsManager.openTool(toolFactoryId, {
contentDefaultTitle: toolParams.contentDefaultTitle,
contentLanguage: toolParams.contentLanguage,
contentType: toolParams.contentType,
values: values,
id: searchToolId,
startSearchAtOpening: toolParams.startSearchAtOpening
}, 'cl');
},
/**
* @private
* Listener called before cell editing
* @param {Ext.grid.plugin.CellEditing} editor The cell editor
* @param {Object} context An editing context event with the following properties:
* @param {Ext.grid.Panel} context.grid The owning grid Panel.
* @param {Ext.data.Model} context.record The record being edited.
* @param {String} context.field The name of the field being edited.
* @param {Mixed} context.value The field's current value.
* @param {HTMLElement} context.row The grid row element.
* @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} being edited.
* @param {Number} context.rowIdx The index of the row being edited.
* @param {Number} context.colIdx The index of the column being edited.
* @param {Boolean} context.cancel Set this to `true` to cancel the edit or return false from your handler.
*/
_onBeforeEdit: function(editor, context)
{
var node = context.record;
return node && !node.isRoot();
},
/**
* @private
* Listener called after cell editing
* @param {Ext.grid.plugin.CellEditing} editor The cell editor
* @param {Object} context An editing context with the following properties:
* @param {Ext.grid.Panel} context.grid The owning grid Panel.
* @param {Ext.data.Model} context.record The record being edited.
* @param {String} context.field The name of the field being edited.
* @param {Mixed} context.value The field's current value.
* @param {HTMLElement} context.row The grid row element.
* @param {Ext.grid.column.Column} context.column The {@link Ext.grid.column.Column} being edited.
* @param {Number} context.rowIdx The index of the row being edited.
* @param {Number} context.colIdx The index of the column being edited.
* @param {Mixed} context.originalValue The original value before being edited.
*/
_onEdit: function(editor, context)
{
var record = context.record,
contentId = record && record.get('contentId'),
dirty = record && record.dirty,
newTitle = context.value;
if (contentId != null && context.field == "text" && newTitle && dirty)
{
var workflowActionId = this.getParams().workflowEditActionId;
this.serverCall("getTitleForEdition", [contentId], function(titleInfo){
var value = newTitle;
if (titleInfo.type == 'multilingual-string')
{
value = titleInfo.value || {};
value[Ametys.cms.language.LanguageDAO.getCurrentLanguage()] = newTitle;
}
Ametys.plugins.cms.content.actions.HierarchicalReferenceTablesActions.doRename(contentId, workflowActionId, value, function (success) {
if (!success)
{
// Cancel changes
record.beginEdit();
var oldTitle = context.originalValue;
record.set('text', oldTitle);
}
record.endEdit();
record.commit();
}, this);
});
}
},
/**
* Listener on creation message.
* @param {Ametys.message.Message} message The creation message.
* @private
*/
_onMessageCreated: function(message)
{
var candidateTargets = message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CANDIDATE);
if (candidateTargets.length > 0)
{
this._lastOutOfDateNodes.push(this._tree.getRootCandidate());
this.showOutOfDate(false);
}
},
/**
* Listener on edition message.
* @param {Ametys.message.Message} message The edition message.
* @private
*/
_onMessageModified: function(message)
{
var me = this,
targets = message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT);
if (targets.length > 0)
{
var nodes = this.getMatchingNodes(targets);
if (nodes.length > 0)
{
me._updateMatchingNodes(targets);
me._lastOutOfDateNodes = Ext.Array.merge(this._lastOutOfDateNodes, nodes);
me.showOutOfDate(false);
}
}
targets = message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT_ROOT);
Ext.Array.each(targets, function(target) {
var contentTypes = target.getParameters().contentType;
var node = me._tree.getRootNode();
if (Ext.Array.equals(node.get('contenttypesIds'), contentTypes) || Ext.Array.equals(this.getParameters().contentType, contentTypes))
{
me._lastOutOfDateNodes = Ext.Array.merge(me._lastOutOfDateNodes, node);
me.showOutOfDate(false);
}
});
},
/**
* Listener on deletion message.
* @param {Ametys.message.Message} message The deletion message.
* @private
*/
_onMessageDeleted: function(message)
{
var me = this;
var targets = message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT);
targets = targets.concat(message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CANDIDATE));
var currentSelection = this._tree.getSelectionModel().getSelection();
var nodes = this.getMatchingNodes(targets);
Ext.Array.each(nodes, function(node) {
if (currentSelection.length > 0 && currentSelection[0] == node)
{
// if deleted node is selected, select its parent
me._tree.getSelectionModel().select([node.parentNode]);
}
node.remove();
});
},
/**
* @private
* Filter nodes to keeps those which are in the tree
* @param {Ametys.message.MessageTarget[]} targets The messages to filter
* @return {Ext.data.NodeInterface[]} The matching nodes
*/
getMatchingNodes: function(targets)
{
var tree = this._tree;
var found = [];
Ext.Array.each(targets, function(target) {
var idModified = target.getParameters().id;
if (idModified)
{
var index = tree.getStore().findExact('contentId', idModified);
if (index != -1)
{
found.push(tree.getStore().getAt(index));
}
}
});
return found;
},
/**
* @private
* Update the text of nodes based on message targets
* @param {Ametys.message.MessageTarget[]} targets The message targets
*/
_updateMatchingNodes: function(targets)
{
var tree = this._tree,
me = this;
Ext.Array.each(targets, function(target) {
var idModified = target.getParameters().id;
if (idModified)
{
var treeStore = tree.getStore();
var index = treeStore.findExact('contentId', idModified);
if (index != -1)
{
var node = treeStore.getAt(index);
node.set('title', target.getParameters().title);
node.set('text', target.getParameters().title);
node.commit();
me._updateParentIfNeeded(treeStore, node, target);
}
}
});
},
/**
* @private
* Update parent of a node if needed
* @param {Ext.data.TreeStore} treeStore The tree store
* @param {Ametys.plugins.cms.content.tree.HierarchicalReferenceTablesTree.HierarchicalReferenceTablesTreeEntry} node The node
* @param {Ametys.message.MessageTarget} target The modified message target
*/
_updateParentIfNeeded: function(treeStore, node, target)
{
var oldParentId = node.get('parentId'),
oldParentIndex = treeStore.findExact('id', oldParentId),
oldParentInTree = null,
newParent = null;
if (oldParentIndex == -1)
{
// is root node, do nothing
return;
}
else
{
oldParentInTree = treeStore.getAt(oldParentIndex);
}
var parentIdInTarget = target.getParameters().parent;
var parentIdInTree = oldParentInTree.get('contentId');
if (parentIdInTarget == parentIdInTree || (parentIdInTree == 'root' && parentIdInTarget == null))
{
// same parent, the tree is up-to-date, no need to refresh some node (it probably comes from an internal D&D, or it is a regular edition)
return;
}
// tree is out-of-date, we will try to move node (it probably comes from an external D&D)
if (parentIdInTarget)
{
var index = treeStore.findExact('contentId', parentIdInTarget);
if (index != -1)
{
newParent = treeStore.getAt(index);
}
}
if (newParent)
{
newParent.insertBefore(node, null);
}
else if (oldParentInTree) // new parent is not in tree but old one is
{
node.remove();
}
},
/**
* @private
* Listener when the selection has changes. Update the tree selection if needeed.
* @param {Ametys.message.MessageBus} message The message bus
*/
_onSelectionChanged: function(message)
{
message = message || Ametys.message.MessageBus.getCurrentSelectionMessage();
var contentTargets = message.getTargets(Ametys.message.MessageTarget.REFERENCE_TABLE_CONTENT);
if (contentTargets.length > 0)
{
var contentId = contentTargets[0].getParameters().id;
var selection = this._tree.getSelectionModel().getSelection();
if (selection.length > 0 && selection[0].get('contentId') == contentId)
{
// Nothing to do, same selection
}
else
{
var index = this._tree.getStore().findExact('contentId', contentId);
if (index != -1)
{
// Find content's in tree, select it
var node = this._tree.getStore().getAt(index);
this._tree.setSelection(node);
}
else
{
// is the contenttype one of the supported?
if (Ext.Array.intersect(contentTargets[0].getParameters().types, this._supportedContentTypes).length > 0)
{
// Try to get the path of current selection in tree
this._getPathInTree(contentTargets[0].getParameters().id, function(pathInTree, args) {
if (pathInTree)
{
// The selected content is part of tree, select it
this.selectByPath(pathInTree)
}
}, this);
}
}
}
}
},
/**
* @template
* Get the path of current selected content into the current tree
* @param {String} contentId The id of content
* @param {Function} callback The callback function to call after getting the path in tree. Parameters have to be:
* @param {String} callback.path The path in tree or null if the content is not part of tree
* @param {Object} callback.args The callback arguments
* @param {Object} [scope] The scope for callback
* @param {Object} [args] The callback arguments.
*/
_getPathInTree: function(contentId, callback, scope, args)
{
Ametys.data.ServerComm.callMethod({
role: "org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper",
methodName: "getPathInHierarchy",
parameters: [contentId],
callback: {
handler: callback,
scope: scope || this
},
waitMessage: false
});
},
/**
* Expand the tree to the given path
* @param {String} path The path to expand
* @param {Boolean} autofocus true to focus to last expanded node
* @param {Boolean} expand true to expand to last expanded node
*/
selectByPath: function (path, autofocus, expand)
{
if (path)
{
this._tree.selectPath(path, 'name', '/', function(success, lastNode) {
if (success)
{
if (autofocus)
{
this._tree.getView().focusNode(lastNode);
}
if (expand)
{
this._tree.expandNode(lastNode);
}
}
}, 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)
{
if (item.records.length > 0 && item.records[0].get('isCandidate'))
{
// A candidate cannot be moved
return;
}
var messageTargets = this._getMessageTargetCfg(item.records);
if (messageTargets.length > 0)
{
item.source = {
relationTypes: [Ametys.relation.Relation.MOVE, Ametys.relation.Relation.REFERENCE, Ametys.relation.Relation.COPY],
targets: messageTargets
};
}
},
/**
* @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.
*/
_getDropInfo: function(targetRecords, item)
{
var messageTargets = this._getMessageTargetCfg(targetRecords);
if (messageTargets.length > 0)
{
item.target = {
relationTypes: [Ametys.relation.Relation.MOVE, Ametys.relation.Relation.REFERENCE, Ametys.relation.Relation.COPY],
targets: messageTargets
};
}
}
});
Ext.define("Ametys.message.HierarchicalReferenceTableMessageTarget", {
override: "Ametys.message.MessageTarget",
statics: {
/**
* @member Ametys.message.MessageTarget
* @readonly
* @property {String} REFERENCE_TABLE_CONTENT_ROOT The target type is the root of a hierarchy of simple contents. The expected parameters are:
* @property {String} REFERENCE_TABLE_CONTENT_ROOT.contentType The content type of the leaves
*/
REFERENCE_TABLE_CONTENT_ROOT: "reference-table-content-root",
/**
* @member Ametys.message.MessageTarget
* @readonly
* @property {String} REFERENCE_TABLE_CONTENT_ROOT_CANDIDATE The target type is the root of the candidates. The expected parameters are:
*/
REFERENCE_TABLE_CONTENT_ROOT_CANDIDATE: "reference-table-content-root-candidate",
/**
* @member Ametys.message.MessageTarget
* @readonly
* @property {String} REFERENCE_TABLE_CANDIDATE The target type is a 'candidate' for an entry of a reference table. See Ametys.plugins.cms.content.ContentMessageTargetFactory parameters to know more of the associated parameters.
*/
REFERENCE_TABLE_CANDIDATE: "reference-table-candidate"
}
});