/*
* Copyright 2016 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.
*/
/**
* This tool does allow to do Solr search on contents
* @private
*/
Ext.define('Ametys.plugins.cms.search.solr.SolrContentSearchTool', {
extend: "Ametys.plugins.cms.search.ContentSearchTool",
statics: {
/**
* @private
* @readonly
* @property {Number} LABEL_WIDTH The label width in the form
*/
LABEL_WIDTH: 160
},
/**
* @cfg {Number} [formMinHeight=200] The form's minimum height
*/
formMinHeight: 200,
/**
* @cfg {Number} [formMaxHeight=600] The form's maximum height
*/
formMaxHeight: 600,
/**
* @property {String} _mode The CodeMirror mode
* @private
*/
_mode: 'text/x-solr-ametys',
/**
* @private
* @property {Boolean} [_readOnly=false] set to 'true' to open tool in read-only mode
*/
_readOnly: false,
/**
* @private
* @property {Boolean/Object} _modelError If false, there is currently no error with the Solr model. Otherwise, it is an object containing information about the error with the Solr model.
*/
getStateId: function()
{
return "uitool-solrsearch"
},
getParams: function()
{
let params = this.callParent(arguments);
if (this.isNotDestroyed())
{
params = Ext.apply({}, {
flex: this.form.collapsed ? this.form._lastKnownFlex : this.form.flex,
contentTypes: this.form.getComponent('contentTypes').getValue(),
query: this.form.getComponent('query').getValue(),
queryFlex: this.form.getComponent('query').flex,
columns: this.form.getComponent('columns').getValue(),
columnsFlex: this.form.getComponent('columns').flex,
facets: this.form.getComponent('facets').getValue(),
facetsFlex: this.form.getComponent('facets').flex
}, params);
}
return params;
},
setParams: function(params)
{
var initialTitle = this.getInitialConfig('title') || '';
if (params.title && !Ext.String.startsWith(params.title, initialTitle + ' - '))
{
params.title = initialTitle + ' - ' + params.title;
this.setTitle(params.title);
}
this._readOnly = params.readOnly === true;
this.callParent(arguments);
if (params.flex)
{
this.form.flex = params.flex;
}
if (params.contentTypes)
{
this.form.getComponent('contentTypes').setValue(params.contentTypes);
}
if (params.query)
{
this.form.getComponent('query').setValue(params.query);
}
if (params.queryFlex)
{
this.form.getComponent('query').flex = params.queryFlex;
}
if (params.columns)
{
this.form.getComponent('columns').setValue(params.columns);
}
if (params.columnsFlex)
{
this.form.getComponent('columns').flex = params.columnsFlex;
}
if (params.facets)
{
this.form.getComponent('facets').setValue(params.facets);
}
if (params.facetsFlex)
{
this.form.getComponent('facets').flex = params.facetsFlex;
}
if (this.isActivated())
{
// As the SolrContentSearchTool enables to customize the columns, we need to manually force the reconfiguring of the columns here
this.store.removeAll();
this._internalSetParams(true);
}
if (this._readOnly)
{
this.searchPanel.getForm().getFields().each(function (field) {
field.setReadOnly(true);
})
}
},
_createSearchFormPanel: function ()
{
var me = this;
var cfg = this._getSearchFormPanelConfig();
var formCfg = Ext.apply(cfg, {
split: true,
border: false,
layout: {
type: 'vbox',
align: 'stretch'
},
ui: 'light',
header: {
titlePosition: 1
},
title: "{{i18n PLUGINS_CMS_UITOOL_SOLR_SEARCH_CRITERIA}}",
collapsible: true,
titleCollapse: true,
animCollapse: false,
minHeight: me.formMinHeight,
maxHeight: me.formMaxHeight,
flex: 0.5,
referenceHolder: true,
defaultButton: 'launch',
defaultButtonTarget: 'el',
scrollable: true,
defaults: {
cls: 'ametys',
labelAlign: 'right',
labelSeparator: '',
labelWidth: Ametys.plugins.cms.search.solr.SolrContentSearchTool.LABEL_WIDTH,
xtype: 'textfield'
},
items: this._getSearchFormPanelItem(),
listeners: {
'resize': function(cmp, width, height)
{
if (!this.collapsed)
{
this._lastKnownFlex = this.flex
}
Ametys.tool.Tool.prototype.setParams.call(me, me.getParams());
}
},
getJsonValues: function()
{
var values = {},
fields = this.form.getFields().items,
fLen = fields.length,
field;
for (f = 0; f < fLen; f++)
{
field = fields[f];
values[field.getName()] = field.getJsonValue();
}
return values;
}
});
this.form = Ext.create ('Ext.form.FormPanel', formCfg);
// Initialize SolrHint to load all needed data
SolrHint.init();
return this.form;
},
/**
* @private
*/
_saveParams: function()
{
Ametys.tool.Tool.prototype.setParams.call(this, this.getParams());
},
/**
* @private
* Get the items of form panel
* @return {Object} The form items configuration
*/
_getSearchFormPanelItem: function()
{
return [
{
xtype: 'edition.select-content-types',
fieldLabel: "{{i18n PLUGINS_CMS_UITOOL_SOLR_SEARCH_CTYPES}}",
ametysDescription: "{{i18n PLUGINS_CMS_UITOOL_SOLR_SEARCH_CTYPES_DESC}}",
name: 'contentTypes',
itemId: 'contentTypes',
allowBlank: false,
readOnly: this._readOnly,
hideTrigger: this._readOnly,
multiple: true,
excludePrivate: false,
excludeMixin: false,
excludeAbstract: false,
excludeReferenceTable: false,
listeners: {
'change': Ext.Function.createSequence(this._onChangeCTypes, this._saveParams),
scope: this
}
},
{
xtype: 'solr-code',
margin: '0 0 0 0', // The 5px classic bottom margin will be replaced by the underlying splitter
mode: this._mode,
readOnly: this._readOnly,
fieldLabel: "{{i18n PLUGINS_CMS_UITOOL_SOLR_SEARCH_QUERY}}",
ametysDescription: "{{i18n PLUGINS_CMS_UITOOL_SOLR_SEARCH_QUERY_DESC}}",
name: "query",
itemId: "query",
minHeight: 40,
flex: 4,
listeners: {
'change': this._saveParams,
'resize': this._saveParams,
scope: this
},
allowBlank: false,
value: '*:*',
cmParams: {
lineWrapping: true,
extraKeys: {
'Ctrl-Space': 'autocomplete'
}
}
},
{
xtype: 'splitter',
cls: 'ametys-solr-splitter',
margin: '0 ' + 26 + ' 0 ' + (Ametys.plugins.cms.search.solr.SolrContentSearchTool.LABEL_WIDTH + 5)
},
{
xtype: 'solr-code',
margin: '0 0 0 0', // The 5px classic bottom margin will be replaced by the underlying splitter
readOnly: this._readOnly,
mode: 'solr-ametys-columns',
fieldLabel: "{{i18n PLUGINS_CMS_UITOOL_SOLR_SEARCH_COLUMNS}}",
ametysDescription: "{{i18n PLUGINS_CMS_UITOOL_SOLR_SEARCH_COLUMNS_DESC}}",
name: 'columns',
itemId: 'columns',
minHeight: 30,
flex: 1,
listeners: {
'change': this._saveParams,
'resize': this._saveParams,
scope: this
},
singleLine: true,
cmParams: {
lineNumbers: false,
lineWrapping: true,
styleActiveLine: false,
extraKeys: {
'Ctrl-Space': 'autocomplete'
},
hintOptions: {
allowAllOption: true
}
}
},
{
xtype: 'splitter',
cls: 'ametys-solr-splitter',
margin: '0 ' + 26 + ' 0 ' + (Ametys.plugins.cms.search.solr.SolrContentSearchTool.LABEL_WIDTH + 5)
},
{
xtype: 'solr-code',
readOnly: this._readOnly,
mode: 'solr-ametys-columns',
fieldLabel: "{{i18n PLUGINS_CMS_UITOOL_SOLR_SEARCH_FACETS}}",
ametysDescription: "{{i18n PLUGINS_CMS_UITOOL_SOLR_SEARCH_FACETS_DESC}}",
name: 'facets',
itemId: 'facets',
minHeight: 30,
flex: 1,
listeners: {
'change': this._saveParams,
'resize': this._saveParams,
scope: this
},
singleLine: true,
cmParams: {
lineNumbers: false,
lineWrapping: true,
styleActiveLine: false,
extraKeys: {
'Ctrl-Space': 'autocomplete'
},
hintOptions: {
fieldName: 'facets'
}
}
}
]
},
_getSwitchModeButtons: function (items)
{
// Nothing
},
_createResultGrid: function(store)
{
var grid = this.callParent(arguments);
grid.on('columnmove', this._onColumnMove, this);
return grid;
},
/**
* Get the selected content types
* @return {String} the coma-separated list of selected content types, if any
*/
getSelectedCTypes: function()
{
return this.form.down('#contentTypes').getValue();
},
isAdvancedMode: function ()
{
return false;
},
getSearchValues: function()
{
let values = this.callParent(arguments);
values.parameters = this._lastExecutionParameters || {};
return values;
},
_launchSearch: function(params)
{
let me = this;
if (params && params.faceting && me._lastExecutionParameters)
{
exe(me._lastExecutionParameters);
}
else
{
Ametys.plugins.coreui.script.ScriptParameters.askScriptParameters(this.getSearchValues().query,
exe,
undefined, // title
undefined, // description
{ values: me.getParams().parameterValues }
);
}
function exe(parameters)
{
me._lastExecutionParameters = parameters;
if (parameters)
{
let values = {};
for (let paramName of Object.keys(parameters))
{
values[paramName] = parameters[paramName].value;
}
let p = Ametys.tool.Tool.prototype.getParams.call(me)
p.parameterValues = values;
// Calling set params to save params correctly, but not this tool instance to avoid re-execution of the search
Ametys.tool.Tool.prototype.setParams.call(me, p)
}
Ametys.data.ServerComm.send({
plugin: 'cms',
url: 'solr-search-model.json',
parameters: {
model: me._modelId,
values: me.getSearchValues(), // reexecute getSearchValues() so the modified _lastExecutionParameters is taken in account
contextualParameters: {}
},
priority: Ametys.data.ServerComm.PRIORITY_MAJOR,
waitMessage: true,
errorMessage: {
msg: "{{i18n UITOOL_SEARCH_ERROR}}",
category: me.self.getName()
},
responseType: 'text',
callback: {
handler: me._getSolrModelCb,
scope: me,
arguments: params
}
});
}
},
/**
* @private
* Callback for the solr model retrieving process
* @param {Object} response the server's response
* @param {Object} params the callback arguments
*/
_getSolrModelCb: function(response, params)
{
var result = Ext.JSON.decode(Ext.dom.Query.selectValue('', response));
if (result.success === true && Ext.isArray(result.columns))
{
this._modelError = false;
// Reconfigure
var fields = Ametys.plugins.cms.search.SearchGridHelper.getFieldsFromJson(result.columns);
Ametys.plugins.cms.search.ContentSearchTool.addDefaultContentFields(fields);
var columns = Ametys.plugins.cms.search.SearchGridHelper.getColumnsFromJson(result.columns, true, this.grid, true);
// Force columns to be visible as they were explicitly set by the user in the form
Ext.Array.forEach(columns, function(column) {
column.hidden = false;
});
Ext.data.schema.Schema.get('default').getEntity(this._modelName).replaceFields(fields, true);
this.grid.reconfigure(this.store, columns);
// Re-sort columns according to the original solr request (and not current state as it was done by reconfigure)
this._ignoreColumnSort = true;
for (let i = 0; i < columns.length; i++)
{
let realCols = this.grid.getColumns();
let realCol = realCols.find(x => x.dataIndex == columns[i].dataIndex);
let currentPosition = realCols.indexOf(realCol);
if (currentPosition != i + 1)
{
this.grid.headerCt.insert(i + 1, realCol);
}
}
this._ignoreColumnSort = false;
var params = params || {};
var sorters = params.sort || Ametys.plugins.cms.search.SearchGridHelper.getSortersFromJson(columns, this.grid);
this.store.sorters = null; // There is no other way to clean old sorters
this.store.setSorters(sorters);
Ametys.plugins.cms.search.solr.SolrContentSearchTool.superclass._launchSearch.call(this, params);
// Store the "initial" formatting
this._initialFormatting = this._getInitialFormatting(result.columns);
}
else
{
var message = "{{i18n UITOOL_SEARCH_ERROR}}";
var details = '';
if (result.error == 'column-error')
{
message = "{{i18n PLUGINS_CMS_UITOOL_SOLR_SEARCH_ERROR_COLUMNS}}";
details = result.message;
}
this._modelError = {
message: message,
details: details
};
this._showModelErrorDialog(this._modelError);
}
},
/**
* @protected
* Shows a dialog box with the current error on the Solr model (typically call this method after the user tries to launch a search)
* @param {Object} dialogOptions The options for the dialog box to display
* @param {String} dialogOptions.message The main text of the box. Should be localized
* @param {String/Error} [dialogOptions.details] The detailed text of the box. Hidden by default. Can be technical.
*/
_showModelErrorDialog: function(dialogOptions)
{
Ametys.log.ErrorDialog.display({
title: "{{i18n UITOOL_SEARCH_ERROR_TITLE}}",
text: dialogOptions.message,
details: dialogOptions.details,
category: 'Ametys.plugins.cms.search.solr.SolrContentSearchTool'
});
},
_onBeforeLoad: function(store, operation)
{
if (this._modelError)
{
this._showModelErrorDialog(this._modelError);
return false;
}
return this.callParent(arguments);
},
/**
* @private
* Get the additional extensions to use for this search tool.
* @param {String} location the location where to put the extensions, can be 'l' for 'left', 'r' for 'right' (defaults to 'l')
* @return {Object[]} The elements to add to the search bar
*/
_getAdditionalExtensions: function (location)
{
return Ametys.plugins.cms.search.solr.SolrContentSearchToolExtensions.getAdditionalButtons(location);
},
/**
* @private
* Callback function invoked whenever the content types' combo box value changed.
* If multiple records are selected, it also computes the common ancestor for all the selected records
* @param {Ext.form.field.Tag} combo The combobox
* @param {Object} newValue The new value
* @param {Object} oldValue The original value
*/
_onChangeCTypes: function(combo, newValue, oldValue)
{
var me = this;
// Delete the facet combo's last query to force reloading the store.
delete me.form.down('#facets').lastQuery;
// Update the contentType for autocompletion
me.form.getComponent('query').setContentTypes(newValue);
me.form.getComponent('columns').setContentTypes(newValue);
me.form.getComponent('facets').setContentTypes(newValue);
// Reinit sorters
me.store.sorters = null; // There is no other way to clean old sorters
me.store.setSorters([]);
},
/**
* @private
* Called when a column is moved to reflect (if possible) the new order on the column field of the search form
* @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
* @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
* @param {Number} fromIdx the starting index
* @param {Number} toIdx the ending index
*/
_onColumnMove: function(ct, column, fromIdx, toIdx)
{
if (this._ignoreColumnSort)
{
return;
}
var columnsField = this.form.down('#columns');
var columnValues = columnsField.getValue();
columnValues = columnValues.trim() ? columnValues.split(",").map(function(s) { return s.trim(); }) : [] /* As a split of "" lead to [""]... here we lead to [] */;
// If there are as many columns in reality as the field is referencing: it means that each column is referenced (no empty field nor '*' operator)
if (columnValues.length == ct.getGridColumns().length - 1) // -1 stand for the special extjs internal column
{
var valueToMove = columnValues[fromIdx];
Ext.Array.removeAt(columnValues, fromIdx);
if (fromIdx < toIdx)
{
// toIdx has changed since we removed a value before
toIdx -= 1;
}
Ext.Array.insert(columnValues, toIdx, [valueToMove])
columnsField.setValue(columnValues.join(', '));
}
},
applyFormatting: function(formatting)
{
var actualColumnFormatting = [];
// We need to retrieve the current columns list before applying the formatting
var current = this.getCurrentFormatting().columns;
// First add column from formatting if they exist
for (let i in formatting.columns)
{
for (let j in current)
{
if (current[j].dataIndex == formatting.columns[i].dataIndex)
{
actualColumnFormatting.push(formatting.columns[i]);
current.splice(j, 1);
break;
}
}
}
formatting.columns = actualColumnFormatting.concat(current);
// update the column field to reflect the formatting
var columnsField = this.form.down('#columns');
columnsField.setValue(formatting.columns.map(c => c.dataIndex).join(", "))
// reinsert the new formatting
arguments[0] = formatting;
this.callParent(arguments);
}
});