/*
* 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 tool displays the list of users
* @private
*/
Ext.define('Ametys.plugins.coreui.users.UsersTool', {
extend: 'Ametys.tool.Tool',
statics: {
/**
* @property {Number} RESULT_LIMIT
* @readonly
* @static
* The maximum number of records to search for
*/
RESULT_LIMIT: 100,
/**
* @property {String} ANY_DIRECTORY_OPTION_ID The id of the '-' options.
* @private
* @readonly
* @static
*/
ANY_DIRECTORY_OPTION_ID: '-',
},
/**
* @property {Ext.data.Store} _store The store with the user records
* See {@link Ametys.plugins.coreui.users.UsersTool.User}
* @private
*/
/**
* @property {Ext.grid.Panel} _grid The grid panel displaying the users
* @private
*/
/**
* @property {Ext.form.field.Text} _searchField The search fields filter
* @private
*/
/**
* @property {String} _previousSearchValue The value of the search field used during the last load request.
* @private
*/
/**
* @property {String} _userTargetId the message target type for users
* @private
*/
/**
* @property {String[]} _contexts The contexts for the populations to display in the combobox.
* @private
*/
/**
* @property {Boolean} _enableAllPopulationsOption True to add an option in the populations combobox for searching over all the populations.
* @private
*/
/**
* @property {String} _allPopulationsOptionId The id of the 'all populations' options.
* @private
* @readonly
*/
_allPopulationsOptionId: '#all',
/**
* @property {Boolean} _showDirectoryCombobox True to show the user directory combobox field
* @private
*/
/**
* @property {Boolean} _showDirectoryColumn True to show the user directory column
* @private
*/
constructor: function(config)
{
this.callParent(arguments);
// Set the role and the message target type
this._userTargetId = config['message-target-id'] || Ametys.message.MessageTarget.USER;
// Listening to some bus messages.
Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onMessageCreated, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onMessageEdited, this);
Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onMessageDeleted, this);
},
getMBSelectionInteraction: function()
{
return Ametys.tool.Tool.MB_TYPE_ACTIVE;
},
/**
* @inheritdoc
* @param {Object} [params] The params. See inheritance
* @param {Object[]} [params.selectedUsers] The users to initially select
* @param {String[]} [params.contexts] The contexts for the populations to display in the combobox. Default to the current contexts.
* @param {Boolean/String} [params.enableAllPopulationsOption=false] True to add an option in the populations combobox for searching over all the populations.
* @param {Boolean/String} [params.showDirectoryColumn=false] True to show the user directory column
* @param {Boolean/String} [params.showDirectoryCombobox=false] True to show the user directory combobox field
*/
setParams: function(params)
{
this.callParent(arguments);
this._initialSelectedUsers = params.selectedUsers || [];
this._contexts = Ext.Array.from(params.contexts || Ametys.getAppParameter('populationContexts'));
this._userPopulationOnly = Ext.isBoolean(params.userPopulationOnly) ? params.userPopulationOnly : params.userPopulationOnly == "true";
this._enableAllPopulationsOption = !this._userPopulationOnly && Ext.isBoolean(params.enableAllPopulationsOption) ? params.enableAllPopulationsOption : params.enableAllPopulationsOption == "true";
this._showDirectoryColumn = Ext.isBoolean(params.showDirectoryColumn) ? params.showDirectoryColumn : params.showDirectoryColumn == "true";
this._grid.down('[dataIndex=directory]').setVisible(this._showDirectoryColumn);
this._showDirectoryCombobox = Ext.isBoolean(params.showDirectoryCombobox) ? params.showDirectoryCombobox : params.showDirectoryCombobox == "true";
this.showOutOfDate();
},
refresh: function()
{
this._userDirectoriesField.setVisible(this._showDirectoryCombobox);
this.showRefreshing();
this._loadPopulations(Ext.bind(this.showRefreshed, this));
},
createPanel: function()
{
this._userPopulationsField = Ext.create('Ext.form.field.ComboBox', this._getPopulationComboboxCfg());
this._userDirectoriesField = Ext.create('Ext.form.field.ComboBox', this._getUserDirectoryComboboxCfg());
// Search input
this._searchField = Ext.create('Ext.form.TextField', {
xtype: 'textfield',
cls: 'ametys',
maxWidth: 250,
flex: 1,
emptyText: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_SEARCH_EMPTY_TEXT}}",
listeners: {change: Ext.Function.createBuffered(this._search, 500, this)}
});
this._store = this.createUserStore();
this._grid = Ext.create("Ext.grid.Panel", {
store: this._store,
scrollable: true,
stateful: true,
stateId: this.getId() + "$grid",
columns: [
{header: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_COL_NAME}}", width: 250, sortable: true, dataIndex: 'displayName', renderer: Ext.bind(this._renderDisplayName, this), hideable: false},
{header: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_COL_LOGIN}}", width: 200, sortable: true, dataIndex: 'login'},
{header: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_COL_POPULATION}}", width: 200, sortable: true, dataIndex: 'populationLabel'},
{header: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_COL_EMAIL}}", flex: 1, sortable: true, dataIndex: 'email'},
{header: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_COL_DIRECTORY}}", width: 200, sortable: true, dataIndex: 'directory', hidden: true}, // hidden by default, will be shown if told
{header: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_COL_CREATION_DATE}}", width: 160, sortable: true, dataIndex: 'creationDate', renderer: Ametys.grid.GridColumnHelper.renderDateTime},
{header: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_COL_CREATION_ORIGIN}}", width: 180, sortable: true, dataIndex: 'creationOrigin', renderer: this._renderCreationOrigin}
],
selModel : {
mode: 'MULTI'
},
dockedItems: [
{
dock: 'top',
ui: 'tool-hintmessage',
itemId: 'stored-infos-hint',
xtype: 'component',
html: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_STORED_INFOS_HINT}}"
},
{
xtype: 'toolbar',
layout: {
type: 'hbox',
align: 'stretch'
},
dock: 'top',
items: [
this._userPopulationsField,
this._userDirectoriesField
],
listeners: {
'resize': function(toolbar, width, height) {
if (width > 450)
{
this._userPopulationsField.setHideLabel(false);
this._userDirectoriesField.setHideLabel(false);
this._userPopulationsField.setMaxWidth(this._userPopulationsField.getInitialConfig('maxWidth'));
this._userDirectoriesField.setMaxWidth(this._userDirectoriesField.getInitialConfig('maxWidth'));
toolbar.getLayout().setVertical(false);
}
else if (width > 300)
{
this._userPopulationsField.setHideLabel(false);
this._userDirectoriesField.setHideLabel(false);
this._userPopulationsField.setMaxWidth(null);
this._userDirectoriesField.setMaxWidth(null);
toolbar.getLayout().setVertical(true);
}
else
{
this._userPopulationsField.setHideLabel(true);
this._userDirectoriesField.setHideLabel(true);
this._userPopulationsField.setMaxWidth(null);
this._userDirectoriesField.setMaxWidth(null);
toolbar.getLayout().setVertical(true);
}
},
scope: this
}
}, {
xtype: 'toolbar',
layout: {
type: 'hbox',
align: 'stretch'
},
dock: 'top',
items: [
this._searchField,
{
flex: 1,
html: "<span title=\"{{i18n PLUGINS_CORE_UI_TOOL_USERS_SEARCH_LIMIT_HELPTEXT}}\">{{i18n PLUGINS_CORE_UI_TOOL_USERS_SEARCH_LIMIT_HELPTEXT}}</span>",
xtype: 'component',
cls: 'a-toolbar-text'
}
],
listeners: {
'resize': function(toolbar, width, height) {
toolbar.getLayout().setVertical(width <= 200);
},
scope: this
}
}
],
listeners: {
selectionchange: {fn: this._onSelectionChange, scope: this}
},
viewConfig: {
plugins: {
ptype: 'ametysgridviewdragdrop',
dragTextField: 'login',
setAmetysDragInfos: Ext.bind(this.getDragInfo, this),
setAmetysDropZoneInfos: Ext.emptyFn
}
}
});
return this._grid;
},
sendCurrentSelection: function()
{
var selection = this._grid.getSelectionModel().getSelection();
var targets = [];
var me = this;
Ext.Array.forEach(selection, function(record) {
targets.push({
id: me._userTargetId,
parameters: {
id: record.get('login'),
populationId: record.get('populationId')
}
})
}, this);
Ext.create('Ametys.message.Message', {
type: Ametys.message.Message.SELECTION_CHANGED,
targets: targets
});
},
/**
* Create the user store that the grid should use as its data source.
* @return {Ext.data.Store} The created store
*/
createUserStore: function ()
{
// Merge default store configuration with inherited configuration (provided by #getStoreConfig).
var storeConfig = Ext.merge({
autoLoad: false,
model: 'Ametys.plugins.coreui.users.UsersTool.User',
proxy: {
type: 'ametys',
reader: {
type: 'json',
rootProperty: 'users'
}
},
remoteSort: false,
sortOnLoad: true,
sorters: [{property: 'displayName', direction:'ASC'}],
listeners: {
beforeload: {fn: this._onBeforeLoad, scope: this},
load: {fn: this._onLoad, scope: this}
}
}, this.getStoreConfig());
return Ext.create('Ext.data.Store', storeConfig);
},
/**
* Returns the elements of configuration of user store to be overridden.
* Override this function if you want to override the user store configuration.
* @return {Object} The elements of store configuration to be overridden
*/
getStoreConfig: function()
{
return {
proxy: {
type: 'ametys',
role: "org.ametys.plugins.core.user.UserDAO",
// No method name because we don't always call the same one
cancelOutdatedRequest: true
}
};
},
/**
* @private
* Get the configuration object for creating the combobox for the user populations
* @return {Object} The configuration
*/
_getPopulationComboboxCfg: function()
{
return {
xtype: 'combobox',
fieldLabel: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_POPULATION_FIELD}}",
name: "userPopulations",
cls: 'ametys',
labelWidth: 75,
maxWidth: 275,
flex: 275,
store: {
fields: ['id', {name: 'label', type: 'string', convert: function(v) {return Ext.String.escapeHtml(v)}}],
proxy: {
type: 'ametys',
plugin: 'core-ui',
url: 'populations.json',
reader: {
type: 'json',
rootProperty: 'userPopulations'
}
},
sorters: [{property: 'label', direction: 'ASC'}],
listeners: {
'beforeload': {fn: this._onBeforeLoadPopulations, scope: this},
'load': {fn: this._onLoadPopulations, scope: this}
}
},
valueField: 'id',
displayField: 'label',
queryMode: 'local',
forceSelection: true,
triggerAction: 'all',
listeners: {
'change': {fn: this._onChangePopulation, scope: this}
}
};
},
/**
* @private
* Get the configuration object for creating the combobox for the user directories
* @return {Object} The configuration
*/
_getUserDirectoryComboboxCfg: function()
{
return {
fieldLabel: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_USER_DIRECTORY_FIELD}}",
name: "userDirectories",
cls: 'ametys',
labelWidth: 150,
maxWidth: 350,
flex: 350,
hidden: true, // hidden by default, will be shown if told
store: {
fields: ['id', {name: 'label', type: 'string'}],
data: [],
sorters: [{property: 'label', direction: 'ASC'}],
listeners: {
'datachanged': Ext.bind(function(store) {
this._userDirectoriesField.clearValue();
this._userDirectoriesField.setValue(Ametys.plugins.coreui.users.UsersTool.ANY_DIRECTORY_OPTION_ID);
}, this)
}
},
valueField: 'id',
displayField: 'label',
queryMode: 'local',
forceSelection: true,
triggerAction: 'all',
listeners: {
'change': Ext.Function.createBuffered(this._search, 500, this)
}
};
},
/**
* @private
* 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 = [];
Ext.Array.each(item.records, function(record) {
targets.push({
id: this._userTargetId,
parameters: {
id: record.get('login'),
populationId: record.get('populationId')
}
});
}, this);
if (targets.length > 0)
{
item.source = {
relationTypes: [Ametys.relation.Relation.REFERENCE],
targets: targets
};
}
},
/**
* Get the user store
* @return {Ext.data.Store} The user store
*/
getStore: function ()
{
return this._store;
},
/**
* Function called before loading the population store
* @param {Ext.data.Store} store The store
* @param {Ext.data.operation.Operation} operation The object that will be passed to the Proxy to load the store
* @private
*/
_onBeforeLoadPopulations: function(store, operation)
{
operation.setParams( Ext.apply(operation.getParams() || {}, {
contexts: this._contexts,
userPopulationOnly: this._userPopulationOnly
}));
},
/**
* @private
* Listener invoked after loading populations
* @param {Ext.data.Store} store The store
* @param {Ext.data.Model[]} records The records of the store
*/
_onLoadPopulations: function(store, records)
{
if (this._enableAllPopulationsOption)
{
// Add an option in the populations combobox for searching over all the populations
store.add({
id: this._allPopulationsOptionId,
label: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_POPULATION_FIELD_OPTION_ALL}}"
});
}
},
/**
* @private
* Function called when the value of the population combobox field changed.
* @param {Ext.form.field.ComboBox} combo The combobox
* @param {String} newValue The new value
* @param {String} oldValue The original value
*/
_onChangePopulation: function(combo, newValue, oldValue)
{
if (newValue == this._allPopulationsOptionId)
{
// Search over all the populations
this._userDirectoriesField.setDisabled(true);
Ext.defer(this._search, 500, this);
return;
}
else
{
this._userDirectoriesField.setDisabled(false);
}
// Populate the user directories combobox store
var data = [{
id: Ametys.plugins.coreui.users.UsersTool.ANY_DIRECTORY_OPTION_ID,
label: "{{i18n PLUGINS_CORE_UI_TOOL_USERS_USER_DIRECTORY_FIELD_OPTION_ALL}}"
}];
var record = combo.getStore().getById(newValue);
if (record != null)
{
Ext.Array.forEach(record.get('userDirectories'), function(item, index) {
data.push({
id: item.id,
label: item.label
});
}, this);
}
this._userDirectoriesField.getStore().loadData(data, false);
},
/**
* @private
* Load the store of the populations combobox.
* @param {Function} [callback] The callback function
*/
_loadPopulations: function(callback)
{
this._userPopulationsField.getStore().load({
scope: this,
callback: function(records) {
// When store loaded, select the 'all' option if it is available AND there are more than one population
if (this._enableAllPopulationsOption && records.length > 1)
{
this._userPopulationsField.select(this._allPopulationsOptionId);
}
// Otherwise select the fist data
else if (records.length > 0)
{
this._userPopulationsField.select(records[0].get('id'));
}
// If there is one and only one population, hide the combobox
var hide = records.length == 1;
this._userPopulationsField.setHidden(hide);
// What's more, if this population has one directory, hide the user directory combobox
if (hide && this._userDirectoriesField.getStore().getRange().length == 2)
{
this._userDirectoriesField.setHidden(true);
}
// Callback function
if (Ext.isFunction(callback))
{
callback();
}
}
});
},
/**
* @private
* Load the user store
*/
_search: function()
{
this._store.load();
},
/**
* @private
* Renderer for user's full name
* @param {Object} value The data value
* @param {Object} metaData A collection of data about the current cell
* @param {Ext.data.Model} record The record
* @return {String} The html value to render.
*/
_renderDisplayName: function(value, metaData, record)
{
if (this._userTargetId == Ametys.message.MessageTarget.USER)
{
return Ametys.grid.GridColumnHelper.renderUser.apply(this, arguments);
}
else
{
return`<img src="${Ametys.helper.Users.getUserImage(null, null, 16)}" class="a-grid-icon a-grid-icon-user"/>` + Ext.String.escapeHtml(value);
}
},
/**
* @private
* Renderer for creation origin enumerator
* @param {Object} value The data value
* @param {Object} metaData A collection of data about the current cell
* @param {Ext.data.Model} record The record
* @return {String} The html value to render.
*/
_renderCreationOrigin: function(value, metaData, record)
{
if (value == 'USER_SIGNUP')
{
return "{{i18n PLUGINS_CORE_UI_TOOL_USERS_CREATION_ORIGIN_USER_SIGNUP}}";
}
if (value == 'ADMIN')
{
return "{{i18n PLUGINS_CORE_UI_TOOL_USERS_CREATION_ORIGIN_ADMIN}}";
}
if (value == 'SYSTEM')
{
return "{{i18n PLUGINS_CORE_UI_TOOL_USERS_CREATION_ORIGIN_SYSTEM}}";
}
return "-"; //Unknwon or not applicable
},
/**
* Gets the id of the population selected in the user population combobox of this tool.
* @return {String} The id of the population selected in the user population combobox
*/
getPopulationComboValue: function()
{
return this._userPopulationsField.getValue();
},
/**
* Gets the id of the user directory selected in the user directory combobox of this tool.
* @return {String} The id of the user directory selected in the user directory combobox
*/
getUserDirectoryComboValue: function()
{
return this._userDirectoriesField.getValue();
},
/**
* Function called before loading the store
* @param {Ext.data.Store} store The store
* @param {Ext.data.operation.Operation} operation The object that will be passed to the Proxy to load the store
* @param {Object} eOpts Event options
* @private
*/
_onBeforeLoad: function(store, operation, eOpts)
{
// If one of the two comboboxes is invalid, cancel the loading
if (this.getPopulationComboValue() == null
|| this.getPopulationComboValue() != this._allPopulationsOptionId && this.getUserDirectoryComboValue() == null)
{
return false;
}
var proxy = operation.getProxy();
// 'all' option is selected
if (this.getPopulationComboValue() == this._allPopulationsOptionId)
{
proxy.methodName = "searchUsersByContexts";
proxy.methodArguments = ['contexts', 'limit', 'offset', 'searchCriteria', 'limitedToStoredUserData'];
proxy.setExtraParam('contexts', this._contexts);
proxy.setExtraParam('limit', this.self.RESULT_LIMIT);
proxy.setExtraParam('searchCriteria', this._searchField.getValue());
proxy.setExtraParam('limitedToStoredUserData', true);
return true;
}
// If no user directory is requested ('-'), search by population
else if (this.getUserDirectoryComboValue() == Ametys.plugins.coreui.users.UsersTool.ANY_DIRECTORY_OPTION_ID)
{
proxy.methodName = "searchUsersByPopulation";
proxy.methodArguments = ['userPopulationId', 'limit', 'offset', 'searchCriteria', 'limitedToStoredUserData'];
proxy.setExtraParam('userPopulationId', this.getPopulationComboValue());
proxy.setExtraParam('limit', this.self.RESULT_LIMIT);
proxy.setExtraParam('searchCriteria', this._searchField.getValue());
proxy.setExtraParam('limitedToStoredUserData', true);
}
else // If a user directory is requested, search in this directory
{
proxy.methodName = "searchUsersByDirectory";
proxy.methodArguments = ['userDirectoryId', 'userPopulationId', 'limit', 'offset', 'searchCriteria', 'limitedToStoredUserData'];
proxy.setExtraParam('userDirectoryId', this.getUserDirectoryComboValue());
proxy.setExtraParam('userPopulationId', this.getPopulationComboValue());
proxy.setExtraParam('limit', this.self.RESULT_LIMIT);
proxy.setExtraParam('searchCriteria', this._searchField.getValue());
proxy.setExtraParam('limitedToStoredUserData', true);
}
},
/**
* @private
* Listener invoked after loading users
* @param {Ext.data.Store} store The store
* @param {Ext.data.Model[]} records The records of the store
* @param {Boolean} successful True if the operation was successful.
* @param {Ext.data.Operation} operation The operation that triggered this load.
*/
_onLoad: function(store, records, successful, operation)
{
if (operation.aborted)
{
// the load has been canceled. Do nothing.
return;
}
if (this._initialSelectedUsers.length > 0)
{
var records = [];
var sm = this._grid.getSelectionModel();
var store = this._grid.getStore();
Ext.Array.each (this._initialSelectedUsers, function (login) {
var id = store.find("id", login);
if (id != '-')
{
records.push(store.getAt(id));
}
});
sm.select(records);
this._initialSelectedUsers = []; // reset
}
},
/**
* Fires a event of selection on message bus, from the selected contents in the grid.
* @param {Ext.selection.Model} model The selection model
* @param {Ext.data.Model[]} selected The selected records
* @param {Object} eOpts Event options
* @private
*/
_onSelectionChange: function(model, selected, eOpts)
{
this.sendCurrentSelection();
},
/**
* Listener when a Ametys.message.Message#CREATED message was received
* @param {Ametys.message.Message} message The received message
* @private
*/
_onMessageCreated: function(message)
{
// Case creation of a population
var populationTargets = message.getTargets(Ametys.message.MessageTarget.USER_POPULATION);
if (populationTargets.length > 0)
{
this.showOutOfDate();
}
// Case creation of a user
var userTarget = message.getTarget(new RegExp('^' + this._userTargetId + '$'), 1);
if (userTarget)
{
var login = userTarget.getParameters().id;
var population = userTarget.getParameters().populationId;
if (this.getPopulationComboValue() == population || this.getPopulationComboValue() == this._allPopulationsOptionId)
{
// The tool is concerned by the message
this._search();
}
}
},
/**
* Listener when a Ametys.message.Message#MODIFIED message was received
* @param {Ametys.message.Message} message The received message
* @private
*/
_onMessageEdited: function(message)
{
// Case edition of a population
var populationTargets = message.getTargets(Ametys.message.MessageTarget.USER_POPULATION);
if (populationTargets.length > 0)
{
this.showOutOfDate();
}
// Case edition of a user
if (message != null && message.getParameters().major === true)
{
var userTarget = message.getTarget(new RegExp('^' + this._userTargetId + '$'), 1);
if (userTarget)
{
var login = userTarget.getParameters().id;
var populationId = userTarget.getParameters().populationId;
if ((this.getPopulationComboValue() == populationId || this.getPopulationComboValue() == this._allPopulationsOptionId) && this._findUserRecord(login, populationId))
{
// The tool is concerned by the message
this._search();
}
}
}
},
/**
* Listener when a Ametys.message.Message#DELETED message was received
* @param {Ametys.message.Message} message The received message
* @private
*/
_onMessageDeleted: function(message)
{
// Case deletion of a population
var populationTargets = message.getTargets(Ametys.message.MessageTarget.USER_POPULATION);
if (populationTargets.length > 0)
{
this.showOutOfDate();
}
// Case deletion of a user
var userTargets = message.getTargets(new RegExp('^' + this._userTargetId + '$'), 1);
var me = this;
Ext.Array.forEach(userTargets, function(target) {
var user = me._findUserRecord(target.getParameters().id, target.getParameters().populationId);
if (user && (target.getParameters().populationId == this.getPopulationComboValue() || this._allPopulationsOptionId == this.getPopulationComboValue()))
{
me._store.remove(user);
}
}, this);
},
/**
* @private
* Gets the first record from the user store that matches the given login and population id.
* @param {String} login The user login
* @param {String} populationId The population id
* @return {Ext.data.Model} the found record, or null if not found.
*/
_findUserRecord: function(login, populationId)
{
var foundRecord;
Ext.Array.each(this._store.getRange(), function(record) {
if (record.get('login') == login && record.get('populationId') == populationId)
{
foundRecord = record;
return false;
}
}, this);
return foundRecord;
}
});