/*
* Copyright 2021 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.
*/
/**
* Provides a Tree for displaying form directories and forms.
* @private
*/
Ext.define('Ametys.plugins.forms.tree.FormDirectoriesTree', {
extend: 'Ext.tree.Panel',
statics: {
/**
* Function to render form's title in result grid
* @param {Object} title The data title
* @param {Object} metaData A collection of metadata about the current cell
* @param {Ext.data.Model} record The record
* @private
*/
renderTitle: function(title, metaData, record)
{
var data = record.data;
if (data.isForm && !data.isConfigured)
{
return '<span title="{{i18n PLUGINS_FORMS_TREE_WARNING_NOT_CONFIGURED}}" >' + Ext.String.escapeHtml(title) + '</span>';
}
return Ext.String.escapeHtml(title);
},
/**
* Page renderer
* @param {Object} value The data value
* @param {Object} metaData A collection of metadata about the current cell
* @param {Ext.data.Model} record The record
* @return {String} The html representation of the page
*/
renderPage: function(value, metaData, record)
{
if (record.get('isForm') == true)
{
var html = "";
for (var index in value)
{
var val = value[index];
var toolId = val.isPage ? 'uitool-page' : 'uitool-sitemappage';
var title = val.isPage ? val.title : '{{i18n plugin.web:PLUGINS_WEB_SITEMAP_TREE_ROOT_LABEL}}';
html += '<a href="javascript:(function(){Ametys.tool.ToolsManager.openTool(\'' + toolId + '\', {id:\'' + val.id + '\'});})()">' + Ext.String.escapeHtml(title) + '</a> <br/>';
}
return html;
}
}
},
/**
* @cfg {Boolean} onlyDirectories true to only have the directories
*/
/**
* @cfg {String} profile The profile ('read_access' or 'write_access') filter. By default, if null, the server will treat the request as 'read_access'
*/
/**
* @private
* @property {Boolean} _isComputingRootNode To prevent multiple requests to refresh the root node.
*/
_isComputingRootNode: false,
constructor: function(config)
{
config.store = this._createStore(config);
config.allowEdition = Ext.isString(config.allowEdition)
? config.allowEdition == "true"
: (config.allowEdition || false);
if (config.onlyDirectories !== true)
{
config.dockedItems = [this._getToolbarCfg(config)];
}
var plugins = config.plugins;
config.plugins = Ext.isArray(plugins) ? plugins : (plugins == null ? [] : [plugins]);
if (config.allowEdition)
{
config.plugins.push({
ptype: 'cellediting',
clicksToEdit: 1,
editAfterSelect: true,
listeners: {
'beforeedit': this._onBeforeEdit,
'edit': this._onEdit,
scope: this
}
});
}
config.viewConfig = config.viewConfig || {};
Ext.apply(config.viewConfig, {
plugins: {
ptype: 'ametystreeviewdragdrop',
containerScroll: true,
appendOnly: true,
sortOnDrop: true,
expandDelay: 500,
setAmetysDragInfos: Ext.bind(this.getDragInfo, this),
setAmetysDropZoneInfos: Ext.bind(this.getDropInfo, this)
}
});
this.callParent(arguments);
// expand root node
var tree = this,
store = this.getStore(),
root = store.getRoot();
store.load({
callback: function() {
tree.expandNode(root);
}}
);
Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onCreatedMessage, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onUpdatedMessage, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MOVED, this._onMovedMessage, this);
},
onDestroy: function()
{
Ametys.message.MessageBus.unAll(this);
this.callParent(arguments);
},
/**
* Refresh the whole tree
* @param {Function} [callback] function to call after refreshing
*/
initRootNodeParameter: function (callback)
{
if (this._isComputingRootNode)
{
return;
}
this._isComputingRootNode = true;
var siteName = Ametys.getAppParameter("siteName");
Ametys.data.ServerComm.callMethod({
role: "org.ametys.plugins.forms.dao.FormDirectoryDAO",
methodName: "getRootProperties",
parameters: [siteName],
callback: {
scope: this,
handler: this._getRootNodeCb,
arguments: {
callback: callback
}
},
errorMessage: {
category: this.self.getName(),
msg: "{{i18n DAOS_FORM_ROOT_ERROR}}"
},
waitMessage: true
});
},
/**
* @private
* Callback function after retrieving root node
* @param {Object} response The server response
* @param {Object} args the callback arguments
*/
_getRootNodeCb: function (response, args)
{
this._isComputingRootNode = false;
this.setRootNode({
name: response.title,
canWrite: response.canWrite,
canEdit: response.canEdit,
canRename: response.canRename,
canEditRight: response.canEditRight,
expanded: this._showEmptyDirectories != null, // only load when we know if current user has right on empty directories or not
text: "{{i18n PLUGINS_FORMS_UITOOL_FORMS_ROOT_NAME}}"
});
if (Ext.isFunction (args.callback))
{
this.store.getRoot().on('expand', function() { args.callback (response.id); }, this, { single: true });
}
},
/**
* @private
* Creates the forms directory store
* @param {Object} config The configuration
* @param {Boolean} [config.onlyDirectories=false] See {@link #cfg-onlyDirectories}
* @param {String} [config.profile=null] See {@link #cfg-profile}
* @return {Ext.data.Store} The form store
*/
_createStore: function(config)
{
var onlyDirectories = config.onlyDirectories === true,
onlyConfiguredForm = config.onlyConfiguredForm === true
profile = config.profile,
siteName = Ametys.getAppParameter("siteName");
var store = Ext.create('Ext.data.TreeStore', {
model: 'Ametys.plugins.forms.tree.FormsTree.FormDirectoryEntry',
sorters: [
{property: 'text', direction:'ASC'}
],
proxy: {
type: 'ametys',
plugin: 'forms',
url: 'forms/list.json',
reader: {
type: 'json',
rootProperty: 'forms'
},
extraParams: {
onlyDirectories: onlyDirectories,
onlyConfiguredForm: onlyConfiguredForm,
profile: profile,
siteName : siteName
}
},
autoLoad: true,
folderSort: true,
root: {
id: 'root',
isForm: false,
expanded: true,
text: "{{i18n PLUGINS_FORMS_UITOOL_FORMS_ROOT_NAME}}"
}
});
return store;
},
/**
* @private
* Gets the toolbar configuration
* @param {Object} config The configuration
* @return {Object} The toolbar configuration
*/
_getToolbarCfg: function(config)
{
return {
dock: 'top',
xtype: 'toolbar',
layout: {
type: 'hbox',
align: 'stretch'
},
defaultType: 'button',
items: [{
// Search input
xtype: 'textfield',
cls: 'ametys',
flex: 1,
maxWidth: 300,
itemId: 'search-filter-input',
emptyText: "{{i18n PLUGINS_FORMS_TREE_FILTER_EMPTY_TEXT}}",
enableKeyEvents: true,
minLength: 3,
minLengthText: "{{i18n PLUGINS_FORMS_TREE_FILTER_MIN_LENGTH_INVALID}}",
msgTarget: 'qtip',
listeners: {change: Ext.Function.createBuffered(this._searchFilter, 500, this)}
}]
}
},
/**
* @private
* Filters forms by input field value.
* @param {Ext.form.Field} field The field
*/
_searchFilter: function (field)
{
var value = Ext.String.trim(field.getValue());
if (this._filterValue == value)
{
// Do nothing
return;
}
this._filterValue = value;
if (value.length > 2)
{
this.expandAll();
this._addSearchFilter();
}
else
{
this._removeSearchFilter();
}
},
/**
* @private
* Add filter on title on the store
*/
_addSearchFilter: function()
{
var id = 'search',
filterFactory = this._availableFilters[id],
filterFn = filterFactory(this._filterValue),
filterCfg = {
id: id,
filterFn: filterFn
};
this.getStore().removeFilter(id);
this.getStore().addFilter(filterCfg);
},
/**
* @private
* Remove the filter on title on the store
*/
_removeSearchFilter: function()
{
this._filterValue = null;
this.getStore().removeFilter('search');
},
/**
* @property {Object} _availableFilters The object containing the available filter functions (or filter function factories if they depend on another argument)
* @private
*/
_availableFilters: {
'search': function(searchedValue) {
return function(record){
function isLike() {
var title = Ext.String.deemphasize(record.get('text')).toLowerCase(),
search = Ext.String.deemphasize(searchedValue).toLowerCase();
return new RegExp(".*(" + search + ").*").test(title);
}
var isDirectory = !record.get('isForm'),
show = isDirectory || isLike();
return show;
}
}
},
/**
* @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 = item.records
.filter(hasRight)
.map(this.getMessageTargetConfiguration)
.filter(function(cfg) {return cfg != null;});
function hasRight(record)
{
if (record.get('isForm'))
{
return record.get('canWrite');
}
else
{
return record.get('canEdit');
}
}
if (targets.length > 0)
{
item.source = {
relationTypes: [Ametys.relation.Relation.MOVE],
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.
*/
getDropInfo: function(targetRecords, item)
{
var targets = targetRecords
.map(this.getMessageTargetConfiguration)
.filter(function(cfg) {return cfg != null});
if (targets.length > 0)
{
item.target = {
relationTypes: [Ametys.relation.Relation.MOVE],
targets: targets
};
}
},
/**
* @private
* Gets the configuration for creating message target
* @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)
{
return null;
}
else if (record.get('isForm'))
{
return {
id: Ametys.message.MessageTarget.FORM_TARGET,
parameters: {
id: record.getId()
}
};
}
else
{
return {
id: Ametys.message.MessageTarget.FORM_DIRECTORY,
parameters: {
id: record.getId(),
name: record.get('text'),
canWrite: record.get('canWrite'),
canEdit: record.get('canEdit'),
canRename: record.get('canRename'),
canEditRight: record.get('canEditRight')
}
};
}
},
/**
* @private
* Add filter on empty directories on the store
*/
_addEmptyDirectoriesFilter: function()
{
var id = 'emptyDirectories',
filterFn = this._availableFilters[id],
filterCfg = {
id: id,
filterFn: filterFn
};
this.getStore().removeFilter(id);
this.getStore().addFilter(filterCfg);
},
/**
* @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 record = context.record;
return record && !record.isRoot() && (record.get("isForm") && record.get('canWrite') || !record.get("isForm") && record.get('canRename'));
},
/**
* @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,
id = record.getId(),
newName = context.value,
oldName = context.originalValue;
if (newName != oldName)
{
if(record.getData().isForm)
{
Ametys.plugins.forms.dao.FormDAO.renameForm([id, newName], renameCb, { ignoreCallbackOnError: false })
}
else
{
Ametys.plugins.forms.dao.FormDirectoryDAO.renameFormDirectory([id, newName], renameCb, { ignoreCallbackOnError: false });
}
function renameCb(response)
{
if (!response || !response.id)
{
// edit failed
record.beginEdit();
record.set('text', oldName);
record.endEdit();
record.commit();
}
}
}
},
/**
* Listener on create messages
* @param {Ametys.message.Message} message The creation message.
* @private
*/
_onCreatedMessage: function(message)
{
var formTarget = message.getTarget(Ametys.message.MessageTarget.FORM_TARGET);
var formDirectoryTarget = message.getTarget(Ametys.message.MessageTarget.FORM_DIRECTORY);
var editingPlugin = this.editingPlugin;
if (formTarget)
{
this._onFormCreatedOrMoved(formTarget, beginEdit);
}
else if (formDirectoryTarget)
{
this._onFormCreatedOrMoved(formDirectoryTarget, beginEdit);
}
function beginEdit(created)
{
// FIXME startEdit is deprecated, but I was not able to found the "new" way to do it
if (editingPlugin)
{
editingPlugin.startEdit(created, 0);
}
}
},
/**
* Listener on moved messages
* @param {Ametys.message.Message} message The moved message.
* @private
*/
_onMovedMessage : function(message)
{
var formTarget = message.getTarget(Ametys.message.MessageTarget.FORM_TARGET);
var formDirectoryTarget = message.getTarget(Ametys.message.MessageTarget.FORM_DIRECTORY);
if (formTarget)
{
this._onFormCreatedOrMoved(formTarget, Ext.emptyFn);
}
else if (formDirectoryTarget)
{
this._onFormCreatedOrMoved(formDirectoryTarget, Ext.emptyFn);
}
},
/**
* Called when a Form or Form directory is created
* @param {Ametys.message.MessageTarget} target The Form message target
* @param {Function} cb The callback
* @private
*/
_onFormCreatedOrMoved: function(target, cb)
{
var store = this.getStore();
var selModel = this.getSelectionModel();
var createdId = target.getParameters().id;
var node = store.getNodeById(target.getParameters().parentId);
if (node == null)
{
node = this.getRootNode();
}
node.set('hasChildren', true);
node.set('expanded', true);
store.load({
node: node,
callback: function() {
selModel.select(node);
var created = store.getById(createdId);
if (created)
{
selModel.select(created);
cb(created);
}
}
});
},
/**
* Listener on update messages
* @param {Ametys.message.Message} message The edition message.
* @private
*/
_onUpdatedMessage: function(message)
{
var formTarget = message.getTarget(Ametys.message.MessageTarget.FORM_TARGET),
formDirectoryTarget = message.getTarget(Ametys.message.MessageTarget.FORM_DIRECTORY),
target = null,
newName;
if (formTarget)
{
target = formTarget;
newName = formTarget.getParameters().title;
}
else if (formDirectoryTarget)
{
target = formDirectoryTarget;
newName = formDirectoryTarget.getParameters().name;
}
if (target)
{
var store = this.getStore(),
node = store.getNodeById(target.getParameters().id);
if (node)
{
node.beginEdit();
node.set('text', newName);
node.endEdit();
node.commit();
store.sort();
}
}
},
/**
* Reloads parent node
* @param {Ametys.message.MessageTarget} target The message target
* @param {Boolean} keepSelection true to keep old selection
*/
reloadParent: function(id, keepSelection)
{
var store = this.getStore(),
node = store.getNodeById(id),
parentId = node && node.get('parentId'),
parentNode = store.getNodeById(parentId);
if (parentNode)
{
var tree = this;
var oldSelIds = tree.getSelection()
.map(function(record) {
return record.getId();
});
store.load({node: parentNode, callback: keepSelection ? keepSelectionFn : Ext.emptyFn});
function keepSelectionFn()
{
var selModel = tree.getSelectionModel();
var oldSel = oldSelIds
.map(function(id) {
return store.getNodeById(id);
})
.filter(function(record) { return record != null });
selModel.deselectAll();
selModel.select(oldSel);
}
}
},
/**
* Refresh tree and select the last selected node
* @param cb the callback function
*/
refresh: function(cb)
{
var selection = this.getSelectionModel().getSelection();
var node = selection.length > 0 ? selection[0] : null;
var selModel = this.getSelectionModel();
this.getStore().load({
scope: this,
callback: function () {
if (node != null)
{
var path = node.getPath("name");
this.expandPath(path, "name", null, function (successful, node) {
if (successful)
{
selModel.select(node);
this.getView().focusNode(node);
}
cb()
})
}
else
{
cb()
}
}
});
}
});