/*
* Copyright 2013 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 helper provides a dialog box to search then select contents
*/
Ext.define('Ametys.cms.uihelper.SelectContentBySearch', {
singleton: true,
/**
* @property {Number} PAGE_SIZE The number of records to display by 'page'
* @readonly
*/
PAGE_SIZE: 50,
/**
* @property {Number} MIN_HEIGHT The default minimum height of the dialog box
* @readonly
*/
MIN_HEIGHT: 300,
/**
* @property {Number} MAX_HEIGHT The default maximum height of the dialog box
* @readonly
*/
MAX_HEIGHT: 600,
/**
* @property {Boolean} [_validated=false] True when the user validate the dialog box
* @private
*/
/**
* Configure and open the search contents dialog box.
* @param {Object} config The configuration options.
* @param {Object} config.modelId The id of search model to base search on.
* @param {Object} [config.searchModelParameters] A map of parameters passed to the search model.
* @param {String} [config.contentType] The id of content type to restrict the search on. Can be null.
* @param {String} [config.icon] The full path to icon (16x16 pixels) for the dialog box. Can be null if iconCls is used
* @param {String} [config.iconCls] One or more space separated CSS classes to be applied to the icon dialog box
* @param {String} config.title The title of the dialog box.
* @param {Function} config.callback The callback function invoked when contents are selected. The callback function will received the following parameters:
* @param {String[]/Ametys.cms.content.Content[]} config.callback.contents The selected contents as a Array of ids or an Array of Ametys.cms.content.Content[]. See #config.fullContents
* @param {String} config.helpmessage1 The message displayed at the top of the first card
* @param {String} config.helpmessage2 The message displayed at the top of the second card
* @param {Boolean} [config.fullContents=false] If `true`, the callback function receives Content objects as argument. If `false`, only content ids are provided.
* @param {Boolean} [config.multiple=false] `true` to allow selecting multiple contents, `false` otherwise.
* @param {Boolean} [config.excludeSubContents=false] `true` to exclude sub-contents from search
* @param {Object} [config.forceValues] A map with the values to force in search form for the field with the key as name. These values are only used if the field exists in the form.
* @param {String} [config.forceMode=hide] If forceValues is used, a String which specifies how the forced field should appear : 'hide' to hide the fields, 'readOnly' to display the fields preveting the user from changing the fields or 'disabled' to disable the fields
* @param {String} [config.boxSize] The dialog box size: 'large' for the window width with a 0.8 ratio or 'default' for a width of 600 pixels
*/
open: function (config)
{
config = config || {};
var modelId = config.modelId;
var contentType = config.contentType;
var contextualParameters = Ext.applyIf(config.searchModelParameters || {}, this.getContextualParameters());
this._validated = false;
var form = this._createConfigurableFormPanel();
var store = this._getStore(
form,
config.excludeSubContents ? config.excludeSubContents : false,
contentType,
modelId,
contextualParameters
);
var box = this._createDialogBox(
config.title,
config.icon,
config.iconCls,
config.minHeight || this.MIN_HEIGHT,
config.maxHeight || this.MAX_HEIGHT,
config.helpmessage1 || '',
config.helpmessage2 || '',
form,
store,
config.boxSize || 'default',
config.multiple || false,
config.fullContents || false,
config.callback
);
Ametys.data.ServerComm.callMethod({
role: "org.ametys.cms.search.model.SearchModelHelper",
methodName: "getRestrictedSearchModelConfiguration",
parameters: [modelId, contextualParameters],
callback: {
handler: Ext.bind(this._getSearchModelCb, this, [box, store], 2),
scope: this,
arguments: config
},
errorMessage: {
msg: "{{i18n UITOOL_SEARCH_ERROR}}",
category: Ext.getClassName(this)
},
waitMessage: "{{i18n plugin.core-ui:PLUGINS_CORE_UI_LOADMASK_DEFAULT_MESSAGE}}"
});
},
/**
* @template
* Get the contextual parameters used to get search model and search results.
* @return {Object} the contextual parameters
*/
getContextualParameters: function ()
{
return {
language: this._getCurrentLanguage()
};
},
/**
* Provides the current language to be used.
* @return {String} The current language
*/
_getCurrentLanguage: function()
{
return Ametys.cms.language.LanguageDAO.getCurrentLanguage();
},
/**
* @private
* Callback function after getting model.
* Updates the model and grid columns
* @param {Object} result The server response as JSON object
* @param {Object} config The configuration options of the dialog box.
* @param {Ametys.window.DialogBox} box The dialog box
* @param {Ext.data.Store} store The search store
*/
_getSearchModelCb: function (result, config, box, store)
{
var form = box.down("#select-content-form");
if (form instanceof Ametys.form.ConfigurableFormPanel)
{
form.configure(result['simple-criteria']);
}
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, false);
store.setProxy({
type: 'ametys',
plugin: result.searchUrlPlugin,
url: result.searchUrl,
reader: {
type: 'json',
rootProperty: 'contents'
}
});
if (result.pageSize != null && result.pageSize > 0)
{
store.setPageSize(result.pageSize);
}
// Update model
store.model.replaceFields(fields, true);
var grid = box.down("#select-content-grid");
grid.reconfigure (store, columns);
store.setSorters([]); // sort by score by default
form.getForm().reset();
this._navHandler(0, grid, form, store);
if (form instanceof Ametys.form.ConfigurableFormPanel)
{
form.setValues(); // setValues must always be called for configurable form panel in order to complete its initialization
}
if (config.forceValues)
{
form.executeFormReady(Ext.bind(this._initValues, this, [form, config.forceValues, config.forceMode || 'hide']));
}
box.show();
},
/**
* @private
* Initialize the form creation with the forced values
* @param {ConfigurableFormPanel} form The form to initialize
* @param {Object} values The values to force
* @param {String} forceMode The force mode : 'hide', 'readOnly' or 'disabled'
*/
_initValues: function (form, values, forceMode)
{
var prefix = form.getFieldNamePrefix();
for (key in values)
{
var fd = form.getField(prefix + key);
if (fd)
{
fd.setValue(values[key]);
if (forceMode == 'readOnly')
{
fd.setReadOnly(true)
}
else if (forceMode == 'disabled')
{
fd.setDisabled(true)
}
else
{
fd.setVisible(false);
}
}
}
},
/**
* Creates the dialog box
* @param {String} title The title of the dialog box.
* @param {String} icon The full path to icon (16x16 pixels) for the dialog box. Can be null if iconCls is used
* @param {String} iconCls CSS classes to be applied to the icon dialog box. Can be null ig icon is used.
* @param {number} minHeight The minimum size of the bow
* @param {number} maxHeight The maximum size of the bow
* @param {String} helpMsg1 The message displayed at the top of the first card
* @param {String} helpMsg2 The message displayed at the top of the second card
* @param {Ametys.form.ConfigurableFormPanel} form The configurable form panel
* @param {Ext.data.Store} store The search store
* @param {String} boxSize The dialog box size: 'large' for the window width with a 0.8 ratio or 'default' for a width of 600 pixels
* @param {Boolean} multiple True to allow multiple selections
* @param {Boolean} fullContents True to returns {@link Ametys.cms.content.Content}. If false only the content identifiers will be returned
* @param {Function} cbFn The callback function to call after choosing contents or canceling the contents search
* @private
*/
_createDialogBox: function (title, icon, iconCls, minHeight, maxHeight, helpMsg1, helpMsg2, form, store, boxSize, multiple, fullContents, cbFn)
{
var grid = Ext.create ('Ext.grid.Panel', {
flex: 1,
store: store,
itemId: 'select-content-grid',
scrollable: true,
columns: [],
selModel : {
mode: multiple ? 'MULTI' : 'SINGLE'
},
viewConfig: {
loadingText: "{{i18n plugin.cms:UITOOL_SEARCH_WAITING_MESSAGE}}"
},
dockedItems: [{
xtype: 'pagingtoolbar',
store: store,
dock: 'bottom',
displayInfo: true,
displayMsg: "{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_RESULT_1}}{0}{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_RESULT_2}}{1}{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_RESULT_3}}{2}",
emptyMsg: "{{i18n plugin.cms:UITOOL_CONTENTEDITIONGRID_NO_RESULT}}"
}],
listeners: { 'selectionchange': Ext.bind(this._onSelectionChange, this, [form], 2)}
});
var box = Ext.create('Ametys.window.DialogBox', {
title: title,
icon: icon,
iconCls: icon ? null : iconCls,
itemId: 'select-content-box',
width: boxSize === "large" ? window.innerWidth * 0.8 : 600,
scrollable: false,
layout: 'card',
activeItem: 0,
minHeight: minHeight,
maxHeight: maxHeight,
freezeHeight: true,
items: [{
border: false,
layout: {
type: 'vbox',
align: 'stretch'
},
items : [
{
xtype: 'component',
cls: 'a-text',
html: helpMsg1
},
form
]
},
{
border: false,
layout: {
type: 'vbox',
align: 'stretch'
},
items : [
{
xtype: 'component',
cls: 'a-text',
html: helpMsg2
},
grid
]
}
],
defaultFocus: form,
closeAction: 'destroy',
referenceHolder: true,
defaultButton: 'validate',
buttons : [{
text: "{{i18n PLUGINS_CMS_HELPER_SELECTCONTENTBYSEARCH_BUTTON_PRECEDE}}",
handler: Ext.bind(this._navHandler, this, [0, grid, form, store]),
disabled: true
}, {
reference: 'next',
text: "{{i18n PLUGINS_CMS_HELPER_SELECTCONTENTBYSEARCH_BUTTON_NEXT}}",
handler: Ext.bind(this._navHandler, this, [1, grid, form, store]),
disabled: false
}, {
reference: 'validate',
text: "{{i18n PLUGINS_CMS_HELPER_SELECTCONTENTBYSEARCH_BUTTON_OK}}",
disabled: true,
handler: Ext.bind(this._validate, this, [grid, fullContents, multiple, cbFn])
}, {
text: "{{i18n PLUGINS_CMS_HELPER_SELECTCONTENTBYSEARCH_BUTTON_CANCEL}}",
handler: Ext.bind(function() {box.close();}, this)
}],
listeners: {
close: Ext.bind(this._onClose, this, [cbFn])
}
});
return box;
},
/**
* Creates the configurable form panel
* @private
*/
_createConfigurableFormPanel: function()
{
return Ext.create ('Ametys.form.ConfigurableFormPanel', {
border: false,
bodyPadding: 10,
itemId: 'select-content-form',
flex: 1,
scrollable: true,
defaultFieldConfig: {
labelWidth: 130,
style: 'margin-right: 30px'
},
additionalWidgetsConfFromParams: {
contentType: 'contentType' // some widgets requires the contentType configuration
},
});
},
/**
* This function is called when navigating into the card layout by 'Next' or 'Precede' buttons
* @param {Number} index the index of card layout to show
* @param {Ext.grid.Panel} grid The search grid
* @param {Ametys.form.ConfigurableFormPanel} form The configurable form panel
* @param {Ext.data.Store} store The search store
* @private
*/
_navHandler: function (index, grid, form, store)
{
var box = grid.findParentByType("dialog");
if (index == 1)
{
if (!form.isValid())
{
return;
}
grid.getSelectionModel().clearSelections();
store.loadPage(1, {
scope: this,
callback: function(records, operation, success) {
if (success && records.length > 1)
{
grid.getSelectionModel().select(records[0]);
grid.getView().focusRow(0);
}
}
});
box.defaultButton = 'validate';
}
box.getLayout().setActiveItem(index);
if (index == 0)
{
form.focus();
box.defaultButton = 'next';
}
box.getDockedItems('toolbar[dock="bottom"]')[0].items.get(0).setDisabled(index==0);
box.getDockedItems('toolbar[dock="bottom"]')[0].items.get(1).setDisabled(index==1);
box.getDockedItems('toolbar[dock="bottom"]')[0].items.get(2).setDisabled(index==0);
},
/**
* Function called before loading the store
* @param {Ext.data.Store} store The search store
* @param {Ext.data.operation.Operation} operation The object that will be passed to the Proxy to load the store
* @param {Ametys.form.ConfigurableFormPanel} form The configurable form panel
* @param {String} contentType The id of content type to restrict the search on. Can be null.
* @param {Boolean} excludeSubContents True to exclude sub-contents from search
* @param {String} modelId The id of search model to base search on.
* @param {Object} contextualParameters The contextual parameters used to get search model and search results.
* @private
*/
_onBeforeLoad: function (store, operation, form, excludeSubContents, contentType, modelId, contextualParameters)
{
operation.setParams(Ext.apply(operation.getParams() || {}, {
values: form.getJsonValues(),
excludeSubContents: excludeSubContents,
restrictedContentTypes: contentType != null ? [contentType] : null,
model: modelId,
contextualParameters: contextualParameters
}));
},
/**
* Listener when the selection has changed in the grid
* @param {Ext.selection.Model} sm The selection model
* @param { Ext.data.Model[]} selected The selected records
* @param {Ametys.form.ConfigurableFormPanel} form The configurable form panel
* @private
*/
_onSelectionChange: function (sm, selected, form)
{
form.findParentByType("dialog").getDockedItems('toolbar[dock="bottom"]')[0].items.get(2).setDisabled(selected.length == 0);
},
/**
* This function is called when validating the dialog box
* @param {Ext.grid.Panel} grid The search grid
* @param {Boolean} fullContents True to returns {@link Ametys.cms.content.Content}. If false only the content identifiers will be returned
* @param {Boolean} multiple True to allow multiple selections
* @param {Function} cbFn The callback function to call after choosing contents or canceling the contents search
* @private
*/
_validate: function (grid, fullContents, multiple, cbFn)
{
var contentIds = [];
var selection = grid.getSelectionModel().getSelection();
if (selection.length == 0)
{
return;
}
for (var i=0; i < selection.length; i++)
{
contentIds.push(selection[i].get('id'));
}
if (cbFn)
{
if (fullContents)
{
Ametys.cms.content.ContentDAO.getContents(contentIds, Ext.bind(this._callCbFullContents, this, [multiple, cbFn], 1));
}
else
{
multiple ? Ext.Function.defer (cbFn, 0, null, [contentIds]) : Ext.Function.defer (cbFn, 0, null, [[contentIds[0]]]);
}
}
this._validated = true;
grid.findParentByType("dialog").close();
},
/**
* @private
* Invoke the callback function, providing content objects.
* @param {Array} contents The content objects.
* @param {Boolean} multiple True to allow multiple selections
* @param {Function} cbFn The callback function to call after choosing contents or canceling the contents search
*/
_callCbFullContents: function(contents, multiple, cbFn)
{
multiple ? Ext.Function.defer(cbFn, 0, null, [contents]) : Ext.Function.defer(cbFn, 0, null, [[contents[0]]]);
},
/**
* @private
* Listener when closing the dialog box
* @param {Function} cbFn The callback function to call after choosing contents or canceling the contents search
*/
_onClose: function(cbFn)
{
if (!this._validated && cbFn)
{
Ext.Function.defer (cbFn, 0, null, [null]);
}
this._validated = false;
},
/**
* Get the store the result grid should use as its data source
* @param {Ametys.form.ConfigurableFormPanel} form The configurable form panel
* @param {Boolean} excludeSubContents True to exclude sub-contents from search
* @param {String} contentType The id of content type to restrict the search on. Can be null.
* @param {String} modelId The id of search model to base search on.
* @param {Object} contextualParameters The contextual parameters used to get search model and search results.
* @private
*/
_getStore: function (form, excludeSubContents, contentType, modelId, contextualParameters)
{
return Ext.create('Ext.data.Store', {
remoteSort: true,
proxy: {
type: 'ametys',
plugin: 'cms',
url: 'search/list.json',
reader: {
type: 'json',
rootProperty: 'contents'
}
},
autoDestroy: true,
pageSize: this.PAGE_SIZE,
sortOnLoad: true,
listeners: {
'beforeload': {
fn: Ext.bind(this._onBeforeLoad, this, [form, excludeSubContents, contentType, modelId, contextualParameters], 2),
scope: this
}
}
});
}
});