Ext.namespace('Zarafa.advancesearch.ui');
/**
* @class Zarafa.advancesearch.ui.SearchGrid
* @extends Zarafa.common.ui.grid.MapiMessageGrid
* @xtype zarafa.searchgrid
*/
Zarafa.advancesearch.ui.SearchGrid = Ext.extend(Zarafa.common.ui.grid.MapiMessageGrid, {
/**
* @cfg {Zarafa.advancesearch.AdvanceSearchContext} searchContext The searchContext to which this panel belongs
*/
searchContext : undefined,
/**
* The {@link Zarafa.advancesearch.AdvanceSearchContextModel} which is obtained from the {@link #context}.
* @property
* @type Zarafa.advancesearch.AdvanceSearchContextModel
*/
model : undefined,
/**
* @constructor
* @param config Configuration structure
*/
constructor : function(config)
{
config = config || {};
if (!Ext.isDefined(config.model) && Ext.isDefined(config.searchContext)) {
config.model = config.searchContext.getModel();
}
if (!Ext.isDefined(config.store) && Ext.isDefined(config.model)) {
/**
* For the first search tab we have already created store instance in model, which is created at the time of the
* search context model initialization so we use that store for the first search tab and for the second search tab
* we create the new instance of the search store and so on.
*/
if(!Ext.isDefined(config.model.store.getSearchStoreUniqueId())){
var model = config.model;
model.store.searchStoreUniqueId = config.searchTabId;
config.store = model.store;
model.pushStore(config.searchTabId , model.store);
} else {
config.store = config.model.createNewSearchStore({searchTabId : config.searchTabId});
}
}
config.store = Ext.StoreMgr.lookup(config.store);
var searchFolder = config.searchCenterPanel.searchPanel.searchFolder;
if(Ext.isDefined(searchFolder)) {
config.store.setSearchStoreEntryId(searchFolder.get('store_entryid'));
config.store.setSearchEntryId(searchFolder.get('entryid'));
}
Ext.applyIf(config, {
xtype: 'zarafa.searchgrid',
cls: 'zarafa-searchgrid',
border : false,
// Don't make the search grid stateful. By default we always want Kopano Core to sort new search results on relevance.
stateful : false,
statefulRelativeDimensions : false,
loadMask : this.initLoadMask(),
sm : this.initSelectionModel(),
cm : new Zarafa.advancesearch.ui.SearchGridColumnModel({
grid: this,
folder : config.model.getDefaultFolder()
}),
enableDragDrop : true,
ddGroup : 'dd.mapiitem',
viewConfig : this.initViewConfig(),
enableColumnHide: false,
enableColumnMove: false,
enableColumnResize: false,
enableHdMenu: false,
autoExpandMin : 200,
// The maximum number of records that the store can hold and still be sortable
//TODO: This value should probably be configurable, but let's not do that until we are absolutely sure
// about doing the sorting this way.
sortableRecordsMax : 500
});
Zarafa.advancesearch.ui.SearchGrid.superclass.constructor.call(this, config);
},
/**
* Initialize event handlers
* @private
*/
initEvents : function()
{
Zarafa.advancesearch.ui.SearchGrid.superclass.initEvents.call(this);
this.on({
'headerclick': this.onHeaderClick,
'cellclick': this.onCellClick,
'rowcontextmenu': this.onRowContextMenu,
'rowdblclick': this.onRowDblClick,
scope : this
});
this.mon(this.getView(), 'livescrollstart', this.onLiveScrollStart, this);
this.mon(this.getView(), 'beforesort', this.onBeforeSort, this);
// Add a buffer to the following 2 event handlers. These are influenced by Extjs when a record
// is removed from the store. However removing of records isn't performed in batches. This means
// that we need to offload the event handlers attached to removing of records in case that
// a large batch of records is being removed.
this.mon(this.getSelectionModel(), 'rowselect', this.onRowSelect, this, { buffer : 1 });
this.mon(this.getSelectionModel(), 'selectionchange', this.onSelectionChange, this, { buffer : 1 });
this.mon(this.model, 'searchstop', this.onSearchStop, this);
this.mon(this.searchContext, 'viewchange', this.onContextViewChange, this);
this.store.on('beforeupdatesearch', this.onBeforeUpdateSearch, this);
},
/**
* Initialize the {@link Ext.grid.GridPanel.loadMask} field
*
* @return {Ext.LoadMask} The configuration object for {@link Ext.LoadMask}
* @private
*/
initLoadMask : function()
{
return {
msg : _('Loading Search results') + '...'
};
},
/**
* Initialize the {@link Ext.grid.GridPanel.sm SelectionModel} field
*
* @return {Ext.grid.RowSelectionModel} The subclass of {@link Ext.grid.AbstractSelectionModel}
* @private
*/
initSelectionModel : function()
{
return new Zarafa.advancesearch.ui.AdvanceSearchRowSelectionModel({
singleSelect : false
});
},
/**
* Initialize the {@link Ext.grid.GridPanel#viewConfig} field
*
* @return {Ext.grid.GridView} The configuration object for {@link Ext.grid.GridView}
* @private
*/
initViewConfig : function()
{
return {
getRowClass : this.viewConfigGetRowClass,
// We need a rowselector depth of 15 because of the nested
// table in the rowBody.
rowSelectorDepth : 15
};
},
/**
* Apply custom style and content for the row body. This will always
* apply the Read/Unread style to the entire row.
*
* @param {Ext.data.Record} record The {@link Ext.data.Record Record} corresponding to the current row.
* @param {Number} rowIndex The row index
* @param {Object} rowParams A config object that is passed to the row template during
* rendering that allows customization of various aspects of a grid row.
* If enableRowBody is configured true, then the following properties may be set by this function,
* and will be used to render a full-width expansion row below each grid row.
* @param {Ext.data.Store} store The Ext.data.Store this grid is bound to
* @return {String} a CSS class name to add to the row
* @private
*/
viewConfigGetRowClass : function(record, rowIndex, rowParams, store)
{
var cssClass = (Ext.isFunction(record.isRead) && !record.isRead() ? 'mail_unread' : 'mail_read');
return 'x-grid3-row-collapsed ' + cssClass;
},
/**
* Event handler which is fired when the currently active view inside the {@link #context}
* has been updated. This will update the call
* {@link #viewPanel}#{@link Zarafa.core.ui.SwitchViewContentContainer#switchView}
* to make the requested view active.
*
* @param {Zarafa.core.Context} context The context which fired the event.
* @param {Zarafa.common.data.Views} newView The ID of the selected view.
* @param {Zarafa.common.data.Views} oldView The ID of the previously selected view.
*/
onContextViewChange : function(context, newView, oldView)
{
if(oldView === Zarafa.common.data.Views.LIVESCROLL) {
this.getView().resetScroll();
}
},
* Event handler for a click on a column header. If a search suggestion is shown
* it will trigger a new search with the suggestion.
* @param {Zarafa.advancesearch.ui.SearchGrid} grid The search grid
* @param {Number} columnIndex The index of the column of which the header was clicked
* @param {Event} event The click event
*/
onHeaderClick: function(grid, columnIndex, event)
{
var suggestion = this.store.suggestion;
// Don't do anything if there is no suggestion or if the click was not on the suggestion text
if ( Ext.isEmpty(suggestion) || event.target.className!=='zarafa-search-suggestion' ){
return;
}
var searchToolbarPanel = this.searchCenterPanel.searchPanel.searchToolbar;
var searchField = searchToolbarPanel.getAdvanceSearchField();
searchField.setValue(suggestion);
searchField.onTriggerClick();
},
/**
* Raw click event handler for the entire grid.
* Toggles the unread/read status when a user clicks on the
* mail icon of a message.
*
* @param {Zarafa.advancesearch.ui.SearchGrid} grid the grid
* @param {Number} rowIndex The index of the clicked row
* @param {Number} columnIndex The column index of the clicked row
* @param {Ext.EventObject} e The click eventobject
*/
onCellClick : function(grid, rowIndex, columnIndex, e)
{
var record = this.store.getAt(rowIndex);
if (!Ext.isDefined(record) || record.get('message_class') != 'IPM.Note'){
return;
}
// Because we render a cell with lots of data, we must calculate ourself
// if the click was on the icon cell.
var clickX = e.xy[0];
var iconCell = e.target;
if ( !iconCell.classList.contains('icon') ){
iconCell = iconCell.querySelector('td.icon');
if ( iconCell === null ){
return;
}
}
var iconCellX = iconCell.getBoundingClientRect().left;
var iconCellWidth = iconCell.getBoundingClientRect().width;
if ( clickX >= iconCellX && clickX <= iconCellX+iconCellWidth ){
Zarafa.common.Actions.markAsRead(record, !record.isRead());
}
},
* Event handler which is triggered when the user opems the context menu.
*
* There are some selection rules regarding the context menu. If no rows where
* selected, the row on which the context menu was requested will be marked
* as selected. If there have been rows selected, but the context menu was
* requested on a different row, then the old selection is lost, and the new
* row will be selected. If the row on which the context menu was selected is
* part of the previously selected rows, then the context menu will be applied
* to all selected rows.
*
* @param {Zarafa.advancesearch.ui.SearchGrid} grid The grid which was right clicked
* @param {Number} rowIndex The index number of the row which was right clicked
* @param {Ext.EventObject} event The event structure
* @private
*/
onRowContextMenu : function(grid, rowIndex, event)
{
var sm = this.getSelectionModel();
if (sm.hasSelection()) {
// Some records were selected...
if (!sm.isSelected(rowIndex)) {
// But none of them was the record on which the
// context menu was invoked. Reset selection.
sm.clearSelections();
sm.selectRow(rowIndex);
}
} else {
// No records were selected,
// select row on which context menu was invoked
sm.selectRow(rowIndex);
}
var records = sm.getSelections();
Zarafa.core.data.UIFactory.openDefaultContextMenu(records, { position : event.getXY(), context : this.searchContext });
},
/**
* Event handler which is triggered when the user double-clicks on a particular item in the
* grid. This will open a contentpanel which contains the selected item.
*
* @param {Zarafa.advancesearch.ui.SearchGrid} grid The Grid on which the user double-clicked
* @param {Number} rowIndex The Row number on which was double-clicked.
* @param {Ext.EventObject} e The event object
* @private
*/
onRowDblClick : function(grid, rowIndex, e)
{
Zarafa.common.Actions.openMessageContent(this.getSelectionModel().getSelected());
},
/**
* Event handler which is trigggerd when the user selects a row from the {@link Ext.grid.GridPanel}.
* This will updates the {@link Zarafa.advancesearch.AdvanceSearchContextModel AdvanceSearchContextModel} with the record which
* was selected in the grid for preview
*
* @param {Ext.grid.RowSelectionModel} selectionModel The selection model used by the grid.
* @param {Integer} rowNumber The row number which is selected in the selection model
* @param {Ext.data.Record} record The record which is selected for preview.
* @private
*/
onRowSelect : function(selectionModel, rowNumber, record)
{
var count = selectionModel.getCount();
if (count === 0) {
this.model.setPreviewRecord(undefined);
} else if (count == 1 && selectionModel.getSelected() === record) {
this.model.setPreviewRecord(record);
}
},
/**
* Event handler which is triggered when the {@link Zarafa.advancesearch.ui.SearchGrid grid}
* {@link Zarafa.core.data.IPMRecord record} selection is changed. This will inform
* the {@link Zarafa.advancesearch.AdvanceSearchContextModel contextmodel} about the change.
*
* @param {Ext.grid.RowSelectionModel} selectionModel The selection model used by the grid.
* @private
*/
onSelectionChange : function(selectionModel)
{
var selections = selectionModel.getSelections();
this.model.setSelectedRecords(selections);
if (Ext.isEmpty(selections)) {
this.model.setPreviewRecord(undefined);
}
},
/**
* Event handler which triggered when scrollbar gets scrolled more then 90% of it`s height.
* it will be used to start live scroll on {@link Zarafa.core.data.ListModuleStore ListModuleStore}.
* also it will register event on {@link Zarafa.core.data.ListModuleStore ListModuleStore} to get
* updated batch of search result status.
*
* @param {Number} cursor the cursor contains the last index of record in grid.
* @private
*/
onLiveScrollStart : function(cursor)
{
this.model.startLiveScroll(cursor);
},
/**
* Event handler which triggered when header of grid was clicked to apply the sorting
* on {@link Zarafa.advancesearch.ui.SearchGrid search grid}. it will first stop the
* {@link Zarafa.core.ContextModel#stopLiveScroll live scroll} and then apply the sorting.
* @param {Ext.grid.GridView} gridView The GridView of the grid panel
* @private
*/
onBeforeSort : function(gridView)
{
// Only check when sorting on the second row, because the first row does not have sorting enabled
if ( gridView.activeHdIndex === 1 ){
// Check if the total number of results is less then the maximum for which we will enable sorting
var store = this.getStore();
var recordTotalCount = store.getTotalCount();
if ( recordTotalCount >= this.sortableRecordsMax ){
Zarafa.common.dialogs.MessageBox.alert(
_('Sorting not possible'),
String.format(_('The search results could not be sorted because the number of items is higher than {0}. Narrow down your results by enabling more filters or by adjusting your input.'), this.sortableRecordsMax));
return false;
}
// Stop the infinite scrolling if it is running
this.model.stopLiveScroll();
// Because we are sorting on the searchdate property and this property is added in the frontend
// we cannot do remote sorting and will change to local sorting
store.remoteSort = false;
// Reset the page size so the pagination toolbar (if shown) will show all results in one page.
var pagesToolbar = this.dialog.findByType('zarafa.paging')[0];
pagesToolbar.pageSize = this.sortableRecordsMax;
var loadedRecordCount = store.getCount();
// If necessary we must first load all records from the store before we can do the sorting
if (loadedRecordCount < recordTotalCount){
// cancel pending load request when sorting is started.
// NOTE: 'updatelist' is already handled when we stop infinite scroll
if (store.isExecuting('list')) {
store.proxy.cancelRequests('list');
}
// Set the options for the load request
var options = {
actionType : Zarafa.core.Actions['list'],
folder: this.model.getFolders()[0],
params: {
restriction: {
start: 0,
limit: recordTotalCount
}
},
callback: function(){
// Do the sorting again, because Ext tried to do the sorting before we reloaded
store.sort(store.sortInfo.field, store.sortInfo.direction);
}
};
store.load(options);
}
}
},
/**
* Event handler for the {@link Zarafa.core.ContextModel#searchstop searchstop} event.
* This will {@link Zarafa.common.ui.LoadMask#hide hide} the {@link Zarafa.common.ui.LoadMask loadmask}, if any.
* @param {Zarafa.core.ContextModel} model The model which fired the event
* @private
*/
onSearchStop : function(model)
{
this.loadMask.hide();
},
/**
* Handler for the beforeupdatesearch event of the search store of the grid
* @param {Zarafa.advancesearch.AdvanceSearchStore} store The store that holds the data
* of this grid.
* @param {Object} searchMeta The returned meta data of the search that was performed
*/
onBeforeUpdateSearch: function(store, searchMeta)
{
// Don't update for the updatesearch action because the suggestion is not send
// with the response
if ( Ext.isDefined(store.lastOptions) && store.lastOptions.originalActionType!==Zarafa.core.Actions.updatesearch ){
var cm = this.getColumnModel();
if ( Ext.isEmpty(store.suggestion) ){
cm.setColumnHeader(0, '');
cm.setColumnTooltip(0, '');
} else {
var encodedSuggestion = Ext.util.Format.htmlEncode(store.suggestion);
cm.setColumnHeader(0, _('Did you mean') +' <span class="zarafa-search-suggestion">' + encodedSuggestion + '</span> ?');
cm.setColumnTooltip(0, _('Did you mean') + ' ' + encodedSuggestion);
}
}
}
});
Ext.reg('zarafa.searchgrid', Zarafa.advancesearch.ui.SearchGrid);